S
SolidJS3mo ago
macrbe

App Structure

Hey, I am struggeling a bit with solid.js as it is new for me. I was hoping to get some guidance when it comes to project structure The project is a simple shop app (without login/auth) that uses and loads .json files along the way. So what I have done is create a cache inside products.store.js which caches the product json files:
...
const createProductFetcher = () => {
const cache = new Map<
string,
{ data: Resource<FullProduct>; refetch: () => void }
>();
const getFullProduct = (category: string, productSlug: string) => {
const key = category + "_" + productSlug;

if (!cache.has(key)) {
const [data, { refetch }] = createResource(
() => ({ category, productSlug }),
fetchProduct //fetches the json file
);
if (data) {
cache.set(key, { data, refetch });
}
}
return cache.get(key) as {
data: Resource<FullProduct>;
refetch: () => void;
};
};
return { getFullProduct };
};
export const { getFullProduct } = createProductFetcher();
...
const createProductFetcher = () => {
const cache = new Map<
string,
{ data: Resource<FullProduct>; refetch: () => void }
>();
const getFullProduct = (category: string, productSlug: string) => {
const key = category + "_" + productSlug;

if (!cache.has(key)) {
const [data, { refetch }] = createResource(
() => ({ category, productSlug }),
fetchProduct //fetches the json file
);
if (data) {
cache.set(key, { data, refetch });
}
}
return cache.get(key) as {
data: Resource<FullProduct>;
refetch: () => void;
};
};
return { getFullProduct };
};
export const { getFullProduct } = createProductFetcher();
so in the ProductDetails page component I use getFullProduct() to get the resource:
function ProductDetail({ category, slug }: { category: string; slug: string }) {
const productResource = () => {
return getFullProduct(category, slug);
};
...
function ProductDetail({ category, slug }: { category: string; slug: string }) {
const productResource = () => {
return getFullProduct(category, slug);
};
...
Now that works as aspected but I have no clue how to reuse that logic in a way that I can: 1. iterate over the cart items of localstorage on page load (no problem with this) 2. somehow load all the related Product json files of the cart in one go and wait for them to be loaded. 3. have those product json/data also being added to the above cache so they do not need to be loaded again when needed elsewhere. Meaning if after the cart is loaded I click on a cart item and navigate to the corresponding productDetail page, that data is retrieved from the cache, as with getFullProduct() Can anyone point me towards a working/cleaner solution? Much appriciated, Matt
1 Reply
Untlsn
Untlsn3mo ago
So you need to move your cache to global or to context and i sugest to make it store, so if you refetch then everythink subscribe to data will be notified. Assumption: You don't use SSR Maybe this will give you some ideas: Cache:
type ProductCacheItem = { data: Resource<FullProduct>, refetch: () => void };
type ProductCache = Record<string, ProductCacheItem>;

const ProductCacheContext = createContext<[get: Store<ProductCache>, set: SetStoreFunction<ProductCache>]>();

function ProductCacheProvider(props: FlowProps) {
return (
<ProductCacheContext.Provider value={createStore({})}>
{props.children}
</ProductCacheContext.Provider>
);
}

function useProductCache() {
const context = useContext(ProductCacheContext);
if (!context) throw new Error('ProductCacheProvider must be used in component tree');
return context;
}
type ProductCacheItem = { data: Resource<FullProduct>, refetch: () => void };
type ProductCache = Record<string, ProductCacheItem>;

const ProductCacheContext = createContext<[get: Store<ProductCache>, set: SetStoreFunction<ProductCache>]>();

function ProductCacheProvider(props: FlowProps) {
return (
<ProductCacheContext.Provider value={createStore({})}>
{props.children}
</ProductCacheContext.Provider>
);
}

function useProductCache() {
const context = useContext(ProductCacheContext);
if (!context) throw new Error('ProductCacheProvider must be used in component tree');
return context;
}
product fetch:
/**
* Convert category and productSlug to universal key
*/
function createProductKey(category: string, productSlug: string) {
return `${category}_${productSlug}`;
}

/**
* Mainly for internal usage
* Fetch product and add it to cache even if product is present in cache
*/
function useProductFetcher(category: string, productSlug: string) {
const [, setCache] = useProductCache();

const [data, { refetch }] = createResource(
() => ({ category, productSlug }),
fetchProduct,
);
setCache(createProductKey(category, productSlug), { data, refetch });
}

/**
* Get product based on category and productSlug
* It will handle caching new data;
*/
function useFetchFullProduct(category: string, productSlug: string) {
const [cache] = useProductCache();

const key = createProductKey(category, productSlug);

untrack(() => {
if (!cache[key]) useProductFetcher(category, productSlug);
});

return () => cache[key];
}
/**
* Convert category and productSlug to universal key
*/
function createProductKey(category: string, productSlug: string) {
return `${category}_${productSlug}`;
}

/**
* Mainly for internal usage
* Fetch product and add it to cache even if product is present in cache
*/
function useProductFetcher(category: string, productSlug: string) {
const [, setCache] = useProductCache();

const [data, { refetch }] = createResource(
() => ({ category, productSlug }),
fetchProduct,
);
setCache(createProductKey(category, productSlug), { data, refetch });
}

/**
* Get product based on category and productSlug
* It will handle caching new data;
*/
function useFetchFullProduct(category: string, productSlug: string) {
const [cache] = useProductCache();

const key = createProductKey(category, productSlug);

untrack(() => {
if (!cache[key]) useProductFetcher(category, productSlug);
});

return () => cache[key];
}
And selected products with localStorage sync
/**
* Create signal that get inital data from localStorage and sync data with it on every change
*/
function createLocalStorageSync<T>(key: string, transformIn: (value: string | null) => T, transformOut: (value: T) => string) {
const selectedProductsKeys = localStorage.getItem('selectedProductsKeys');

const [value, setValue] = createSignal<T>(
transformIn(selectedProductsKeys),
);

return [
value,
(setter: Exclude<T, Function> | ((prev: T) => T)) => {
setValue(setter);
transformOut(untrack(value));
},
] as const;
}

/**
* Controller for selected products.
*
*/
function useSelectedProducts() {
const [cache] = useProductCache();

const [selectedProducts, setSelectedProducts] = createLocalStorageSync(
'selectedProductsKeys',
(value) => value?.split(';').map((key) => {
const [category, productSlug] = key.split('_');
useProductFetcher(category, productSlug);
return key;
}) || [],
(arr) => arr.join(';'),
);

return [
() => selectedProducts().map((key) => cache[key]),
{
addProduct(category: string, productSlug: string) {
setSelectedProducts((old) => [...old, createProductKey(category, productSlug)]);
},
removeProduct(index: number) {
setSelectedProducts((old) => old.toSpliced(index, 1));
},
clear() {
setSelectedProducts([]);
},
},
];
}
/**
* Create signal that get inital data from localStorage and sync data with it on every change
*/
function createLocalStorageSync<T>(key: string, transformIn: (value: string | null) => T, transformOut: (value: T) => string) {
const selectedProductsKeys = localStorage.getItem('selectedProductsKeys');

const [value, setValue] = createSignal<T>(
transformIn(selectedProductsKeys),
);

return [
value,
(setter: Exclude<T, Function> | ((prev: T) => T)) => {
setValue(setter);
transformOut(untrack(value));
},
] as const;
}

/**
* Controller for selected products.
*
*/
function useSelectedProducts() {
const [cache] = useProductCache();

const [selectedProducts, setSelectedProducts] = createLocalStorageSync(
'selectedProductsKeys',
(value) => value?.split(';').map((key) => {
const [category, productSlug] = key.split('_');
useProductFetcher(category, productSlug);
return key;
}) || [],
(arr) => arr.join(';'),
);

return [
() => selectedProducts().map((key) => cache[key]),
{
addProduct(category: string, productSlug: string) {
setSelectedProducts((old) => [...old, createProductKey(category, productSlug)]);
},
removeProduct(index: number) {
setSelectedProducts((old) => old.toSpliced(index, 1));
},
clear() {
setSelectedProducts([]);
},
},
];
}
Want results from more Discord servers?
Add your server