DEV Community

Monique Dingding
Monique Dingding

Posted on

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!

Top comments (4)

Collapse
 
whymatter profile image
Oliver

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!

Collapse
 
marko911 profile image
Marko Bilal

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.

Collapse
 
dance2die profile image
Sung M. Kim • Edited

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.

Collapse
 
alexribeirodev profile image
Alex Ribeiro

Awesome!