15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-17

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)に渡すふたつの引数(stateaction)に型づけが要ります。型エイリアス(type aliases)で定めました。

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フックを使いました。TypeScriptの型推論が働きますので、型づけしなくても動きます。

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)。

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

型づけを加える

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

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」参照)。引数があれば、やはりジェネリックで型を与えられます。ただし、これらの型はTypeScriptの基本テンプレートから除かれますので、これからは使うことをお勧めしません。

型とは別に、コールバック関数はuseCallbackでメモ化しておきましょう(「Create React App 入門 09: useCallbackフックで無駄な処理を省く」参照)。

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)で定めました。ですから、型チェックはもちろん働きます。けれど、エディタのコード補完が利きません。そこで、変数に入れることにしましょう。ここでは、つぎのようにSymbol()を用いました。

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:

	}
};

型を定めた変数(decrementTypeincrementType)はexportしましたので、他のモジュールもimportさえすればコード補完が利くのです。これで、タイプミスも減らせるでしょう。

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
>> CodeSandboxへ

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?