サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

React Hooks: クラスのコンポーネントをuseState()で関数に書き替える


これまでのReactのコンポーネントでは、状態(state)をもたせたいときはクラス(class)で定めました。けれど、React 16.8からは、関数コンポーネントでも、状態が保てます。そのための機能がフック(Hook)です(「フックの導入」参照)。本稿では、もっとも基本となるステートフックuseState()により、状態が備わったクラスコンポーネントを関数コンポーネントに書き替えてみます。

01 クラスを関数に書き替える

採り上げるのは「Create React App 入門 02: クリックしたマス目にXをつける」でつくった作例です。シリーズのこの回では、モジュールsrc/components/App.jsのコンポーネントに状態をもたせたいがため、関数からクラスに書き替えました。本稿のお題は、ステートフックuseState()を用いて、再び関数コンポーネントに戻すことです。

まずは、状態(state)は一旦置いて、コンポーネント(App)のクラスを関数に書き替えます。コンポーネントに備えるメソッドも含めて、アロー関数式=>で書くことにしましょう。メソッドは関数コンポーネント内のローカル変数(const)に定めましたので、this参照は添えません。このあとステートフックで加える状態についても同じです。

src/components/App.js

// class App extends React.Component {
const App = () => {
	/* constructor(props) {
		super(props);
		this.state = {
			squares: Array(9).fill(null)
		};
		this.handleClick = this.handleClick.bind(this);
	} */
	// handleClick(i) {
	const handleClick = (i) => {
		/* const squares = [...this.state.squares];
		squares[i] = 'X';
		this.setState({squares: squares}); */
	};
	// render() {
	return (
		<div className="game">
			<Board
				onClick={(i) => handleClick(i)}
			/>
			{/* squares={this.state.squares}
			onClick={(i) => this.handleClick(i)} */}
		</div>
	);
	// }
}

02 ステートフックuseState()を使う

ステートフックuseState()を使うと、関数コンポーネントに状態を備えることができます。フック(Hook)というのは、Reactの機能をつなぐ(hook into)関数です。useState()は、関数コンポーネントにReactの状態(state)をつなぎ止めます。戻り値は、ふたつの要素を収めた配列です。第1要素がstate変数で状態を保ち、第2要素の関数によりその値を書き替えます。配列の分割代入を使って変数(const)に取り出すのが便利でしょう。useState()の引数には、状態の初期値を渡してください。なお、フックはReactの関数内からしか呼び出せません。


import React, { useState } from 'react';

function 関数コンポーネント() {
	const [state変数, set関数] = useState(初期値);

}

モジュールsrc/components/App.jsのクラスコンポーネント(App)にもたせたstateおよびsetState()の呼び出しは、useState()関数から受け取ったstate変数(squares)および設定関数(setSquares())によりつぎのように書き替えてください。ともにローカル変数(const)に宣言しましたので、this参照は添えません。

src/components/App.js

// import React from 'react';
import React, { useState } from 'react';

const App = () => {
	/* constructor(props) {
		super(props);
		this.state = {
			squares: Array(9).fill(null)
		};
		this.handleClick = this.handleClick.bind(this);
	} */
	const [squares, setSquares] = useState(Array(9).fill(null));
	const handleClick = (i) => {
		// const squares = [...this.state.squares];
		const _squares = [...squares];
		_squares[i] = 'X';
		// this.setState({squares: squares});
		setSquares(_squares);
	};
	return (
		<div className="game">
			<Board
				squares={squares}

			/>
				{/* squares={this.state.squares} */}
		</div>
	);
}

これで、前出「Create React App 入門 02: クリックしたマス目にXをつける」の作例と同じく、クリックしたマス目にX印がつきます(図001)。クラスコンポーネントが、ステートフックuseState()を用いて関数コンポーネントに書き替えられたということです(サンプル001)。useState()関数の構文は、以下の表001にまとめておきました。詳しくは、リンクしたReact公式サイトの情報をご参照ください。

図001■クリックしたマス目にX印がつく

図001

表001■useState()関数

useState()
引数

state変数に与える初期値。

  • はじめてレンダリングするときに、state変数に設定される。
  • 以降のレンダリング時には、直近のstate変数値を受け取る。
戻り値 state変数と設定関数を要素に収めた配列。
  1. 現在の状態をもつstate変数。
  2. state変数の値を引数値に改める設定関数

設定関数には、値でなく関数を渡すこともできる。その場合、引数は直近のstate変数値で、戻り値が新たなstate変数値となる(「関数型の更新」参照)。


(直近のstate変数値) => {
	// 処理
	return 新たなstate変数値
}

フックを呼び出すのはトップレベルのみ

React公式サイトの「フックのルール」にはつぎのような注意が記されています。フックはレンダリングのたびに、必ずすべてが同じ順序で呼び出されなければならないのです(「最近Reactを始めた人向けのReact Hooks入門」の「Hooksを使う上で絶対に守ること」参照)。

フックをループや条件分岐、あるいはネストされた関数内で呼び出してはいけません。代わりに、あなたの React の関数のトップレベルでのみ呼び出してください。これを守ることで、コンポーネントがレンダーされる際に毎回同じ順番で呼び出されるということが保証されます。

サンプル001■React Hooks: Tic Tac Toe 01

03 ○×ゲームのアプリケーションを関数コンポーネントに書き替える

「Create React App 入門」シリーズは、「Create React App 入門 05: 無駄な処理を省く」でReact公式サイトの「チュートリアル:React の導入」と同じ動きの作例を、コンポーネント分けしてつくりました。この中のクラスで定めたルートモジュール(src/components/App.js)を、関数コンポーネントに書き替えてみましょう。考え方はすでに解説したとおりです。コードのどこをどう直すのか、つぎに掲げます。

src/components/App.js

// import React from 'react';
import React, { useState } from 'react';

// class App extends React.Component {
function App() {
	/* constructor(props) {
		super(props);
		this.state = {
			history: [
				{squares: new Array(9)}
			],
			stepNumber: 0,
			xIsNext: true,
			finished: false
		};
		this.handleClick = this.handleClick.bind(this);
	} */
	const [history, setHistory] = useState([
		{squares: new Array(9)}
	]);
	const [stepNumber, setStepNumber] = useState(0);
	const [xIsNext, setXIsNext] = useState(true);
	const [finished, setFinished] = useState(false);
	// handleClick(i) {
	const handleClick = (i) => {
		// if (this.state.finished) { return; }
		if (finished) { return; }
		// if (this.state.stepNumber >= 9) {
		if (stepNumber >= 9) {
			// this.setState({finished: true});
			setFinished(true);
			return;
		}
		// const history = this.state.history.slice(0, this.state.stepNumber + 1);
		const _history = history.slice(0, stepNumber + 1);
		const squares = [..._history[_history.length - 1].squares];
		console.log('history:', _history.length, stepNumber);
		if (squares[i]) { return; }
		const winner = calculateWinner(squares);
		if (winner) {
			// this.setState({finished: true});
			setFinished(true);
			return;
		}
		// squares[i] = this.state.xIsNext ? 'X' : 'O';
		squares[i] = xIsNext ? 'X' : 'O';
		/* this.setState({
			history: [...history, {squares}],
			stepNumber: history.length,
			xIsNext: !this.state.xIsNext
		}); */
		setHistory([..._history, {squares}]);
		setStepNumber(_history.length);
		setXIsNext(!xIsNext);
	};
	// jumpTo(step) {
	const jumpTo = (step) => {
		/* this.setState({
			stepNumber: step,
			xIsNext: (step % 2) === 0,
			finished: false
		}); */
		setStepNumber(step);
		setXIsNext((step % 2) === 0);
		setFinished(false);
	}
	// render() {
	// const history = [...this.state.history];
	const _history = [...history];
	// const squares = [...history[this.state.stepNumber].squares];
	const squares = [..._history[stepNumber].squares];
	const winner = calculateWinner(squares);
	const status = (winner) ?
		'Winner: ' + winner :
		// 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
		'Next player: ' + (xIsNext ? 'X' : 'O');
	const moves = _history.map((step, move) => {

		return (
			<li key={move}>
				{/* <button onClick={() => this.jumpTo(move)}>{desc}</button> */}
				<button onClick={() => jumpTo(move)}>{desc}</button>
			</li>
		);
	});
	return (
		<div className="game">
			<Board

				finished={finished}
				onClick={(i) => handleClick(i)}
			/>
				{/* finished={this.state.finished}
				onClick={(i) => this.handleClick(i)} */}

		</div>
	);
	// }
}

クラスコンポーネントのstateは、ひとつのオブジェクトでした。けれども、state変数は単純な値でよく、いくつでも宣言できます。まとめたい値があったらオブジェクトにして、それぞれをプロパティとして定めればよいのです。書き直したアプリケーションを、CodeSandboxにサンプル002として公開しました。

サンプル002■React Hooks: Tic Tac Toe 02


作成者: 野中文雄
作成日: 2020年04月13日


Copyright © 2001-2020 Fumio Nonaka.  All rights reserved.