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>
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
.
Using props can make the implementation more complex, especially when the components are far apart. An alternative method could be used instead.
Context API - Context provides a way to share values like these between components without explicitly passing a prop through every level of the tree.
Context API is a good solution when there is a close common parent among the used components or the scope of implementation is very clear. For example, the states will only be used in an extension with new components.
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.
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.
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:
Dispatch an action directly - This is done when all data necessary to update the state is already available.
A good example of directly dispatching an action is in the LoginAccount
container. The component already had everything needed to dispatch the showNotification
action: the error type, and the error message to be shown.
Use a dispatcher function - Dispatcher functions define additional steps that should occur before dispatching an action. Typically, the dispatcher simply requests the data required to dispatch the action.
A good example is in the MyAccountMyOrders
container. At some point, it needs to update the orderList
state. However, it does not have the orders data required to dispatch an action.
Instead, it makes use of the requestOrders
method dispatcher of the OrderDispatcher
class. This dispatcher method takes the page as an argument and requests the
orders. If the request succeeds, it will then dispatch the getOrderList
action with the returned orders. If there is an error, it will dispatch the showNotification
and setLoadingStatus
actions.
Use a dispatcher function - Dispatcher functions define additional steps that should occur before dispatching an action. Typically, the dispatcher simply requests the data required to dispatch the action.
A good example is in the MyAccountMyOrders
container. At some point, it needs to update the orderList
state. However, it does not have the orders data required to dispatch an action.
Instead, it makes use of the requestOrders
method dispatcher of the OrderDispatcher
class. This dispatcher method takes the page as an argument and requests the
orders. If the request succeeds, it will then dispatch the getOrderList
action with the returned orders. If there is an error, it will dispatch the showNotification
and setLoadingStatus
actions.
<aside> ⚠️ Dispatch an action and dispatcher functions are different things!
</aside>
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:
<ENTITY>.action.js
- Defines actions and action creators.<ENTITY>.reducer.js
- Contains reducers that are active in the store.<ENTITY>.dispatcher.js
- Contains a dispatcher class with methods to dispatch actions..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:
GET_ORDER_LIST
and SET_ORDER_LOADING_STATUS
.getOrderList
and setLoadingStatus
.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>
.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.
scandipwa/src/store/Order/Order.reducer.js
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
.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.
scandipwa/src/store/Order/Order.dispatcher.js
<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>
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>
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:
mapStateToProps
- A function that allows you to make global states available in the container as props. This will subscribe to Redux store updates.mapDispatchToProps
- This function enables you to define props that will have access to the dispatch
function.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>
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
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>
getStore().dispatch
gives access to the dispatch
function, which is the same as the parameter on mapDispatchToProps
.getStore().getState()
function gives access to the state object, which is the same as the parameter on the mapStateToProps
.<aside> ✅ You can learn more about the store object by referring to the Redux official documentation on the store object.
</aside>