I am passionate about code, strong teamwork, and good conversation. Open to full-time, part-time and freelance roles, collaborative teams and impactful projects. Basically curious. Connect with me

Flexible Products List and Quick View Modal Components Group

context:

I was making an ecommerce app in React. I should have made in Next or similar tech, as it would be easier for SEO and other things, but I have chosen React as of now. So, it has a search page, where all the products would be listed, along with it is filters and other things.

As it is clear from the image above that there will be a products list and on hovering it, there will be a quick view button. On clicking it, a small modal would open which will just show the details of the clicked product

This same kind of list is also present in home page, but with a twist. The home page has 2 sections containing the list:

  • Featured products list
  • Discounted products lits
featured products list:

This is similar to the search page but, it does not have filters and all other features, except the modal view.

discounted products list:

Same as that of the other 2, but has discount badges, discounted price mentions, etc.

requirements:

I wanted a structure which will have the following:

  • Common Product Unit
    • A common product component
    • A common modal for quick view of the component
    • I do not want multiple modals for each of the component, but a single modal resused for different product card
    • I wanted to use zustand as a store for state sharing between the products list and modal component.
    • Each group of products list, a quick view modal and corresponding store should be a single unit that can be integrated easily with any other modules or can be a single unit in itself
    • Example, the search page itself has multiple components which are tied together through a big zustand store and I wanted this custom products list unit to be merged with this store. Also, wanted this custom unit to be worked as a single independent unit so that I can use it to create the featured and discounted products section
  • Module system:
    • I have different modules in my app, which has their own local stores inside the module boundary. So, if I want to disable one module, it should not affect the whole app but the module will not be available to be used. Basically, each module has theier own working and context so that it can act as plug n play.
    • As, I am making the product unit, I can’t globalize my module store but can use it as dependency when creating a product unit. That way, I can just provide my local store so that the common product unit will be created for the module and can just become part of my local module to be used by it.
my approach/solution:
product list store creation factory:

This will be responsible for creating a store for each product unit, that binds quick view modal and product card together by sharing data. Even if we have multiple product cards, we will have only one quick view modal. This will be the store that can work independently or can be merged with an existing one.

src/stores/products-list-skeleton-store.ts
TypeScript
import type {
ProdListSkeletonStateType,
ProdListStoreType,
} from "@/interfaces/common-prod-list.interface";
import type { StateCreator } from "zustand";
const InitialState: ProdListSkeletonStateType<any> = {
products: [],
quickView: false, // For modal toggle
quickViewProd: null, // For product data in modal
};
// 1st Arg: EntireStore (The final store shape)
// 2nd Arg: [] (No input middleware)
// 3rd Arg: [] (No output middleware)
// 4th Arg: FeatureSlice (What this specific function builds)
const createProdListStore = <T, TParentStore = any>(
initialState: ProdListSkeletonStateType<T> = InitialState,
): StateCreator<TParentStore, [], [], ProdListStoreType<T>> => {
return (set) => ({
...initialState,
resetProdListStore: () =>
set((state: any) => ({ ...state, ...InitialState })),
updateProdListStore: (data: Partial<ProdListSkeletonStateType<T>>) =>
set((state: any) => ({ ...state, ...data })),
});
};
export default createProdListStore;

It is clear from tho code, that this function is responsible for creating the zustand store that will act as a shared state between each of the component in the common product unit.

I have added “any” as the product type as of now, but it wil be replaced with the actual product type that will be having in my app. One more point need to considered is the type of arguments needed to create a store, specially the first and the last argument.

First argument can be used to pass the type of the parent store if we need to merge it otherwise its default type is “any”. The last argument type is for the currently created store.

creating common product unit in presentation layer:

I have divided it into 3 parts:

  • Product Card compnent
  • Quick View modal content component
  • Quick View Modal component

Product Card Component:

src/components/custom/product/product-card.component.tsx
TypeScript
import { useStore, type StoreApi } from "zustand";
type ProductCardProps = {
product: any;
showDiscount?: boolean;
// openQuickView: () => void;
store: StoreApi<AtLeastOne<BaseProdStoreContract>>;
};
const ProductCard = ({
product,
store,
showDiscount = false,
}: ProductCardProps) => {
// const updateStore = useSearchState((state) => state.updateStore);
const updateStore = useStore(
store,
(state) => state.updateStore || state.updateProdListStore,
);
const openQuickView = () => {
// Adding "!" so that "updateStore" will be available for sure as we have applied "AtleastOne"
updateStore!({ quickView: true, quickViewProd: product });
};
//...More Code for Product card
}

Few things need to be noted here:

  • AtleastOne Type:
    • This is a custom type that I had created to let typescript know about the available functions. The store should have either “updateStore()” or “updateProdListStore()”, otherwise it will throw error.
    • “[K in Keys]-?” loops over all the keys provided through “T” and removes its optionality and makes every key required.
    • “Record<K, T[K]>” maps each key as its key and the function as its value
    • “Exclude<Keys, K>” removes the loop’s current key and its value
    • “Pick”, picks up the other keys except the one that is removed
    • “Partial” makes all of the picked one optional
    • So, basically, each loop takes the function’s name as its key, puts an object as its value which consists of the function as the mandatory and all other functions as optional
    • Then, “[Keys]” applied over it just returns the objects with different combinations of optionality of the passed type.
    • As the “AtleastOne” is a “type” rather than “const”, [Keys], wiill return the values as types with OR conjuction. For example:
type A = {
"a": 10
"b": true
}
/*
A[Keys] = 10 | true
*/
src/interfaces/app-global.interface.ts
export type AtLeastOne<T, Keys extends keyof T = keyof T> =
{
[K in Keys]-?: Record<K, T[K]> & Partial<Pick<T, Exclude<Keys, K>>>;
}[Keys];
  • BaseProdStoreContract Type:
    • This is where both the function is added as type with both of them being optional so that we can pass them to “AtleastOne” to make them in “either or” condition.
src/interfaces/common-prod-list.interface.ts
export interface BaseProdStoreContract {
updateStore?: (data: any) => void;
updateProdListStore?: (data: any) => void;
// You can easily add more optional functions here later!
}
  • Respective store is passed through props. As the zustand store is instantiated once only through the app, passing them through the props does not cause any issue.
  • In line #16, I have also made sure to prioritize “updateStore()” than the other one. This makes it easier to integrate with other stores and prioritize their function than the base store function. We can add any function as per the store we use and add it in the “BaseProdStoreContract” so that we can use it in this common unit as and prioritize as well as per our requirement.

Quick View modal content component:

type StoreType = AtLeastOne<BaseProdStoreContract> & {
quickViewProd: any | null;
};
type ProductQuickViewProps = {
store: StoreApi<StoreType>;
};
const QuickViewProduct = ({ store }: ProductQuickViewProps) => {
// Adding "!" so that product will be available for sure
// const product = useSearchState((state) => state.quickViewProd!);
const product = useStore(store, (state) => state.quickViewProd!);
// Product content code
}

Quick View Modal component:

type StoreType = AtLeastOne<BaseProdStoreContract> & {
quickView: boolean;
quickViewProd: any | null;
};
type ProductQuickViewModalProps = {
store: StoreApi<StoreType>;
};
const QuickViewModal = ({ store }: ProductQuickViewModalProps) => {
const quickView = useStore(store, (state) => state.quickView);
const updateStore = useStore(
store,
(state) => state.updateStore || state.updateProdListStore,
);
const handleOpen = (state: boolean) => {
// Adding "!" so that "updateStore" will be available for sure as we have applied "AtleastOne"
updateStore!({ quickView: state });
// if (!state) {
// resetStore() // Reset store when dialog closes
// }
};
return (
<Dialog open={quickView} onOpenChange={handleOpen}>
<DialogContent className="min-w-[75vw] w-full">
<QuickViewProduct store={store} />
</DialogContent>
</Dialog>
);
};

The common product unit completes here, where we have the product card, modal container and the selected product modal content.

Now, we can use this common unit to create multiple specific integrated component unit like featured products list and search products list.

featured products list:
const InitialState: ProdListSkeletonStateType<PopularProduct> = {
products: popularProducts,
quickView: false,
quickViewProd: null,
};
const usePopularProductsStore = create<ProdListStoreType<PopularProduct>>(
createProdListStore<PopularProduct>(InitialState),
);
export default usePopularProductsStore;

This is the store for the featured products list, where I used the factory to create a store which will be used independently for this section or integrated component section

const PopularProducts = () => {
return (
<>
<PopularProductQuickViewModal />
<PopularProductsContent />
</>
);
};
export default PopularProducts;

This is the container which will contain the featured products list and the single reusable modal

const PopularProductsContent = () => {
const popularProducts = usePopularProductsStore((state) => state.products);
// createStoreSubscriber(usePopularProductsStore)
const prodsListUi = popularProducts.map((product: PopularProd) => (
<ProductCard
key={product._id}
product={product}
store={usePopularProductsStore}
// attributes={attributes}
/>
));
return (
// products list ui codes
)
}

This is where I am using the newly created store to list the products using the common unit’s product card. I am passing the new store as a prop to the product card.

import QuickViewModal from "@/components/custom/product/quick-view-prod-modal.component";
import usePopularProductsStore from "./popular-products.store";
const PopularProductQuickViewModal = () => {
return <QuickViewModal store={usePopularProductsStore} />;
};
export default PopularProductQuickViewModal;

This is the quick view modal component for featured products where I have used the quick view modal of the common product unit.

This whole setup is what looks like the image I provided above.

product search page
const InitProdListData: ProdListSkeletonStateType<SortedProd> = {
products: sortedProducts,
quickView: false,
quickViewProd: null,
};
const useSearchState = create<SearchStore>((set, get, ...args) => ({
...InitialState,
...createProdListStore<SortedProd>(InitProdListData)(set, get, ...args),
updateStore: (data: Partial<SearchStoreState>) =>
set((state) => ({ ...state, ...data })),
// ... rest of the code
}));
export default useSearchState;

I already had an exisitng store which had all the reuqired shared states for the search module. I just created the common products list store and merged with the existing one, as can be seen from line #9. And all others (like the product card and quick view modal) is same as that of the configuration above. This results as per the above provided image.

This is just a sample of how things can be re-used and structured so that it can be extandable. The same strategy can be used for different modules or structures w/o making the state global to the app. Just passing the module store makes it easily integrable and scalable.

Leave a comment