Build Your Own React-Redux Using useReducer and useContext Hooks
Get a deeper understanding of React-Redux by building it yourself!
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!
How does react-redux
work?
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
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
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
useContext
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>
</>
)
}
React-Redux — useReducer and useContext
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
Project Link
Conclusion
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 !!!