Implementing State Management using Context API and Hooks in React

gloriamaris

Monique Dingding

Posted on September 27, 2019

Implementing State Management using Context API and Hooks in React

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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
  }
}

Enter fullscreen mode Exit fullscreen mode

/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>
  )
}

Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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'))

Enter fullscreen mode Exit fullscreen mode

<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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Happy coding!

💖 💪 🙅 🚩
gloriamaris
Monique Dingding

Posted on September 27, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related