Build Your Own React-Redux Using useReducer and useContext Hooks

By Chidume Nnamdi 🔥💻🎵🎮

We often use the react-redux library in our React apps when we decide to include some sort of state management tool.

The react-redux library makes it very easy to use Redux in React. The library (react-redux) makes use of the Context API to pass the store down to nesting components without the long props chaining.

The Provider encapsulates the root component, and all other components can plug into the context to retrieve or write to it.

A free tip: Use Bit (github) to easily manage, share and reuse your React components. Modularity and reusability are key factors to a better and more sustainable code!

Share components to bit.dev -> use and sync them across projects

It uses the old React context to provide the store returned by the createStore in the redux library down the component tree.

const store = createStore{reducers}
class App {
render() {
return (
<Provider store={store}>
<MyComponent />
</Provider>
)
}
}

It uses the connect function to create a Higher-Order component

class LoginComponent extends Component {
// ...
}
export default connect(LoginComponent)

that connect the dispatch function in store to the component and also maps the state of the store to the props of the component.

class LoginComponent extends Component {
// ...
}
export default connect(LoginComponent)

With it, the component gets the state and can also dispatch actions to the store to change the state.

In this post, we will replicate the features of the react-redux library using hooks.

Hooks is one of the greatest features ever added to the React library since its creation. Hooks brought ‘state’ to functional components. Now, functional components can create and manage local states on their own just like stateful class components. This is made possible by the useState hook. Other hooks are:

  • useReducer
  • useLayoutEffect
  • useContext
  • useMemo
  • useRef
  • useCallback
  • useDebugValue

useReducer is a hook used to setup and manange state transitions in our components. The general form of the useReducer hook is:

const [state, dispatch] = useReducer((state, action)=> {...}, initialState)

The first argument is the reducer function, this is a pure function that accepts the current state and an action and returns the state. The useReducer returns an array, we destructured the array to get the state returned and the dispatch action.

import React, { useReducer } from 'react'
function App () {
const counter = 0
const [state, dispatch] = useReducer((state, action) => {state + action}, counter);
return (
<div>
<h3>Counter: {state}</h3>
<button onClick={(e)=> dispatch(1)}>Incr.</button>
<button onClick={(e)=> dispatch(-1)}>Decr.</button>
</div>
)
}

We have a counter app here, the reducer function simply adds the action and the state passed to it and returns the summation, this is an addition reducer. See it returns the state in the state variable and the dispatch function in the dispatch variable destructured from the array returned by the useReducer.

We called the dispatch function with value 1 in the onClick listener on the Incr. button so whenever it is clicked an action is dispatched, the reducer function is run with params of the current state and the value 1, this increments the state and we have 1 in our DOM. When we click the Decr. button the dispatch function is called with value -1, the reducer function is called with the current state and the value -1, the result will be 0 because the previous state was 1 and the dispatch is -1, so summing:

1 + (-1) = 0

This hook is used to get the current context of a Provider. To create and provide a context, we use the React.createContext API.

const myContext = React.createContext()

We put the root component between the myContext Provider:

function App() {
return (
<myContext.Provider value={900}>
<Root />
</myContext.Provider>
)
}

To consume the value provided by the <myContext.Provider></myContext.Provider> we use the useContext hook.

function Root() {
const value = useContext(myContext)
return (
<>
<h3>My Context value: {value} </h3>
</>
)
}

Instead of using Redux to create our store, we use useReducer hook. The useReducer hook provides us with the state and dispatch. The state will be passed down to the nesting components and the dispatch function that updates the state will also be passed down with the state, so any subscribing component can call the dispatch function to change the state, the change in state will be received from the top to the leaves, and all components subscribing to the store will update to reflect the new changes.

The useReducer will be run on the topmost component where we want to store to boil down from. Then, we will create a context using React.createContext and use the context to reference the Provider

import React, { Component, useReducer, useContext } from 'react';
import { render } from 'react-dom';
const MyContext = React.createContext()
const MyProvider = MyContext.Provider;

Here we import the useReducer and useContext hook functions. Next, we create a context and get the Provider property assigned to MyProvider.

Now we will create a component FirstC render the component between MyProvider component in App:

// ...
function FirstC(props) {
return (
<div>
</div>
)
}
function App () {
return (
<div>
<MyProvider>
<FirstC />
</MyProvider>
</div>
)
}
render(<App />, document.getElementById('root'));

Now let’s create our store using useReducer. We will make our state to hold a Books property. The Books property will hold the name of the current book. So we will create an initial state:

const initialState = {
Books: 'Dan Brown: Inferno'
}

This is what the subscribing components will first get when rendered before any updating. We will pass it to useReducer as the initial state, then we create a reducer function that will update the state based on the type of action dispatched.

// ...
function App () {
const initialState = {
Books: 'Dan Brown: Inferno'
}
    const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'ADD_BOOK':
return { Books: action.payload }
default:
return state
}
}, initialState);
return (
<div>
<MyProvider>
<FirstC />
</MyProvider>
</div>
)
}

See, we have our reducer function to listen for ADD_BOOK action, we return a new Book state object with the action payload as value. If no action is caught we return the current state.

Now we will pass an object consisting of the state and dispatch to MyProvider via its value prop:

// ...
function App () {
const initialState = {
Books: 'Dan Brown: Inferno'
}
    const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'ADD_BOOK':
return { Books: action.payload }
default:
return state
}
}, initialState);
return (
<div>
<MyProvider value={{state, dispatch}}>
<FirstC />
</MyProvider>
</div>
)
}

Now we need to connect subscribing components to the state and dispatch values.

Like react-redux, we will create a function connect that will attach the state and dispatch function to the props of the component, so they can reference the state and dispatch from their props. This connect function will return a higher-order component whose props is connected to the store.

// ...
function connect(mapStateToProps, mapDispatchToProps) {
return function (Component) {
return function () {
const {state, dispatch} = useContext(MyContext)
const stateToProps = mapStateToProps(state)
const dispatchToProps = mapDispatchToProps(dispatch)
const props = {...props, ...stateToProps, ...dispatchToProps}
            return (
<Component {...props} />
)
}
}
}
// ...

See it receives functions as params. These functions will map the state to the props of the component and the other will map the dispatch function to the props of the component. It returns a function component that consumes the context MyContext using the useContext hook function, the useContext will make it get the value(an object consisting of the state and the dispatch function returned by the useReducer hook, {state, dispatch}) passed to the MyProvider component in the App component.

Then we run the mapStateToProps function param passing in the state because it will map the state to the props. Then likewise we run mapDispatchToProps to attach the dispatch function to the component props. Next, we aggregate the objects into the props object and spread them as props to the Component. With this, any component passed to the connect will have its props the state and dispatch, so it can access the state props.state and the dispatch actions using the dispatch function.

Now, let’s make our FirstC component connect to the store and dispatch an ADD_BOOK action to change the state. First, we will create the functions that will map the state and the dispatch to the FirstC component props

// ...
function mapStateToProps(state) {
return {
books: state.Books
}
}
function mapDispatchToProps(dispatch) {
return {
dispatchAddBook: (payload)=> dispatch({type: 'ADD_BOOK', payload})
}
}
// ...

In the mapDispatchToProps function, we have function dispatchAddBook that takes the payload we want to be dispatched, then we call the dispatch with action ADD_BOOK and the payload. So in the FirstC component, we will dispatch ADD_BOOK action by calling `props.dispatchAddBook.

Now we call the connect function with these function, then call the returned function with FirstC.

// ...
const HFirstC = connect(mapStateToProps, mapDispatchToProps)(FirstC)
// ...

See, the higher-order functional component returned is stored in HFirstC. This is what will be rendered instead of FirstC.

// ...
function App () {
const initialState = {
Books: 'Dan Brown: Inferno'
}
    const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'ADD_BOOK':
return { Books: action.payload }
default:
return state
}
}, initialState);
return (
<div>
<MyProvider value={{state, dispatch}}>
<HFirstC />
</MyProvider>
</div>
)
}
// ...

Now, in FirstC lets make the JSX in the render function, render the state and also add a function so when clicked will dispatch the ADD_BOOK with payload Dan Brown: Origin

// ...
function FirstC(props) {
return (
<div>
<h3>{props.books}</h3>
<button onClick={()=> props.dispatchAddBook("Dan Brown: Origin")}>Dispatch 'Origin'</button>
</div>
)
}
// ...

Now, we are done. Running the app we will see Dan Brown: Inferno show up on the action, that is because it is the initial state and the subscribing components will render the initial state on startup. Then, when we click the Dispatch 'Origin' button on the FirstC component the DOM will render Dan Brown: Origin

Multiple components subscribing to our mini React Redux

We had only one component subscribed to the store. Now, let’s create on component to subscribe to the store.

// ...
function SecondC(props) {
return (
<div>
<h3>{props.books}</h3>
<button onClick={()=> props.dispatchAddBook("Dan Brown: The Lost Symbol")}>Dispatch 'The Lost Symbol'</button>
</div>
)
}
function _mapStateToProps(state) {
return {
books: state.Books
}
}
function _mapDispatchToProps(dispatch) {
return {
dispatchAddBook: (payload)=> dispatch({type: 'ADD_BOOK', payload})
}
}
const HSecondC = connect(_mapStateToProps, _mapDispatchToProps)(SecondC)
// ...

Now we will render FirstC and SecondC components side by side.

// ...
function App() {
// ...
return (
<div>
<MyProvider value={{state, dispatch}}>
<HFirstC />
<HSecondC />
</MyProvider>
</div>
)
}
// ...

When we click Dispatch 'Origin' in FirstC component both components will update to render Dan Brown: Origin, also when we click Dispatch 'The Lost Symbol' button in SecondC component, both components will update to reflect Dan Brown: The Lost Symbol

We have successfully created the react-redux library using hooks: useReducer and useContext. The useContext produced the store while the useReducer consumed the store and connected it to the functional components props value. Any action and state update, performed by a component, are picked up by the subscribing components no matter their relationship with the producing component and its hierarchy level.

The idea is simple; use useReducer to create the store, use the Context API to create the context and pass the store down, use useContext to consume the context.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me

Thanks !!!