Implementing State Management using Context API and Hooks in React
Monique Dingding
Posted on September 27, 2019
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!
Posted on September 27, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.