Implementing State Management using Context API and Hooks in React
Tags: react, redux, javascript, frontend
Published: 2019-09-27 10:54:51

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](https://kentcdodds.com/blog/prop-drilling), but that's just me.

With React's release of production-grade features such as [Context API](https://reactjs.org/docs/context.html) and [Hooks](https://reactjs.org/docs/hooks-intro.html), 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](https://frontarm.com/james-k-nelson/when-context-replaces-redux/), 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.

```javascript
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`

```javascript
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`

```javascript
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.

```javascript
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](https://reactjs.org/docs/hooks-reference.html) if you are unfamiliar with many of the concepts introduced above.

### Combining Actions and Reducers
**Root reducer:** `/store/rootReducer.js`

```javascript
import { combineReducers } from 'redux'
import productsReducer from './products/reducers'

export default combineReducers({
  products: productsReducer
})
```
**Root actions:** `/store/rootActions.js`

```javascript
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`

```javascript
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**

```javascript
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!