ReactにTypeScriptを使おうとすると、TypeScriptの基礎知識だけでは足りず、Reactの型について知らなければなりません。それで、戸惑うことも多いでしょう。そういう人たちのためにReactとTypeScriptの使い方をまとめたのが「React+TypeScript Cheatsheets」です。本稿では「useReducer」の項を参考に、useReducerフックで簡単なカウンターをつくってみます。
TypeScriptが加わったReactのひな型アプリケーションをつくる
TypeScriptを加えたReactのひな型アプリケーションは、Create React Appでつくりましょう。コマンドラインツールでnpx create-react-app アプリケーション名に、オプション--template typescriptを添えてください。
npx create-react-app react-typescript-usereducer --template typescript
アプリケーションのディレクトリ(react-typescript-usereducer)に切り替えて、npm startとコマンド入力すれば、ローカルサーバでひな型アプリケーションが開きます。
カウンターをつくる
まずは、リデューサのモジュール(src/reducer.ts)です。リデューサはJSX要素を返しませんので、拡張子は.tsxでなく.tsとしました(「【TypeScript】.ts拡張子と.tsx拡張子」参照)。リデューサ(reducer)に渡すふたつの引数(stateとaction)に型づけが要ります。型エイリアス(type aliases)で定めました。
type STORE = { count: number };
type ACTIONTYPE =
| { type: "increment" }
| { type: "decrement" };
const reducer = (state: STORE, action: ACTIONTYPE) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
};
export default reducer;
つぎに、カウンターの表示と操作のモジュール(src/Counter.tsx)です。ここで、useReducerフックを使いました。TypeScriptの型推論が働きますので、型づけしなくても動きます。
import React from 'react';
import reducer from './reducer';
const initialState = { count: 0 };
const Counter = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<div>
<button onClick={() => dispatch({ type: "decrement" })}>
-
</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: "increment" })}>
+
</button>
</div>
);
};
export default Counter;
ルートのモジュール(src/App.tsx)は、カウンターのコンポーネント(Counter)をimportしてJSX要素に差し込むように書き替えましょう。これでカウンターのアプリケーションは、とりあえず動きます(図001)。
// import React from 'react';
// import logo from './logo.svg';
import Counter from './Counter';
import './App.css';
function App() {
return (
<div className="App">
{/* <header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header> */}
<Counter />
</div>
);
}
export default App;
図001■ useReducerフックを使ったカウンター
型づけを加える
アロー関数式で変数(const)に定めたリデューサをReactの関数として型づけするのが、React.Reducerです。引数に状態とアクションを受け取って、状態を返します。引数の型はジェネリックで与えてください(「Typing a useReducer React hook in TypeScript」の「The type of useReducer」参照)。
import React from 'react';
// const reducer = (state: STORE, action: ACTIONTYPE) => {
const reducer: React.Reducer<STORE, ACTIONTYPE> = (state, action) => {
};
関数コンポーネントを型づけるのは、React.FunctionComponentあるいはReact.FCです(「React+TypeScript Cheatsheets」の「Function Components」参照)。引数があれば、やはりジェネリックで型を与えられます。ただし、これらの型はTypeScriptの基本テンプレートから除かれますので、これからは使うことをお勧めしません。
型とは別に、コールバック関数はuseCallbackでメモ化しておきましょう(「Create React App 入門 09: useCallbackフックで無駄な処理を省く」参照)。
// import React from 'react';
import React, { useCallback } from 'react';
const Counter = () => {
const decrement = useCallback(() => dispatch({ type: "decrement" }), []);
const increment = useCallback(() => dispatch({ type: "increment" }), []);
return (
<div>
{/* <button onClick={() => dispatch({ type: "decrement" })}> */}
<button onClick={decrement}>
-
</button>
{/* <button onClick={() => dispatch({ type: "increment" })}> */}
<button onClick={increment}>
+
</button>
</div>
);
};
アクションのtypeを変数に入れる
リデューサのモジュール(src/reducer.ts)でアクションのtype(ACTIONTYPE)は、文字列のリテラル型(literal types)で定めました。ですから、型チェックはもちろん働きます。けれど、エディタのコード補完が利きません。そこで、変数に入れることにしましょう。ここでは、つぎのようにSymbol()を用いました。
export const incrementType = Symbol('increment');
export const decrementType = Symbol('decrement');
type ACTIONTYPE =
// | { type: "increment" }
| { type: typeof incrementType }
// | { type: "decrement" };
| { type: typeof decrementType };
const reducer: React.Reducer<STORE, ACTIONTYPE> = (state, action) => {
switch (action.type) {
// case "increment":
case incrementType:
// case "decrement":
case decrementType:
}
};
型を定めた変数(decrementTypeとincrementType)はexportしましたので、他のモジュールもimportさえすればコード補完が利くのです。これで、タイプミスも減らせるでしょう。
// import reducer from './reducer';
import reducer, { decrementType, incrementType} from './reducer';
const initialState = { count: 0 };
const Counter = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
// const decrement = useCallback(() => dispatch({ type: "decrement" }), []);
const decrement = useCallback(() => dispatch({ type: decrementType }), []);
// const increment = useCallback(() => dispatch({ type: "increment" }), []);
const increment = useCallback(() => dispatch({ type: incrementType }), []);
};
でき上がった作例は、サンプル001としてCodeSandboxに公開しました。それぞれのモジュールのコードや、具体的な動きについてはこちらをご覧ください。

