In the beginning of development with React, Redux has been a great tool for managing data with certain drawbacks. The drawbacks include:
React has introduced context and its own hook 'useContext' to prevent from passing data through layers. With the hook useReducer, we can easily create a dispatch function by yourself. With the help of context, we can use the dispatch function everywhere, then why not make a try with replacing redux with useContext and useReducer ?
In this article, I will write down a tutorial on how to replace redux by useContext and useReducer with a working example. The code is here.
Just before this tutorial, I want to share something with hooks. React Hooks reinforce the development of react and make it more flexible. In general, there are two ways : 'use' and 'with'. 'use' is always used for customizing hooks, for example, 'useEffect', or customized 'useMyEffect', 'useContext', or 'useMyContext'. 'with' is always used as HOC component. You can check an example of it with my article Create HOC pattern 'withContext' for context.
Ok, let's start our tutorial:
The first step is to create Context. In react-redux, we distinguish state and dispatch with 'mapStateToProps' and 'mapDispatchToProps', so in our example, we also create two context to distinguish 'data' and 'action'.
Let's create two contexts: StateContext and DispatchContext with React.createContext
.
const initialContext = {...}
const StateContext = React.createContext(initialContext)
const DispatchContext = React.createContext(undefined)
I am making a simple example, and this reducer function supports two actionTypes: ADD and SUBTRACT.
export const ActionTypes = {
ADD: "ADD",
SUBTRACT: "SUBTRACT",
}
export const reducer = (state, action) => {
switch (action.type) {
case ActionTypes.ADD:
return ++state
case ActionTypes.SUBTRACT:
return --state
default:
return state
}
}
Then, in the ContextProvider component, we wrap our children component inside the two context providers, to make data and actions available anywhere.
We also need to use useReducer with the reducer function to have state and dispatch, and then pass them as the initial values in <StateContext.Provider/>
and <DispatchContext.Provider/>
export const ContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialContext)
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
In order to use the state and dispatch actions everywhere in the app, we customize two hook functions.
In the hook useUIDispatch, we get the dispatch function from context, return a series of actions with ActionTypes. We use useCallback and useMemo for memoization.
export const useUIState = () => {
return React.useContext(StateContext)
}
export const useUIDispatch = () => {
const dispatch = React.useContext(DispatchContext)
if (dispatch === undefined) {
throw new Error("useBookingDispatch must be used within a BookingProvider")
}
const add = React.useCallback(() => {
dispatch({ type: ActionTypes.ADD })
}, [dispatch])
const subtract = React.useCallback(() => {
dispatch({ type: ActionTypes.SUBTRACT })
}, [dispatch])
return React.useMemo(
() => ({
add,
subtract,
}),
[dispatch]
)
}
<App/>
inside the Providerfunction App() {
return (
<ContextProvider>
<>
<Number />
<Button />
</>
</ContextProvider>
)
}
import { useUIDispatch } from "./UIContext"
export const Button = () => {
const { add, subtract } = useUIDispatch()
return (
<>
<button
type="button"
onClick={() => {
add()
}}
>
+
</button>
<button
type="button"
onClick={() => {
subtract()
}}
>
-
</button>
</>
)
}
export const Number = () => {
const state = useUIState()
return <>{state}</>
}
That's all of it. You can find a working demo on here. Thanks for reading !