TypeScript + Reactでコンポーネントを書くとき、Context を使っているコンポーネントに対してどう型を書くのが正解か迷ったので、調べたことをメモしておきます。
調べるきっかけとなったコンポーネントは React 16.3 以前の Legacy Context を使った書き方になっていたのですが、ついでに新しい Context での書き方と、最新の Hooks の useContext()
を使った場合も調べます。
なお、Context および useContext()
についての説明は公式ドキュメントに譲ります。
目次
- 目次
- Contextを使う前のサンプルコード
- 1. React 16.3 以降の Context を使う場合
- 2. React 16.3 以前の Legacy Context を使う場合
- 3. React Hooksの 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;
ThemedButton
に theme
を渡すために 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)
の Provider
の value
や (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 と変わりません。