Introduction
State management in React
What is Redux?
Actions
// myAction.js const reduxAction = payload => { return { type: 'action description', payload } }; export default reduxAction;
Reducers
// myReducer.js const reducer = (state, action) => { const { type, payload } = action; switch (type) { case "action type": return { ["action description"]: payload }; default: return state; } }; export default reducer;
Store
// myStore.js
import { createStore } from 'redux'
const store = createStore(componentName);
What is the React Context API?
import React, {createContext} from 'react';
const newContext = createContext({ theme: 'dark' });
<Provider value={theme: 'light'}> { children } </Provider >
<Consumer>
{value => <span>{value}</span>}
</Consumer>
The useReducer hook
const myReducer = (state, action) => { const { type } = action; switch (action) { case 'action description': const newState = // Perform an action based on this action description return newState; default: throw new Error() // Throw a new error if the switch case is not picked up } } const [state, dispatch] = useReducer(myReducer, []);
In the lines of code above, we have defined our reducer function, which we used as an argument in useReducer to instantiate a new state. We also get a dispatch function from the return value of useReducer.
// This allows us to do this return ( <button onClick={() => dispatch({ type: 'action type'})}> {state} </button> )
To put it all together . . .
Now, let us explore how to combine useReducer and Context API to create some elegant code.
import React, { createContext, useReducer } from 'react';
const initialState = {};
const store = createContext(initialState);
const { Provider } = store;
const StateProvider = ({ children }) => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'action description':
const newState = // perform an action based on the action description
return newState;
default:
throw new Error();
};
}, initialState);
return <Provider value={{ state, dispatch }}>{children}</Provider>;
};
export { store, StateProvider }
To use our state, we’ll need to wrap it around the component whose children would need access to the store. If we want a global state, we will do this:
// store.js
// root index.js file
import { createRoot } from "react-dom/client";
import App from './App';
import { StateProvider } from './store.js';
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StateProvider>
<App />
</StateProvider>
);
Now, you can access your store from any component in the component tree. All you need to do is import the useContext hook from React and your store from store.js.
// deeplyNestedComponent.js
import React, { useContext } from 'react';
import { store } from './store.js';
const deeplyNestedComponent = () => {
const globalState = useContext(store);
console.log(globalState); // this will return { theme: light }
};
Similarities and differences between Redux and useReducer + Context API
Similarities
- Both use the “Flux architecture” (a design pattern that uses actions and reducers).
- They both solve the problem of “prop drilling.”
- Both create a separation of concerns between your state management logic and your UI.
Differences
- Redux passes down an instance of the current Redux store, while Context passes down only the current state value. This means every change to state value results in all components changing in Context, while Redux only triggers re-render for components which use the portion of the state.
- Redux is a standalone library you need to install to use with React, unlike Context, which comes with React Core.
- Redux supports using middleware, which brings opportunities to perform complex operations before modifying your state. To do this with Context, you can use useEffect.
- Redux store can be visualized with the Redux DevTools extension. This comes in handy with troubleshooting. You could replicate this in Context with the React Developer Tools and a logger in Reducer; however, this is more tedious.
Use cases to aid your decision-making
When you do not need a large, combined reducer, using Redux may be redundant and become overkill.
Use Context API + useReducer when:
- The app is mid-sized.
- You’re working on an isolated part of a large application whose state has become complex but does not need to be accessed by other components.
- Your state does not change often.
- The codebase of your application is relatively small.
Use Redux when:
- You have many application states required in many places in the app.
- The app requires the input of multiple developers on a medium-to-large codebase.
- The state will be updated frequently.
- The logic to update the state is complex.
- You want to visualize the changes to your state as it is triggered.
Conclusion
In modern web development, choosing the right tool is important. You can use Redux for your global state and use isolated Context for some larger components within the app.
The next time you are about to reach for the Redux sledgehammer, ask yourself: do I really need Redux, or can Context API get this done?