Introduction
The main goal of this article is to replace Redux with React Context API. We will achieve this by going to any connected component and replace a line of code with a connect
function that we will write.
When you finish reading this article, you'll be able to migrate from Redux to React Context API smoothly, quicker, and without rewriting plenty of code. We will achieve our goal doing these five steps:
- Step 1: Model your reducers in an object
- Step 2: Create the
combineReducer
function - Step 3: Create the app's provider
- Step 4: Create the
connect
function - Step 5: Connect the components to the provider
Prerequisites
You will need some knowledge in React, Redux and React Context API. Also, some knowledge of Higher Order Functions and currying functions in javascript would be helpful.
Before we start...
If you want to do this tutorial while you read, you can open this CodeSandbox using React and Redux in a new tab, fork it and start coding. The CodeSandbox is embedded at the end of the article for easy reference.
Folder Structure
Let's create the folder structure where we are going to place the code. This structure is a suggestion and should not dictate how you organize your code.
directory
└─── src
| | ... other-directories
| |─── provider
| | provider.js
| | connect.js
| | reducers.js
| |─── utils
| | combineReducers.js
| | ... other-directories
Step 1: Model your reducers
Go to reducers.js
and start placing all the reducers of the app with it's key inside an object.
import homeReducer from '../path' | |
import anotherReducer from '../path' | |
export const reducerExample = { | |
home: homeReducer, | |
example: anotherReducer | |
} | |
Step 2: Start writing the combineReducer
function
First, let's start writing the function that will create the root reducer and the parameters it needs.
import { reducers } from '../path' | |
/** | |
* Combines all reducers into one to create the root reducer of the | |
* application | |
* | |
* @param {object} reducers | |
*/ | |
export const combineReducer = (reducers) => { | |
const globalState = {}; | |
} |
2.1 • Start modeling the initial global state
In this iteration, we will call each reducer to get its initial state. Pass undefined
as the state
parameter and anything you want as the action
parameter, each reducer will return the initial state provided. Then, the results are added to the globalState
variable.
Object.entries() gives us an array of key-value pairs from the reducers
object passed as a parameter.
// ... prev. code ommited | |
export const combineReducer = (reducers) => { | |
const globalState = {}; | |
for (const [key, value] of Object.entries(reducers)) { | |
// check it its a reducer | |
if (typeof value === 'function') { | |
globalState[key] = value(undefined, { type: '__@@PLACEHOLDER_ACTION__' }); | |
} else { | |
// let the developer know the value is not a reducer | |
console.error(`${value} is not a function`); | |
} | |
} | |
} | |
2.2 • Start writing the global reducer function
Now, we are going to write the primary reducer function. We are writing this function to pass it to the useReducer hook later on.
// ... prev. code ommited | |
export const combineReducer = (reducers) => { | |
// ... prev. code ommited | |
/** | |
* Global reducer function; this is passed to the useReducer hook | |
* | |
* @param {object} state | |
* @param {object} action | |
*/ | |
const reducerFunction = (state, action) => { | |
// used to compare prevState vs the one returned by the reducer | |
let hasStateChanged = false; | |
// updated state object to be returned | |
const updatedStateByReducers = {}; | |
} | |
} | |
2.3 • Let’s update the global state
The most important part of this function is to get the next state. We are going to iterate through each reducer
available and pass the action
parameter to get the next state returned by the reducer.
In the iteration, we are going to compare the returned object with the current state. If these objects are not the same, it means there was an update, and we are going to replace the state
with the updated object.
// ... prev. code ommited | |
export const combineReducer = reducers => { | |
// ... prev. code ommited | |
const reducerFunction = (state, action) => { | |
// ... prev. code ommited | |
/** | |
* this is where dispatching happens; | |
* the action is passed to all reducers one by one. | |
*/ | |
for (const reducer in reducers) { | |
if (reducers.hasOwnProperty(reducer)) { | |
//match reducer with the state property it should update | |
const currentStateByKey = state[reducer]; | |
// get the reducer based on key | |
const currentReducer = reducers[reducer]; | |
// call each reducer and pass the action to it | |
const returnedStateByReducer = currentReducer(currentStateByKey, action); | |
// compared objects | |
const areStateByKeyEqual = returnedStateByReducer !== currentStateByKey; | |
hasStateChanged = hasStateChanged || areStateByKeyEqual; | |
// replace prevState by the updated state we just got | |
updatedStateByReducers[reducer] = returnedStateByReducer; | |
} | |
} | |
}; | |
}; |
Next, if the state has changed, we will return the updated state. If not, we return the previous state.
// ... prev. code ommited | |
export const combineReducer = reducers => { | |
// ... prev. code ommited | |
const reducerFunction = (state, action) => { | |
// ... prev. code ommited | |
// finally, return the updatedStateByReducers if its not equal to the prevState | |
return hasStateChanged ? updatedStateByReducers : state; | |
}; | |
}; |
2.4 • Finish writing the function
Finally, we will return an array with the initial state and the global reducer function. These values will be passed to the useReducer hook.
import * as Immutable from "immutable"; | |
import * as React from "react"; | |
export const combineReducer = reducers => { | |
const globalState = {}; | |
// set default state returned by reducer and its reducer | |
for (const [key, value] of Object.entries(reducers)) { | |
if (typeof value === "function") { | |
globalState[key] = value(undefined, { type: "__@@PLACEHOLDER_ACTION__" }); | |
} else { | |
console.error(`${value} is not a function`); | |
} | |
} | |
/** | |
* Global reducer function; this is passed to the useReducer hook | |
* | |
* @param {object} state | |
* @param {object} action | |
*/ | |
const reducerFunction = (state, action) => { | |
let hasStateChanged = false; | |
const updatedStateByReducers = {}; | |
/** | |
* this is where dispatching happens; | |
* the action is passed to all reducers one by one. | |
* we iterate and pass the action to each reducer and this would return new | |
* state if applicable. | |
*/ | |
for (const reducer in reducers) { | |
if (reducers.hasOwnProperty(reducer)) { | |
const currentStateByKey = state[reducer]; | |
const currentReducer = reducers[reducer]; | |
const returnedStateByReducer = currentReducer(currentStateByKey, action); | |
const areStateByKeyEqual = returnedStateByReducer !== currentStateByKey; | |
hasStateChanged = hasStateChanged || areStateByKeyEqual; | |
updatedStateByReducers[reducer] = returnedStateByReducer; | |
} | |
} | |
return hasStateChanged ? updatedStateByReducers : state; | |
}; | |
// return the initial state and the global reducer | |
return [globalState, reducerFunction]; | |
}; |
Step 3: Write the app's Provider
Let's write the app's provider. Then, import the object containing our reducers and the combineReducer
function from the previous step.
import * as React from 'react'; | |
import reducer from './path to the object containing the reducers/'; | |
import { combineReducer } from './path to combineReducer function'; | |
const AppStateProvider = React.createContext({}); | |
const ContextProvider = ({ children }) => { | |
} |
3.1 • Wrap up the function in the useCallback hook
We are not expecting our initial state, or the objects containing the reducers, to change on each re-render. So, let's optimize our function by using the useCallback hook.
useCallback will return a memoized version of the callback that only changes if one of the inputs has changed. There is no need for this function to run on every re-render.
// ... prev. code ommited | |
const ContextProvider = ({ children }) => { | |
const reducers = React.useCallback(() => { | |
return combineReducer(reducer); | |
}, [combineReducer]); | |
} | |
3.2 • Setup the rest of the provider
Next, let's wrap up the provider by doing a few more steps. First,
destructure the returned value of the useCallback function and set up the useReducer hook.
Once that's done, create a useMemo hook to wrap the returned value of the useReducer hook. Why useMemo? Since this is the global provider, there are two main reasons:
- Your context value changes frequently
- Your context has many consumers
// ... prev. code ommited | |
const ContextProvider = ({ children }) => { | |
const reducers = React.useCallback(() => { | |
return combineReducer(reducer); | |
}, [combineReducer]); | |
// call the function to get initial state and global reducer | |
const [initialState, mainReducer] = reducers(); | |
// setup useReducer with the returned value of the reducers function | |
const [state, dispatch] = React.useReducer(mainReducer, initialState); | |
// pass in the returned value of useReducer | |
const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); | |
}; | |
3.3 • Finish up the app's provider
Finally, let's return the consumer and export the provider and have it ready to pass context to all the children below it.
import * as React from 'react'; | |
import reducer from './path to the object containing the reducers/'; | |
import { combineReducer } from './path to combineReducer function'; | |
const AppStateProvider = React.createContext({}); | |
const ContextProvider = ({ children }) => { | |
const reducers = React.useCallback(() => { | |
return combineReducer(reducer); | |
}, [combineReducer]); | |
// call the function to get initial state and global reducer | |
const [initialState, mainReducer] = reducers(); | |
// setup useReducer with the returned value of the reducers function | |
const [state, dispatch] = React.useReducer(mainReducer, initialState); | |
// pass in the returned value of useReducer | |
const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); | |
return <AppStateProvider.Provider value={contextValue}>{children}</AppStateProvider.Provider>; | |
}; | |
export { ContextProvider, AppStateProvider as ContextConsumer }; | |
Step 4: Start writing the connect
function
The HOC function is the last function we will write before we start connecting the component to the provider.
This function will pass the state and the global reducer to each component. This "connects" to React Context Provider API and lets our components consume the values given by it.
The simplest use case of our function is a purely curried one and will take three parameters:
-
mapStateToProps
- required -
mapDispatchToProps
- optional - not all components dispatch actions -
Component
- required
import * as React from "react"; | |
import { ContextConsumer } from "./path to provider"; | |
/** | |
* function in charge of combining the factories, props and context to a React.Component | |
* | |
* @param {factory} mapStateToProps | |
* @param {factory} mapDispatchToProps | |
* @return {function(React.Component): function(object): *} | |
*/ | |
const connect = (mapStateToProps, mapDispatchToProps) => { | |
/** | |
* returns a funcion and passes the React.Component as a parameter | |
*/ | |
return Component => { | |
/** | |
* that returns a function, the 'props' parameter gives use | |
* any props that this component may have. If console.log this | |
* parameter and you'll see an empty object. | |
* | |
* We will place all combined props here | |
*/ | |
return props => {}; | |
}; | |
}; |
4.2 • Return a connected component
Let's place the Context.Consumer
to have access to the global state and dispatch function. Then, let's pass value.state
to the mapStateToProps
function.
Remember, the mapDispatchToProps
parameter is optional. If you pass this parameter, pass value.dispatch
to the mapDispatchToProps
function.
Finally, let's combine all props
and add the final result to the component. Now this component is connected
to the React Context API.
import * as React from "react"; | |
import { ContextConsumer } from "./path to provider"; | |
/** | |
* function in charge of combining the factories, props and context to a React.Component | |
* | |
* @param {factory} mapStateToProps | |
* @param {factory} mapDispatchToProps | |
* @return {function(React.Component): function(object): *} | |
*/ | |
const connect = (mapStateToProps, mapDispatchToProps) => { | |
/** | |
* returns a funcion and passes the React.Component as a parameter | |
*/ | |
return Component => { | |
/** | |
* that returns a function, the 'props' parameter gives use | |
* any props that this component may have. If console.log this | |
* parameter and you'll see an empty object. | |
* | |
* We will place all combined props here | |
*/ | |
return props => { | |
return ( | |
<ContextConsumer.Consumer> | |
{value => { | |
const stateToProps = mapStateToProps(value.state); | |
// the dispatch provided by the consumer; our global reducer | |
const dispatchToProps = mapDispatchToProps | |
? mapDispatchToProps(value.dispatch) | |
: null; | |
const componentProps = { | |
...stateToProps, | |
...props, | |
// not all components need to dispatch actions so its optional | |
...(mapDispatchToProps && { | |
...dispatchToProps | |
}) | |
}; | |
return <Component {...componentProps} />; | |
}} | |
</ContextConsumer.Consumer> | |
); | |
}; | |
}; | |
}; |
Step 5: The last step: connect our components
Now we can migrate from Redux to React Context Provider API quickly and with little refactoring on our part.
Replace the Redux Provider
Let's start by replacing the Provider
from Redux with the one we created. Your main app file should look like below:
import React from "react"; | |
import { ContextProvider } from "./path to provider"; | |
export default function App() { | |
return ( | |
<ContextProvider> | |
{ | |
// ... the rest of your app | |
} | |
</ContextProvider> | |
); | |
} |
Replace the Redux connect
function
Finally, let's replace the connect
function imported from Redux with our connect
function. Your component should look like below.
import * as React from "react"; | |
import { setFirstName } from "./actions"; | |
import InputField from "../../components/InputField"; | |
import connect from "../../path to our connect function"; | |
const Container1 = ({ getFirstName, getLastName, dispatchFistName }) => { | |
return ( | |
<article> | |
<p> | |
<strong> | |
{getFirstName} {getLastName} | |
</strong> | |
</p> | |
<InputField | |
label={"Set first name"} | |
value={getFirstName} | |
handleOnChange={value => dispatchFistName(value)} | |
/> | |
</article> | |
); | |
}; | |
/** | |
* A factory function that connects to the state given by the provider | |
* | |
* @param {object} state The state found in the provider | |
* @returns object | |
*/ | |
const mapStateToProps = state => { | |
return { | |
getFirstName: state.container1.firstName, | |
getLastName: state.container2.lastName | |
}; | |
}; | |
/** | |
* A factory function of methods that dispatches actions to the provider | |
* | |
* @param {function} dispatch Main dispatch function that updates the provider | |
*/ | |
const mapDispatchToProps = dispatch => { | |
return { | |
dispatchFistName: firstName => dispatch(setFirstName(firstName)) | |
}; | |
}; | |
export default connect(mapStateToProps, mapDispatchToProps)(Container1); |
You can access all the properties returned from mapStateToProps
and mapDispatchToProps
as props
inside the connected component.
Lastly, refresh the page and the app should be connected to the React Context API. Repeat this step to all the components that you want to replace Redux with React Context Provider API.
Here is a CodeSandbox with all the code we wrote and connected to React Context API
Conclusion
So there you have it, in five steps, we successfully moved away from Redux and replace it with React Context API.
- Model your reducers in an object
- Create the
combineReducer
function - Create the app's provider
- Create the
useConnect
function - Connect the components to the provider
Resources
Articles
- Using Context API in React (Hooks and Classes) by Tania Rascia.
- A Beginner’s Guide to Currying in Functional JavaScript by M. David Green.
CodeSanbox
Github Repo
Top comments (4)
Thanks for this article. This is really helpful.
thank you so much! I hope everything is clear and well explained :)
Yes. I don't know redux however I have a project with redux and I need to convert it to context API but I am stuck on actions and dispatchers I don't know how I will call dispatchers from components. :(
From context or from redux? Sorry for the late reply, I needed to disconnect from development outside of work for a few months and recharge