The term "store" in this article refers to a JavaScript object we can use to keep track of state. Not to be confused with Magento stores.
ScandiPWA uses Redux to keep track of global state. If used correctly, Redux is predictable and easy to debug.
If you need to keep a state that will be the same throughout the application, and possibly shared between multiple components, create a Redux store. Examples:
Breadcrumbs are stored in a Redux store, because multiple pages need to be able to set them
The Cart is stored in a Redux store, because both the cart overlay and the cart page need to use it, though they don't have a direct parent-child relationship. Also, it is guaranteed that you will need information about only 1 cart in the application.
Avoid using a global Redux store if it is component-specific, and you need to be able to store a different value in each component. Use the component's state instead.
Following Redux practices, the ScandiPWA theme contains 1 Redux store. However, since the application needs to maintain different kinds of global state, the top-level Redux store actually tracks an object containing multiple "sub-stores". Each of these sub-stores, has a dedicated subdirectory in store where it is defined.
The use of Redux stores is not ScandiPWA-specific, so it is best to learn about it from the oficial redux documentation. However, we will go though an example of how it is used in ScandiPWA to help you understand how it interacts with the application.
Example: Breadcrumbs
At the top of many pages in the ScandiPWA theme, you will see breadcrumbs. These are path-like indicators that help the user understand where in the app they currently are, and improve navigation:
Since we would never need to keep track of multiple different breacrumb paths at once, they are implemented in a global redux store. Once a component receives enough data to know what the breadcrumbs should be (such as a list of parent categories as above), it dispatches an update to the breadcrumbs. This state can now be read by the component responsible for rendering breadcrumbs.
The first step in creating a redux store is defining what it's initial state should be. We need to keep track of two things - the breadcrumbs themselves and a boolean indicating wether they should be visible on the current page.
Now, we need to describe how the state should update in response to certain actions. It might seem unintuitive at first, but Redux state cannot be updated directly. Instead, you are allowed to define reducers - functions that describe how the state should transition when an action is dispatched.
We want to handle two types of actions - one that updates the breadcrumbs, and one that updates their visibility:
// action types are actually defined in the .action fileexportconstUPDATE_BREADCRUMBS='UPDATE_BREADCRUMBS';exportconstTOGGLE_BREADCRUMBS='TOGGLE_BREADCRUMBS';
All actions are simple JavaScript objects that carry information which the reducer can interpret to update the state. The UPDATE_BREADCRUMBS action would look like this:
{// all actions have a `type` field that indicates// what kind of action it is. it must be unique// among action types type:UPDATE_BREADCRUMBS,// the UPDATE_BREADCRUMBS type action also has a // breadcrumbs field that carries information// about what the new breadcrumbs should be breadcrumbs: [...] // some array}
The TOGGLE_BREADCRUMBS would be similar, but carry a different type of data, a boolean:
{ type:TOGGLE_BREADCRUMBS,// the UPDATE_BREADCRUMBS type action also has a // breadcrumbs field that carries information// about what the new breadcrumbs should be areBreadcrumbsVisible:true// or false}
Note that, by themselves, actions do not do anything - they are just objects with some fields. However, when an action is dispatched ("sent" to Redux, we'll get to that later), Redux passes it on to all reducers (functions we define). Each reducer can look at the action and update its state by returning a new value.
/** @namespace Store/Breadcrumbs/Reducer */exportconstBreadcrumbsReducer= ( state =getInitialState(),// previous state, or the initial state if none action // we get the action that was dispatched) => {// we are only interested in certain types of actionsswitch (action.type) {// if this is an UPDATE_BREADCRUMBS actioncaseUPDATE_BREADCRUMBS:// we know that it will have some data in action.breadcrumbsconst { breadcrumbs } = action;// we update the statereturn {...state,// to keep the same value breadcrumbs // except with a new breadcrumbs value };// similarly we want to update the state for TOGGLE_BREADCRUMBS actionscaseTOGGLE_BREADCRUMBS:const { areBreadcrumbsVisible } = action;return {...state, areBreadcrumbsVisible };default:// it is possible the action was not related to breadcrumbs.// then we can just return the original state unchanged.return state; }};
Now that the reducer is defined, we need to include it in our single global Redux state.
src/app/store/index.js (simplified & annotated)
// copyrightimport { combineReducers, createStore} from'redux';import BreadcrumbsReducer from'Store/Breadcrumbs/Breadcrumbs.reducer';// [...] import the other reducers/** @namespace Store/Index/getReducers */exportconstgetStaticReducers= () => ({ BreadcrumbsReducer,// [...] include the other reducers});// a bunch of Redux API calls essentially creating a store from the above
2. The .action File: Defining Possible Actions
The reducer we created can respond to certain actions described above, but creating those action object manually would get repetitive and error-prone. Hence, we create functions that can create these action objects for us:
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.
3. The .dispatcher File: Dispatching Helpers
It can be convenient to have a file that defines helpers for dispatching actions. That's what the .dispatcher file is for:
import { toggleBreadcrumbs, updateBreadcrumbs }from'Store/Breadcrumbs/Breadcrumbs.action';/** @namespace Store/Breadcrumbs/Dispatcher */exportclassBreadcrumbsDispatcher {// utility method for updating breadcrumbs// given a category,// and a dispatch function (from Redux)updateWithCategory(category, dispatch) {constbreadcrumbs=this._getCategoryBreadcrumbs(category);dispatch(toggleBreadcrumbs(true));dispatch(updateBreadcrumbs(breadcrumbs)); }_getCategoryBreadcrumbs(category) {...}updateWithProduct(product, dispatch) {...}updateWithCmsPage(cmsPage, dispatch) {...}}exportdefaultnewBreadcrumbsDispatcher();
Unlike the reducer or action creators, you are free to have side effects in the dispatcher. For example, in store/Cart/Cart.dispatcher.js, addProductToCart makes a GraphQl mutation request before updating the store by dispatching a cart data update.
import { connect } from'react-redux';import Breadcrumbs from'./Breadcrumbs.component';// given the global state, need to return an object// containing the values of the state that we need// these will be passed as props to Breadcrumbs/** @namespace Component/Breadcrumbs/Container/mapStateToProps */exportconstmapStateToProps= (state) => ({ breadcrumbs:state.BreadcrumbsReducer.breadcrumbs, areBreadcrumbsVisible:state.BreadcrumbsReducer.areBreadcrumbsVisible});// we specify mapDispatchToProps even though we don't need it// so that ScandiPWA plugins can use it if necessary/** @namespace Component/Breadcrumbs/Container/mapDispatchToProps */exportconstmapDispatchToProps= () => ({});// Breadcrumbs will get breadcrumbs and areBreadcrumbsVisible as propsexportdefaultconnect(mapStateToProps, mapDispatchToProps)(Breadcrumbs);