This tutorial demonstrates how to use the Wix CLI to build a Custom Products Catalog app on the Wix Platform. The app contains a dashboard page that lists, searches, sorts, adds, and deletes products in a Wix Stores catalog.
Note: This tutorial uses the Stores Catalog V3 API only. This is sufficient for private apps. However, apps published to the Wix App Market should support both the V1 and V3 Stores Catalog APIs. For more details, see Exposing Apps Publicly and Privately.
By following this tutorial, you'll learn how to:
The end result will look like this:

We'll use the following steps to build the Custom Products Catalog app:
Before getting started, make sure that:
We also recommend that you check out the Patterns documentation. Patterns is a complex React library. Familiarizing yourself with the basics will help you better understand the code in this tutorial.
We use the Wix CLI to initialize our Custom Products Catalog app. In the process of initializing our app, the Wix CLI automatically:
src folder containing initial boilerplate code for an app with a dashboard page.package.json file containing your app dependencies.To initialize the app:
Open a terminal and navigate to the folder where you want to create your app.
Run the following command:
If prompted to install the @wix/create-new package, press y.
Select Create a new Wix App.
Select Create a basic app.
Enter a name for your app. Let’s name our app Custom Products Catalog.
Press Enter to accept the default folder name (custom-products-catalog).
When asked to configure Wix MCP for your IDE, select your preferred MCP.
A green Success message confirms that your app has been registered in the Custom Apps page.
Press Enter to accept the default namespace (custom-products-catalog).
Press Enter to accept the default code identifier (custom_products_catalog).
You now have a new app in the Custom Apps page, a new folder in your local file system, and a local Git repository for developing and testing your app.
We use the CLI's generate command to create a dashboard page for our app.
To create the dashboard page:
Navigate to your newly created folder for your app.
Run the following command and follow the prompts:
When prompted for the kind of extension, select Dashboard Page.
When prompted for a page title, enter Custom Products Catalog.
When prompted for the page route, press Enter to accept the default (custom-products-catalog).
Upon completion, the extension files are created at src/extensions/dashboard/pages/custom-products-catalog/:
For more information about these files, see Dashboard Page Extension Files and Code.
Tip: The CLI's default scaffold also created a my-page extension at src/extensions/dashboard/pages/my-page/. You can leave it (it won't interfere with this tutorial) or delete the my-page/ folder to keep the dashboard sidebar showing only the Custom Products Catalog page.
Now that you’ve initialized your app, you can run a local development server to see the app in action, and view local changes as you develop your app.
To run a local development server for your app:
In the terminal, from your app's folder, run the following command:
Important: This tutorial uses Stores Catalog V3. New Wix sites use V3 by default. If you choose an existing site instead, confirm it uses V3 with the Catalog Versioning API.
The CLI prompts you to choose a development site (test site), which you’ll use throughout this tutorial to run and test your app. You can choose an existing Wix site as your development site, or create a new one. Let’s Create a new Development Site. The newly created development site is automatically named Dev Sitex followed by a number (for example, Dev Sitex 12345), and can be found in your Wix account’s list of sites.
Follow the prompt to open the app installation page in your default browser. If the browser doesn’t open, install your app on your test site manually and skip the next step.
Click Agree & Add to install your app on your development site.
In the terminal menu, select Dashboard to open your development site’s dashboard.
In the dashboard sidebar, click Apps > Custom Products Catalog to see your newly created app’s dashboard page. We add the content of our app’s dashboard page in the next step.

Your app is now running on your development site. As you develop your app, any changes made to your app’s code are reflected in real time on your development site.
If your changes don’t show up, try refreshing the page, or closing and reopening the development site.
In this step, we'll add permissions for the app. Every SDK API requires specific permissions to use. In this app, we will use the searchProducts(), createProduct(), and deleteProduct() functions in the Wix Stores Products V3 API to get a list of all products, and to add and delete them.
To use these functions, we need to give our app permission requirements in the app dashboard. Once we do this, anyone installing the app will be prompted to grant the specified permissions.
Our app calls searchProducts() to read products and createProduct()/deleteProduct() to add and remove them, so it needs both a read and a write product scope. Each method's reference page (linked above) lists the exact scope it requires in its Permission Scopes section, including the scope's ID (for example, SCOPE.STORES.PRODUCT_READ).
To add a scope:
searchProducts(), and a write scope for createProduct() and deleteProduct().For more information on configuring permissions, see Configure permissions for your app.
Our app integrates with Wix Stores, so we need to set up Wix Stores on our site, then reinstall the app so that our site requests the required permissions:
Before we start coding our app, we need to install some npm packages. The @wix/dashboard and @wix/design-system packages come pre-installed in the project that the CLI generated.
In your terminal, run the following commands to install the remaining packages:
The purpose of each of these packages will become clear as we progress.
It’s finally time to start writing some code!
Our plan in this tutorial is to create a React component that defines our dashboard page. We’re going to wrap this component with providers that will manage our data fetching and provide Wix styling for our dashboard page component.
To do this, we’ll create a higher-order component that will accept our dashboard page component and return it wrapped in the necessary providers.
To create the higher-order component:
Create a new file in your app's repo under src > extensions > dashboard named withProviders.tsx.
Import FC from React, WixDesignSystemProvider from the design system, and WixPatternsProvider from Patterns:
WixDesignSystemProvider provides styling for Wix Design System components.WixPatternsProvider provides styling and data manipulation for Wix Patterns components.Write a function named withProviders to wrap our dashboard page component in WixDesignSystemProvider and WixPatternsProvider:
Your complete withProviders.tsx file should look like this:
Our dashboard page component will need to make SDK calls to create and delete Wix Stores product data using React hooks. To simplify our component code, we define these in a separate TypeScript file.
To create your app’s React hooks:
Create a new folder in your app's repo under src > extensions > dashboard named hooks.
Create a new file in src > extensions > dashboard > hooks named stores.ts.
Import productsV3 from Wix Stores so we can use the Wix Stores Products V3 API:
Import useCallback:
useCallback is a react hook that caches a callback function, returning a memorized version of the function that changes only if one of the dependencies has changed.
We use this hook to define how a new product is created and how a product is deleted using the optimisticActions prop and createProduct/deleteProduct from productsV3.
Whenever any of these dependencies change, useCallback redefines the callback function.
Import CollectionOptimisticActions:
CollectionOptimisticActions is a class that provides a set of utilities for managing optimistic actions on a Wix Collection. It allows us to perform various actions, such as adding, updating, and deleting items from a collection while assuming that these actions will succeed, even before confirming with the server. Learn more about the CollectionOptimisticActions class.
Create a function to create new products named useCreateProduct:
Inside this function:
Use productsV3 to get the createProduct() function.
Use useCallback() to manage our createProduct() call:
Note: The Products V3 API requires several fields on every product:
productTypephysicalProperties when productType is PHYSICAL)variantsInfo.variants with at least one variantA production app would normally collect these values from the user. To keep the Create Product modal minimal, this tutorial only asks for a product name and hardcodes the rest in useCreateProduct, so every new product is a physical product priced at $10.
Let's break down the above code:
name comes from user input; the other fields satisfy the V3 API's required-fields contract with fixed defaults.optimisticActions.createOne() to create the new product.
submit function. createProduct takes the V3 product object directly and returns the created product, so we pass newProduct from the closure and wrap the response in an array for the optimistic collection.useCallback().Your function should look like this:
Create a function to delete products named useDeleteProducts():
Inside this function:
Use productsV3 to get the deleteProduct() function.
Use useCallback() to manage our deleteProduct() calls:
Let's break down the above code:
optimisticActions.deleteMany() to delete the products.
deleteProduct() for each one. deleteProduct takes the product ID directly as a string.useCallback().Your function should look like this:
Your complete stores.ts file should look like this:
We need a modal where the user can enter the name of the new product and confirm or cancel. This modal will be opened using a button on the dashboard page. To build this modal:
Create a new folder in your app's repo under src > extensions > dashboard named components.
Create a new file in src > extensions > dashboard > components named create-product.tsx.
Import:
React, useEffect, and useState.Modal component.Create a CreateProductModal component to define the appearance and functionality of the modal. Use the following code:
Let’s break down the above code:
useEffect() to ensure that when the showModal prop updates, the shown state variable aligns with it.productName.FormField component that takes an input for the product name.Your complete create-product.tsx file should look like this:
Finally, we have all the pieces in place to set up our dashboard page.
Open the custom-products-catalog.tsx file at src/extensions/dashboard/pages/custom-products-catalog/.
Delete all the contents - we're starting from scratch.
Add the following import statements:
This file is where we start to use Patterns.
Patterns is a React library with advanced components that extend the functionality of the core UI React components from the Wix Design System. It simplifies Wix app development by enabling you to easily and consistently implement complex functionalities like querying, displaying, and filtering collection data from remote servers.
In our code, we use Patterns components to create a table of products that you can query, filter, and sort. The table also has functionality to create new products, delete existing products, and choose which columns to display.
Import everything we set up in the previous sections:
Define a type for filtering the table's data:
This is the product property by which the table can be filtered. We'll pass this type to the useTableCollection hook later.
Define a map from column IDs to the field paths that searchProducts accepts for sorting:
The SortableField type narrows the allowed values to the field paths that the V3 searchProducts endpoint accepts as sort fields. The map then translates between the table column's id, which we use as a short human-friendly slug, and the API's full field path.
For example, the Price column has id: 'price', but the SDK expects the full path actualPriceRange.minValue.amount when sorting by price. The map entry price: 'actualPriceRange.minValue.amount' makes the translation explicit. Columns whose id already equals a sortable field path don't need an entry.
Map product types to strings so that they display in a readable way in the table:
Products() componentIt's finally time to make our dashboard page component. We will:
Create a function for our dashboard page component named Products:
Export the component wrapped by withProviders():
The rest of the code on this page will be written inside the Products() function.
Initialize the shown state variable to manage the visibility of the modal we created earlier.
Use productsV3 to get the searchProducts() and deleteProduct() functions.
Create the tableState using useTableCollection.
useTableCollection is a hook that returns a tableState object. This is where we define:
Let's break down the above code:
query object and returns the matching products plus a total count.
query parameter. It contains the requested limit, the active sort, and any filters the user has applied.from and to becomes two separate _updatedDate conditions combined with $and, because the V3 search filter requires a single operator per field per condition.id in sortFieldByColumnId to get the field path the SDK accepts, and translate the order from the patterns library's lowercase format (asc/desc) into the SDK's uppercase enum (ASC/DESC).searchProducts. Pass the cursor paging limit, the filter, and the sort spec as the first argument. In the second argument, request the additional response fields THUMBNAIL and PLAIN_DESCRIPTION. These aren't returned by default, but the Avatar and Description columns need them.total. A production app should implement cursor-based pagination using pagingMetadata.cursors.next from the response, and call countProducts to display a true total.updatedDate filter uses the dateRangeFilter factory.Create the optimisticActions class using useOptimisticActions.
useOptimisticActions is a hook that returns a CollectionOptimisticActions class. The code below defines logic that anticipates the results of the query that we defined in the previous step. When there is a change that sends a new query to the server (for example, a new filter), this logic will be applied to the data already in the page's memory while we're waiting for the server's response.
Let's break down the above code:
tableState above. It returns a function that returns true if the product would pass the filters, and false if it wouldn't.
false if the product wouldn't pass the Last Updated filter.true if the product would pass all of the filters.Define the createProduct() and deleteProducts() functions using the hooks we created in Step 7:
At this stage, your Products() function should look like this:
We define our page as follows:
Define the CollectionPage component that will wrap all our components on this page:
Add a CollectionPage.Header component that contains the page title, breadcrumbs, and a button that opens our Add Product modal:
Add a CollectionPage.Content component below the CollectionPage.Header. This will wrap the rest of the components on this page.
Add the CreateProductModal component that we created in Step 8:
Add a Table component.
The following code displays a table containing our columns and fetched data. When no items are selected, there is also a button that opens an accordion to customize the columns displayed, and a button that opens an accordion to filter the data. When at least one product is selected, the table displays the number of selected products, the maximum number of products that can be selected, a Select All option, and a Delete button.
Let's identify where some of our key functionality is implemented in the above code. For more information on all the Table props, see Patterns Table Component.
CollectionToolbarFilters component to display an accordion with the filtering options. The Last Updated filter uses the DateRangeFilter component.MultiBulkActionToolbar component to create a button in the toolbar that deletes all selected products. When the button is clicked, the user is asked to confirm their decision in a modal (line 25).Image component from the Wix Design System and feed it the URL from the product's media.main.thumbnail.url field. To make this field appear in the response, we requested it via the THUMBNAIL field in the searchProducts call.Box for layout with direction set to vertical and gap to 3px, indicating vertical stacking with a gap of 3px. Inside this box, add two Text components for the product's name and plainDescription. The description comes from the PLAIN_DESCRIPTION requested field.actualPriceRange.minValue.amount and prefix it with a dollar sign. The column is sortable: true; sorting maps to the actualPriceRange.minValue.amount field path via sortFieldByColumnId.productType field using productTypeToDisplayName which we defined earlier. If there is no product type, return an empty string._updatedDate as a readable date. This column is defaultHidden: true, so the user must opt in via the columns control to see it.Your page structure code should look like this:
Your complete code should look like this:
Now that the app’s code is ready, you can test it locally using the Wix CLI.
To test your app, do the following:
Run a local development server for your app using the wix dev command in your terminal. Your app’s dashboard page will now look like this:

Click the Add Product button and use the modal to add a product.
Select some products and click the Delete button.
If things don’t look right, open your browser’s developer tools and check for errors in the console.
You have now fully developed an app that allows users to add or remove products from a Wix Stores products catalog through a dashboard page.
After testing your app and seeing that it works as expected, you can build and deploy your app.
Last updated: 13 July 2026