DEV Community

Cover image for React Context with TypeScript: Part 4 - Creating a context with no default and no undefined check
Carl
Carl

Posted on • Edited on • Originally published at carlrippon.com

React Context with TypeScript: Part 4 - Creating a context with no default and no undefined check

This is the final post in a series of posts on React context with TypeScript. In the previous post, we consumed a context in a class component. In this post, we will learn how to create a context without having to pass a default and then do any undefined checks when consuming it.

The problem

The type for createContext requires a default value to be passed into it, but often it doesn't make sense to pass a default. So, we end up passing undefined as the default:

const ThemeContext = React.createContext<
  ThemeContextType | undefined
>(undefined);

... and then checking for undefined everywhere we consume it:

const { theme, setTheme } = useTheme()!;

Creating a wrapper for creating a context

A solution is to create a wrapper around createContext that deals with the default and the undefined check:

export function createCtx<ContextType>() {
  const ctx = React.createContext<
    ContextType | undefined
  >(undefined);
  function useCtx() {
    const c = React.useContext(ctx);
    if (!c)
      throw new Error(
        "useCtx must be inside a Provider with a value"
      );
    return c;
  }
  return [useCtx, ctx.Provider] as const;
}

This function first creates the context with the generic type passed into it with undefined as its default value.

A nested function is then defined, which wraps the useContext hook. A variable c is assigned to the return value of the useContext hook which is the generic type passed in or undefined:

Type of c

We then throw an error if c is falsy, which deals with the undefined check. This means that when c is returned from the nested function, it can't undefined and is only the generic type we passed in:

Type of c

Notice also we use a const assertion (as const) on the last line to ensure TypeScript infers a tuple type rather than an array of union types.

Creating a context

We can now use our createCtx function to create a context rather than React's createContext:

const [useTheme, CtxProvider] = createCtx<
  ThemeContextType
>();

Creating a provider

Our createCtx function returns a tuple, which contains a provider component in the second element (CtxProvider). We can the create our specific provider component containing our required state:

export const ThemeProvider = ({
  children
}: Props) => {
  const [theme, setTheme] = React.useState(
    "white"
  );
  ...
  return (
    <CtxProvider value={{ theme, setTheme }}>
      {children}
    </CtxProvider>
  );
};

This can then be placed at the appropriate position in the component tree:

export const App = () => (
  <ThemeProvider>
    <Header />
  </ThemeProvider>
);

Consuming the context

Our createCtx also returns a hook (useTheme) in the tuples first element. We can use this without having to do any undefined checks:

const Header = () => {
  const { theme, setTheme } = useTheme();
  return (
    <div style={{ backgroundColor: theme }}>
      <select
        value={theme}
        onChange={e =>
          setTheme(e.currentTarget.value)
        }
      >
        <option value="white">White</option>
        <option value="lightblue">Blue</option>
        <option value="lightgreen">Green</option>
      </select>
      <span>Hello!</span>
    </div>
  );
};

Neat!

A full working implementation is available by clicking the link below. Give it a try and change the theme value and see the background change color.

Open full implementation

Wrap up

The createCtx function is a generic function that can be used to create contexts for many situations. It simplifies consuming code because checks for undefined are not necessary.

That concludes this series of posts on React context with TypeScript. I hope you enjoyed it!

Originally published at https://www.carlrippon.com/react-context-with-typescript-p4/ on Mar 10, 2020.

Top comments (0)