DEV Community

Kevin Sullivan
Kevin Sullivan

Posted on

React Context: One way to do it

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;

Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

combined with

export const useTicTacToeContext = () =>
  React.useContext(TicTacToeContext) || missingProvider();

const missingProvider = () => {
  throw new Error(
    "useTicTacToeContext must be used inside a TicTacToeProvider"
  );
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?

Top comments (0)