React + TypeScript: useReducerを使ったカウンターのアプリケーションに型づけする

ReactにTypeScriptを使おうとすると、TypeScriptの基礎知識だけでは足りず、Reactの型について知らなければなりません。それで、戸惑うことも多いでしょう。そういう人たちのためにReactとTypeScriptの使い方をまとめたのが「[React+TypeScript Cheatsheets](https://github.com/typescript-cheatsheets/react)」です。本稿では「[useReducer](https://github.com/typescript-cheatsheets/react#usereducer)」の項を参考に、`useReducer`フックで簡単なカウンターをつくってみます。


# TypeScriptが加わったReactのひな型アプリケーションをつくる
TypeScriptを加えたReactのひな型アプリケーションは、[Create React App](https://create-react-app.dev/)でつくりましょう。コマンドラインツールで`npx create-react-app アプリケーション名`に、オプション[`--template typescript`](https://create-react-app.dev/docs/adding-typescript/#installation)を添えてください。

```terminal
npx create-react-app react-typescript-usereducer --template typescript
```

アプリケーションのディレクトリ(`react-typescript-usereducer`)に切り替えて、`npm start`とコマンド入力すれば、ローカルサーバでひな型アプリケーションが開きます。


# カウンターをつくる
まずは、リデューサのモジュール(`src/reducer.ts`)です。リデューサはJSX要素を返しませんので、拡張子は`.tsx`でなく`.ts`としました(「[【TypeScript】.ts拡張子と.tsx拡張子](https://qiita.com/y-hira18/items/f67cfc45a70c7c25708a)」参照)。リデューサ(`reducer`)に渡すふたつの引数(`state`と`action`)に型づけが要ります。[型エイリアス(type aliases)](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases)で定めました。

```react:src/reducer.ts
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`](https://ja.reactjs.org/docs/hooks-reference.html#usereducer)フックを使いました。TypeScriptの型推論が働きますので、型づけしなくても動きます。

```react:src/Counter.tsx
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)。

```react:src/App.tsx
// 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フックを使ったカウンター
![2009001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/857a2ed8-98ae-8665-31d0-c2189bc4ab26.png)


# 型づけを加える
アロー関数式で変数(`const`)に定めたリデューサをReactの関数として型づけするのが、`React.Reducer`です。引数に状態とアクションを受け取って、状態を返します。引数の型はジェネリックで与えてください(「[Typing a useReducer React hook in TypeScript](https://www.sumologic.jp/blog/react-hook-typescript/)」の「The type of _useReducer_」参照)。

```react:src/reducer.ts
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](https://github.com/typescript-cheatsheets/react#function-components)」参照)。引数があれば、やはりジェネリックで型を与えられます。ただし、これらの型は[TypeScriptの基本テンプレートから除かれます](https://github.com/facebook/create-react-app/pull/8177)ので、これからは使うことをお勧めしません。

型とは別に、コールバック関数は[useCallback](https://ja.reactjs.org/docs/hooks-reference.html#usecallback)でメモ化しておきましょう(「[Create React App 入門 09: useCallbackフックで無駄な処理を省く](http://www.fumiononaka.com/Business/html5/FN2102003.html)」参照)。

```react:src/Counter.tsx
// 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)](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)で定めました。ですから、型チェックはもちろん働きます。けれど、エディタのコード補完が利きません。そこで、変数に入れることにしましょう。ここでは、つぎのように`Symbol()`を用いました。

```react:src/reducer.ts
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`さえすればコード補完が利くのです。これで、タイプミスも減らせるでしょう。

```react:src/Counter.tsx
// 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に公開しました。それぞれのモジュールのコードや、具体的な動きについてはこちらをご覧ください。

#### サンプル001■React + TypeScript: Using useReducer
[![2009001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ff53c6a1-7872-f96d-a3cd-560988b1a5c9.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-typescript-using-usereducer-fkcsx)