dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

TypeScript: ReactのContextに型をつける(useContextと16.3以前のLegacy Contextも含む)

TypeScript + Reactでコンポーネントを書くとき、Context を使っているコンポーネントに対してどう型を書くのが正解か迷ったので、調べたことをメモしておきます。

調べるきっかけとなったコンポーネントは React 16.3 以前の Legacy Context を使った書き方になっていたのですが、ついでに新しい Context での書き方と、最新の Hooks の useContext() を使った場合も調べます。

なお、Context および useContext() についての説明は公式ドキュメントに譲ります。

目次


Contextを使う前のサンプルコード

import React from "react";
import "./App.css";

const ThemedButton: React.FC<{ theme: string }> = props => (
  <button className={props.theme}>Click me</button>
);

const Toolbar: React.FC<{ theme: string }> = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton theme={props.theme} />
  </div>
);

const App: React.FC = () => {
  return (
    <div className="app">
      <header className="app-header">
        <Toolbar theme="dark" />
      </header>
    </div>
  );
};

export default App;

ThemedButtontheme を渡すために Toolbar にも props を渡しています。


1. React 16.3 以降の Context を使う場合

import React from "react";
import "./App.css";

const ThemeContext = React.createContext("light"); // (1)

class ThemedButton extends React.Component {
  static contextType = ThemeContext; // (1)
  context!: React.ContextType<typeof ThemeContext>; // (2)

  render() {
    return (
      <button className={this.context}>Click me</button>
    );
  }
}

const ThemedButtonFC: React.FC<{ theme: string }> = props => (
  <button className={`app-toolbar-button ${props.theme}`}>Click me</button>
);

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark"> // (1-2)
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
      <div>
        <div>
          <ThemeContext.Consumer>
            {value => <ThemedButtonFC theme={value} />} // (1-3)
          </ThemeContext.Consumer>
        </div>
      </div>
    </ThemeContext.Provider>
  );
};

export default App;

ThemedButtonFC および <ThemeContext.Consumer>...</ThemeContext.Consumer> については、Consumerを使った箇所も確認したかったのでオマケです。

新しいContextでは、React.createContext(defaultValue) を使ってContextオブジェクトを作ります。
コメントで(1) と書いたところは、通常のContextのお作法通りに書いた箇所です。
これだけで、(1-2)Providervalue(1-3)Consumer 内の関数の value は正しく型チェックされるようになります。

ポイントは (2) のところで、どうやらこれがないとコンポーネント内のthis.contextが型チェックされないようです。
この書き方はReactの型定義ファイルを参考にしました。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L421-L434

 /**
  * If using the new style context, re-declare this in your class to be the
  * `React.ContextType` of your `static contextType`.
  *
  * ```ts
  * static contextType = MyContext
  * context!: React.ContextType<typeof MyContext>
  * ```
  *
  * @deprecated if used without a type annotation, or without static contextType
  * @see https://reactjs.org/docs/legacy-context.html
  */


2. React 16.3 以前の Legacy Context を使う場合

Legacy Context の場合はこのようになります。

import React from "react";
import PropTypes from "prop-types";
import "./App.css";

// (1)
type ThemeContext = {
  theme: string;
};

class ThemedButton extends React.Component {
  static contextTypes = {
    theme: PropTypes.string
  };
  context!: ThemeContext; // (1)

  render() {
    return (
      <button className={this.context.theme}>
        Click me
      </button>
    );
  }
}

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

class App extends React.Component {
  static childContextTypes = {
    theme: PropTypes.string
  };

  getChildContext(): ThemeContext { // (1)
    return { theme: "light" };
  }

  render() {
    return (
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
    );
  }
}

export default App;

公式ドキュメント相当の信頼できそうな情報は見つけられなかったのですが、こちらのブログを参考にしました。

ポイントとしては、Legacy Contextの場合、コンテキストはPropTypesで記述するためランタイムでのチェックができません。
そのため、(1) にあるように同じ構造のTypeScriptの型を定義した後、親のgetChildContext()、子のthis.contextに型を付けています。

ブログの記事との違いとして、ブログでは

context: ThemeContext;

としてますが、これだと--strictPropertyInitializationオプションがONのときに以下のエラーが出ます。

Property 'context' has no initializer and is not definitely assigned in the constructor. ts(2564)


3. React Hooksの useContext() を使った場合

最後に、Hooksを使った場合。

import React, { useContext } from "react";
import "./App.css";

const ThemeContext = React.createContext("light");

const ThemedButton: React.FC = props => {
  const theme = useContext(ThemeContext);

  return <button className={theme}>Click me</button>;
};

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark">
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
    </ThemeContext.Provider>
  );
};

export default App;

コンポーネントが Function Component で書けるようになるということ以外は 1 と変わりません。