ScandiPWA utilizes React components, and in some cases, it may be necessary to share states with a distant component. Using props might not be a suitable solution, but global states can offer a solution to this problem. To achieve this, ScandiPWA utilizes the React Redux library to allow components to read data from the Redux store and dispatch actions to update the state in the store. However, it is necessary to connect the components with the Redux store.

Registering a reducer is an easy task with the use of plugins, but it’s important to make all files related to Redux follow the same structure for consistency across the project and make it easier for different people to work in them.

<aside> ⚠️ In this article, the term "store" refers to a JavaScript object that can be used to keep track of global states. This is not to be confused with Magento stores.

</aside>

Why does ScandiPWA use global states?

React works by rendering trees of components (What are React components?). The data is stored in the component's state and can be passed to its children using props. This is a good solution for most cases.

<aside> ℹ️ Using props to share data is great when the immediate child needs it.

</aside>

The problem arises when you need to share data from children's components with their parents or with a distant component.

In this example, component E needs to update the rendering of component D.

Untitled

Using props can make the implementation more complex, especially when the components are far apart. An alternative method could be used instead.

Untitled

Untitled

ScandiPWA uses Redux as the state management library to keep track of global states.

Redux is a state management library that allows states to be stored in an object tree inside a single store. If used correctly, Redux is predictable and easy to debug.

How does ScandiPWA use Redux?

ScandiPWA uses React Redux and lets your ScandiPWA components read data from the Redux store, and dispatch actions to the store to update state.

<aside> ℹ️ The use of Redux stores is not specific to ScandiPWA, hence it is best to learn about it from the official React Redux documentation. However, this page provides examples of how it is used in ScandiPWA to help you understand how it interacts with the application.

</aside>

Redux introduces:

ScandiPWA also uses dispatcher functions. Dispatchers are used when the data necessary to update the state is not available.

Untitled

The Redux state can be updated using either the dispatcher or by dispatching an action directly. The method you choose depends on the specific update you want to perform:

<aside> ⚠️ Dispatch an action and dispatcher functions are different things!

</aside>

How to work with Redux files?

To ensure consistency among Redux stores made by different people in a project, a defined structure of rules and directories should be established to make them work together.

Following Redux practices, the ScandiPWA theme contains 1 Redux store. However, since the application needs to maintain different kinds of global states, the top-level Redux store actually tracks an object containing multiple "sub-stores". Each of these sub-stores has a dedicated subdirectory in src/store/ where it is defined.

<aside> ➡️ Redux file names are always UpperCamelCase (also known as PascalCase), for example, CartItem.

</aside>

This section explains how to structure each file, describing their behavior as well as their responsibilities. A component named <COMPONENT> should have a store located in folder src/store/<COMPONENT> containing the following files:

How to work with .action.js files?

Redux actions are plain javascript objects that contain the ****information ****needed to update the state, its type field that tells what kind of action to perform. Reducers update the store based on the value of the action dispatched.

<aside> 🚨 Redux strongly discourages creating side effects in the reducer, or action creators. Avoid making requests, mutating non-Redux state, or other changes in these functions. This will make them less predictable and harder to debug.

</aside>

Usually, the action.js file defines:

For example, the Order.action.js file in ScandiPWA:

export const GET_ORDER_LIST = 'GET_ORDER_LIST';
export const SET_ORDER_LOADING_STATUS = 'SET_ORDER_LOADING_STATUS';

/** @namespace Store/Order/Action/getOrderList */
export const getOrderList = (orderList, status) => ({
    type: GET_ORDER_LIST,
    orderList,
    status
});

/** @namespace Store/Order/Action/setLoadingStatus */
export const setLoadingStatus = (status) => ({
    type: SET_ORDER_LOADING_STATUS,
    status
});

These types are used in the example below with order.reducer.js to define the new status based on the action.

The action creators are used in the exampĺe with the order.dispatcher.js.

<aside> ⚠️ Always include the namespaces to action creator functions.

</aside>

How to work with .reducer.js files?

Reducers serve as state updaters within Redux. They take in ready data and are responsible for updating the global state. In most cases, the state is represented by a simple object. Action Reducers play a crucial role in transforming the current state into the desired updated state based on the dispatched actions.

For example, the Order.reducer.js file is responsible for defining reducers related to the Order component.

In this example, the OrderReducer determines the new state of isLoading and orderList based on the dispatched action.

<aside> 🚨 Redux strongly discourages creating side effects in the reducer, or action creators. Avoid making requests, mutating non-Redux state, or other changes in these functions. This will make them less predictable and harder to debug.

</aside>

Tutorial: Create a Reducer Plugin

How to work with .dispatcher.js files?

Its main purpose is to initiate data retrieval operations, ensuring that the required data is available for subsequent state updates within the reducer.

<aside> ⚠️ It’s recommended to define your GraphQL queries in the src/query/ using the Util/Query(How to create a ScandiPWA query?).

</aside>

The dispatcher.js file should define a class with the dispatcher methods and export a new object of this class as default.

<aside> ℹ️ Very often, dispatcher files will execute queries or mutations, learn more about it here! How to work with requests?

</aside>

Dispatcher methods must include at least the dispatch argument, which is a function used to dispatch an action.

import { actionCreator } from 'Store/Entity/Entity.action';

/** @namespace Store/EntityDispatcher/Dispatcher */
export class EntityDispatcher {
    updateSomething(dispatch, argument) {
	      //do something with the argument
        const dataNeeded = getNeededData(argument);
        //dispatch an action with the needed data
        dispatch(actionCreator(dataNeeded))
    }

		//helper function 1
    _getNeededData(category) {...}

}

export default new EntityDispatcher();

Unlike the reducer or action creators, dispatchers are free to have side effects. In the example below, handleReorderMutation makes a GraphQL mutation request.

For example, the Order.dispatcher.js dispatcher.

<aside> 🚨 Please note, that in ScandiPWA, there are many dispatchers that extend the QueryDispatcher class. Despite all, it’s not recommended to use it. It is an outdated and hard-to-debug concept, which is limited to only one query. Opt for using dispatchers as simple classes with methods, avoid extending QueryDispatcher.

</aside>

<aside> ✅ It is recommended to use await and try/catch in your dispatchers instead of then() and final(). This allows for easy use of plugins in your dispatchers.

</aside>

How to register a reducer in the global store?

In order to register a reducer in the global store, you must create a plugin to inject the reducer into the global store. The plugin must be placed in the src/plugin/ directory, and receive the following name: src/plugin/<MY STORE>Reducer.plugin.js.

Then, you can use the following template and make the appropriate changes:

import BreadcrumbsReducer from '../store/Breadcrumbs/Breadcrumbs.reducer';

export default {
	'Store/Index/getStaticReducers': {
		function: (args, callback) => ({
			...callback(args),
			BreadcrumbsReducer
		})
	}
}

<aside> ➡️ You may replace BreadcrumbsReducer class name with <MY STORE>Reducer.

</aside>

How to connect a component to the global state?

Redux provides a helpful function called connect to facilitate this connection between the components and the store (learn more about the connect function). ScandiPWA uses the <COMPONENT>.container.js files to make this connection.

The connect function accepts 2 arguments and returns a wrapper function that should take your container as a parameter. This should be exported.

export connect(mapStateToProps, mapDispatchToProps)(<YOUR_CONTAINER>)

The arguments are mapStateToProps and mapDispatchToProps, they are also described on the How to Work with Components page:

Untitled

In the following code, observe the usage and declaration of both functions and how connect receives them(including the container class):

import { connect } from 'react-redux';
import { setCategoryBreadcrumbs } from '../../store/Breadcrumbs/Breadcrumbs.action';
import BreadcrumbsDispatcher from '../../store/Breadcrumbs/Breadcrumbs.dispatcher';
// [..] other imports

/** @namespace Component/Breadcrumbs/Container/mapStateToProps */
export const mapStateToProps = (state) => ({
	breadcrumbs: state.BreadcrumbsReducer.breadcrumbs,
});

/** @namespace Component/Breadcrumbs/Container/mapDispatchToProps */
export const mapDispatchToProps = (dispatch) => ({
	requestCategory: () => BreadcrumbsDispatcher.requestCategory(dispatch),
	setCategoryBreadcrumbs: (category) => dispatch(setCategoryBreadcrumbs(category))
});

// container class declaration goes here. How does it looks like?
export class TestContainer extends PureComponent {
	[...]
	// container logic
	[...]
}

export default connect(mapStateToProps, mapDispatchToProps)(TestContainer);

<aside> ➡️ Please declare empty mapStateToProps and mapDispatchToProps on your component even though you are not using them. This will help other extension developers to extend your code.

</aside>

How to extend mapStateToProps or mapDispatchToProps?

<aside> ⚠️ Some containers may not have the mapStateToProps or mapDispatchToProps functions defined for you to extend. For example, the Field container in ScandiPWA does not have them. In such cases, you can use the getStore method from the util/Store directory to access the Redux state and dispatch when extending the component.

</aside>

Tutorial: Create a plugin to mapStateToProps and mapDispatchToProps

How to access the Redux store outside the container?

The getStore() function from the util/Store directory allows you to have access to the Redux Store.

<aside> ⚠️ The getStore function should not be used inside .container.js files. It is intended for use when you need to extend a react component that does not define mapStateToProps and mapDispatchToProps or for other functionalities outside of a react component.

</aside>

<aside> ✅ You can learn more about the store object by referring to the Redux official documentation on the store object.

</aside>