React Context: One way to do it
Kevin Sullivan
Posted on January 17, 2020
If you haven't already read Kent C. Dodds' post on React Context, do it now.
I've since tweaked it to my own liking.
import * as React from "react";
import { useTicTacToe } from "./hook";
export const TicTacToeContext = React.createContext<
ReturnType<typeof useTicTacToe>
>(undefined);
export const TicTacToeProvider: React.FC = ({ children }) => (
<TicTacToeContext.Provider value={useTicTacToe()}>
{children}
</TicTacToeContext.Provider>
);
export const useTicTacToeContext = () =>
React.useContext(TicTacToeContext) || missingProvider();
const missingProvider = () => {
throw new Error(
"useTicTacToeContext must be used inside a TicTacToeProvider"
);
};
// SELECTORS
export const useBoard = () => useTicTacToeContext().board;
export const useCurrentPlayerId = () => useTicTacToeContext().currentPlayerId;
export const useGameOver = () => useTicTacToeContext().gameOver;
export const useTied = () => useTicTacToeContext().tied;
export const useWinnerId = () => useTicTacToeContext().winnerId;
export const useWinningSquares = () => useTicTacToeContext().winningSquares;
export const useMove = () => useTicTacToeContext().move;
export const useNewGame = () => useTicTacToeContext().newGame;
Keep it stupid simple
For me, the context's sole job is to to provide the return value from a hook, to all its children. So, in this context, the hook is imported, passed into the context....and that's it.
Feel free to not shoot yourself in the foot
The context defaults to undefined. The react docs indicate that you can default to any value.
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.
Yeah, this might make is easier to test, but when you can render the provider in the test, it's not that much easier. Besides, easy is the enemy of simple. Defaulting to undefined,
export const TicTacToeContext = React.createContext<TicTacToeContextType>(
undefined
);
combined with
export const useTicTacToeContext = () =>
React.useContext(TicTacToeContext) || missingProvider();
const missingProvider = () => {
throw new Error(
"useTicTacToeContext must be used inside a TicTacToeProvider"
);
};
prevents you from rendering the consumers outside of a provider. I haven't yet found a good reason to render a consumer without rendering a provider. If you know of any, let me know. Thanks.
Lastly, a few selectors to make reading the hooks a little nicer.
A few other tidbits:
Typescript helper
ReturnType<typeof useTicTacToe> // the type of the return value of the useTicTacToe
Performance
The only time I consider renders is when there is a callback that might be used in a useEffect
and mess up the deps array. If the callback is going into a component that doesn't useEffect
, like an <input>
, then I don't bother with any performance optimizations.
Conclusion
Well, this is one way to do it.
What's your way?
Posted on January 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.