Context Module Functions
June 19, 2021 •3 min read
Take this simple context example to increment and decrement a count value.
const CountContext = React.createContext();
const countReducer = (state, action) => {
switch (action.type) {
case "increment": {
return { ...state, count: state.count + 1 };
}
case "decrement": {
return { ...state, count: state.count - 1 };
}
default: {
return state;
}
}
};
const CountProvider = () => {
const [state, dispatch] = React.useReducer(countReducer, { count: 0 });
const value = [state, dispatch];
return <CountContext.Provider value={value} />;
};
const useCount = () => {
return React.useContext(CountContext);
};
export { CountProvider, useCount };
We can use the context and create custom incrementCount and decrementCount functions which call dispatch
.
import { useCount } from 'context/count'
const Count = () => {
const [state, dispatch] = useCount()
const incrementCount = () => dispatch({type: 'increment'})
const decrementCount = () => dispatch({type: 'decrement'})
return (....)
}
The drawback of this approach is the messy API of dispatching actions directly, as well as it being open to typos on the action type.
We can mitigate the typos using Enums or Action Creators, or make the reducer throw an error on an unknown action type:
const countReducer = (state, action) {
switch (action.type) {
...
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
Helper Functions
One approach can be creating helper functions and include them in the context:
const incrementCount = React.useCallback(
() => dispatch({ type: "increment" }),
[dispatch]
);
const decrementCount = React.useCallback(
() => dispatch({ type: "decrement" }),
[dispatch]
);
const value = { state, incrementCount, decrementCount };
return <CountContext.Provider value={value} />;
This can then be consumed in the component:
const { state, incrementCount, decrementCount } = useCount();
But we now have to wrap the functions in React.useCallback
to enable us to add them to dependency arrays.
Context Module Functions Pattern
The Context Module Functions pattern is where we export helper functions that accept dispatch
.
const CountContext = React.createContext();
const countReducer = (state, action) => {
switch (action.type) {
case "increment": {
return { ...state, count: state.count + 1 };
}
case "decrement": {
return { ...state, count: state.count - 1 };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
};
const CountProvider = () => {
const [state, dispatch] = React.useReducer(countReducer, { count: 0 });
const value = [state, dispatch];
return <CountContext.Provider value={value} />;
};
const useCount = () => {
const context = React.useContext(CountContext)
if (context === undefined) {
throw new Error(`useCount must be used within a CountProvider`)
}
return context
}
const incrementCount = (dispatch) => dispatch({ type: "increment" });
const decrementCount = (dispatch) => dispatch({ type: "decrement" });
// Note how the functions are exported as a module and not part of the context value
export { CountProvider, useCount, incrementCount, decrementCount };
This is how we would use it:
import { useCount, incrementCounter, decrementCounter } from "context/count";
const Count = () => {
const [state, dispatch] = useCount();
return (
<>
<button onClick={() => decrementCount(dispatch)}>Decrement</button>
<button onClick={() => incrementCount(dispatch)}>Increment</button>
</>
);
};
This pattern helps with:
- Avoiding mistakes in dependency lists
- Improves performance
- Helps reduce duplication
- Makes code splitting easier