State management has always been THE pain point in React.
For years now, Redux has always been the most popular solution, but it requires a certain learning curve and patience to learn its intricacies. Also, I find some of the repetitive parts of it annoying, such as calling connect(mapStateToProps, mapDispatchToProps)
at every time the store is needed inside a component, and/or side effects like prop drilling, but that's just me.
With React's release of production-grade features such as Context API and Hooks, developers can already implement global state management without having to use external libraries (e.g. Redux, Flux, MobX, etc.) for the same purpose.
Heavily motivated by this article, I was inspired to build global state management in React using Context API.
Definition of Terms
- Context - A component in React that lets you pass data down through all of the subcomponents as state.
- Store - An object that contains the global state.
-
Action - A payload of information that send data from your application to your store through
dispatch
. Working hand in hand with Action creator, API calls are typically done here. - Reducer - is a method that transforms the payload from an Action.
The Concept
The goal of this state management is to create two Context components:
- StoreContext - to handle the store (aka the global state) and
- ActionsContext - to handle the actions (functions that modify the state)
As you can see in the Folder structure provided below, actions and reducers (methods that transform the store) are separated per module thereby needing a method that will combine them into one big action and reducer object. This is handled by rootReducers.js
and rootActions.js
.
Folder Structure
State management is under the /store
folder.
components/
layout/
common/
Header/
index.js
header.scss
Header.test.js
Shop/
index.js
shop.scss
ShopContainer.js
Shop.test.js
store/
products/
actions.js
reducers.js
index.js
rootActions.js
rootReducers.js
The View: <Shop/>
component
The simplest way to showcase state management is to fetch a list of products.
const Shop = () => {
const items = [/** ...sample items here */]
return (
<div className='grid-x grid-padding-x'>
<div className='cell'>
{
/**
* Call an endpoint to fetch products from store
*/
items && items.map((item, i) => (
<div key={i} className='product'>
Name: { item.name }
Amount: { item.amount }
<Button type='submit'>Buy</Button>
</div>
))
}
</div>
</div>
)
}
Welcome to the /store
Product Actions: /products/actions.js
export const PRODUCTS_GET = 'PRODUCTS_GET'
export const retrieveProducts = () => {
const items = [
{
'id': 1,
'amount': '50.00',
'name': 'Iron Branch',
},
{
'id': 2,
'amount': '70.00',
'name': 'Enchanted Mango',
},
{
'id': 3,
'amount': '110.00',
'name': 'Healing Salve',
},
]
return {
type: PRODUCTS_GET,
payload: items
}
}
Product reducers: /products/reducers.js
import { PRODUCTS_GET } from './actions'
const initialState = []
export default function (state = initialState, action) {
switch (action.type) {
case PRODUCTS_GET:
return [ ...state, ...action.payload ]
default:
return state
}
}
/store/index.js
is the entry point of state management.
import React, { useReducer, createContext, useContext, useMemo } from 'react'
const ActionsContext = createContext()
const StoreContext = createContext()
export const useActions = () => useContext(ActionsContext)
export const useStore = () => useContext(StoreContext)
export const StoreProvider = props => {
const initialState = props.rootReducer(props.initialValue, { type: '__INIT__' })
const [ state, dispatch ] = useReducer(props.rootReducer, initialState)
const actions = useMemo(() => props.rootActions(dispatch), [props])
const value = { state, dispatch }
return (
<StoreContext.Provider value={value}>
<ActionsContext.Provider value={actions}>
{props.children}
</ActionsContext.Provider>
</StoreContext.Provider>
)
}
I suggest reading on Hooks if you are unfamiliar with many of the concepts introduced above.
Combining Actions and Reducers
Root reducer: /store/rootReducer.js
import { combineReducers } from 'redux'
import productsReducer from './products/reducers'
export default combineReducers({
products: productsReducer
})
Root actions: /store/rootActions.js
import * as productsActions from '../store/products/actions'
import { bindActionCreators } from 'redux'
const rootActions = dispatch => {
return {
productsActions: bindActionCreators(productsActions, dispatch)
}
}
export default rootActions
If you have noticed, I still used redux functions such as combineReducers
and bindActionCreators
. Personally, I did not want to reinvent the wheel, but feel free to create your own.
Finally, we inject our contexts to the entry point of our application and modify our component to retrieve the data from the store:
App entry point: /src/index.js
import { StoreProvider } from './store'
import rootReducer from './store/rootReducer'
import rootActions from './store/rootActions'
ReactDOM.render(
<StoreProvider rootReducer={rootReducer} rootActions={rootActions}>
<App />
</StoreProvider>
, document.getElementById('root'))
<Shop/>
component
const Shop = () => {
const { state } = useStore()
const { productsActions } = useActions()
useEffect(() => {
state.products.length === 0 && productsActions.retrieveProducts()
}, [state.products, productsActions])
return (
<div className='grid-x grid-padding-x'>
<div className='cell'>
{
/**
* Call an endpoint to fetch products from store
*/
items && items.map((item, i) => (
<div key={i} className='product'>
Name: { item.name }
Amount: { item.amount }
<Button type='submit'>Buy</Button>
</div>
))
}
</div>
</div>
)
}
Happy coding!
Top comments (4)
Hi, I read your article and stumbled across the point where you mention the connect method of redux. Now since hooks came out you can use the new redux hooks useDispatcher and useSelector. Those two make working with redux much easier!
Nothing to do with the content of the post but I did this in an attempt to avoid adding redux to a new app at work and then found myself having to implement things that were already done by react redux bindings. I wasted a lot of time trying to work around by just using context but ended up using redux anyways because it just made more sense.
I only see not using redux if your app really doesnt require much state management. I could be wrong but I spent 6-7 weeks doing this and couldn't justify not using redux.
Only yesterday, I finally gave in and replaced contexts with redux store.
To easy the pain, I used easy-peasy, which wraps Redux underneath the hood.
In my case, the migration was snappy as I was following Kent C. Dodd's Context Pattern mentioned in How to use React Context effectively, which exposes state & actions/dispatch separately via hooks, which is what Easy-Peasy does.
The upside was that, I was forced to "group" related states and actions together.
The reason my context approach was getting out of hand was, not because of any issues Context API (well, it does trigger re-render when it's updated everywhere) but because I was dumping all possible states without organizing them.
Awesome!