Skip to content

Latest commit

 

History

History
281 lines (220 loc) · 11.1 KB

context.md

File metadata and controls

281 lines (220 loc) · 11.1 KB
id title
context
Context

Basic Example

import * as React from "react";

interface AppContextInterface {
  name: string;
  author: string;
  url: string;
}

const AppCtx = React.createContext<AppContextInterface | null>(null);

// Provider in your app

const sampleAppContext: AppContextInterface = {
  name: "Using React Context in a Typescript App",
  author: "thehappybug",
  url: "http://www.example.com",
};

export const App = () => (
  <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);

// Consume in your app

export const PostInfo = () => {
  const appContext = React.useContext(AppCtx);
  return (
    <div>
      Name: {appContext.name}, Author: {appContext.author}, Url:{" "}
      {appContext.url}
    </div>
  );
};

You can also use the Class.contextType or Context.Consumer API, let us know if you have trouble with that.

Thanks to @AlvSovereign

Extended Example

Using React.createContext with an empty object as default value.

interface ContextState {
  // set the type of state you want to handle with context e.g.
  name: string | null;
}
//set an empty object as default state
const Context = React.createContext({} as ContextState);
// set up context provider as you normally would in JavaScript [React Context API](https://reactjs.org/docs/context.html#api)

Using React.createContext and context getters to make a createCtx with no defaultValue, yet no need to check for undefined:

import * as React from "react";

const currentUserContext = React.createContext<string | undefined>(undefined);

function EnthusasticGreeting() {
  const currentUser = React.useContext(currentUserContext);
  return <div>HELLO {currentUser!.toUpperCase()}!</div>;
}

function App() {
  return (
    <currentUserContext.Provider value="Anders">
      <EnthusasticGreeting />
    </currentUserContext.Provider>
  );
}

Notice the explicit type arguments which we need because we don't have a default string value:

const currentUserContext = React.createContext<string | undefined>(undefined);
//                                             ^^^^^^^^^^^^^^^^^^

along with the non-null assertion to tell TypeScript that currentUser is definitely going to be there:

return <div>HELLO {currentUser!.toUpperCase()}!</div>;
//                              ^

This is unfortunate because we know that later in our app, a Provider is going to fill in the context.

There are a few solutions for this:

  1. You can get around this by asserting non null:

    const currentUserContext = React.createContext<string>(undefined!);

    (Playground here) This is a quick and easy fix, but this loses type-safety, and if you forget to supply a value to the Provider, you will get an error.

  2. We can write a helper function called createCtx that guards against accessing a Context whose value wasn't provided. By doing this, API instead, we never have to provide a default and never have to check for undefined:

    import * as React from "react";
    
    /**
     * A helper to create a Context and Provider with no upfront default value, and
     * without having to check for undefined all the time.
     */
    function createCtx<A extends {} | null>() {
      const ctx = React.createContext<A | undefined>(undefined);
      function useCtx() {
        const c = React.useContext(ctx);
        if (c === undefined)
          throw new Error("useCtx must be inside a Provider with a value");
        return c;
      }
      return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple
    }
    
    // Usage:
    
    // We still have to specify a type, but no default!
    export const [useCurrentUserName, CurrentUserProvider] = createCtx<string>();
    
    function EnthusasticGreeting() {
      const currentUser = useCurrentUserName();
      return <div>HELLO {currentUser.toUpperCase()}!</div>;
    }
    
    function App() {
      return (
        <CurrentUserProvider value="Anders">
          <EnthusasticGreeting />
        </CurrentUserProvider>
      );
    }

    View in the TypeScript Playground

  3. You can go even further and combine this idea using React.createContext and context getters.

    /**
     * A helper to create a Context and Provider with no upfront default value, and
     * without having to check for undefined all the time.
     */
    function createCtx<A extends {} | null>() {
      const ctx = React.createContext<A | undefined>(undefined);
      function useCtx() {
        const c = React.useContext(ctx);
        if (c === undefined)
          throw new Error("useCtx must be inside a Provider with a value");
        return c;
      }
      return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple
    }
    
    // usage
    
    export const [useCtx, SettingProvider] = createCtx<string>(); // specify type, but no need to specify value upfront!
    export function App() {
      const key = useCustomHook("key"); // get a value from a hook, must be in a component
      return (
        <SettingProvider value={key}>
          <Component />
        </SettingProvider>
      );
    }
    export function Component() {
      const key = useCtx(); // can still use without null check!
      return <div>{key}</div>;
    }

    View in the TypeScript Playground

  4. Using React.createContext and useContext to make a createCtx with unstated-like context setters:

    export function createCtx<A>(defaultValue: A) {
      type UpdateType = React.Dispatch<
        React.SetStateAction<typeof defaultValue>
      >;
      const defaultUpdate: UpdateType = () => defaultValue;
      const ctx = React.createContext({
        state: defaultValue,
        update: defaultUpdate,
      });
      function Provider(props: React.PropsWithChildren<{}>) {
        const [state, update] = React.useState(defaultValue);
        return <ctx.Provider value={{ state, update }} {...props} />;
      }
      return [ctx, Provider] as const; // alternatively, [typeof ctx, typeof Provider]
    }
    
    // usage
    
    const [ctx, TextProvider] = createCtx("someText");
    export const TextContext = ctx;
    export function App() {
      return (
        <TextProvider>
          <Component />
        </TextProvider>
      );
    }
    export function Component() {
      const { state, update } = React.useContext(TextContext);
      return (
        <label>
          {state}
          <input type="text" onChange={(e) => update(e.target.value)} />
        </label>
      );
    }

    View in the TypeScript Playground

  5. A useReducer-based version may also be helpful.

Mutable Context Using a Class component wrapper

Contributed by: @jpavon

interface ProviderState {
  themeColor: string;
}

interface UpdateStateArg {
  key: keyof ProviderState;
  value: string;
}

interface ProviderStore {
  state: ProviderState;
  update: (arg: UpdateStateArg) => void;
}

const Context = React.createContext({} as ProviderStore); // type assertion on empty object

class Provider extends React.Component<{}, ProviderState> {
  public readonly state = {
    themeColor: "red",
  };

  private update = ({ key, value }: UpdateStateArg) => {
    this.setState({ [key]: value });
  };

  public render() {
    const store: ProviderStore = {
      state: this.state,
      update: this.update,
    };

    return (
      <Context.Provider value={store}>{this.props.children}</Context.Provider>
    );
  }
}

const Consumer = Context.Consumer;

Something to add? File an issue.