1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JSフレームワークReactでデータ処理、まとめ版(フック学習重点強化)

Last updated at Posted at 2023-04-03

更新情報:演習6を根本から書き直し。演習7にuseTransitionフック、useDeferredValueフックの説明を追加など。

前記事の続編をそれぞれのフレームワーク毎に分類し、時流の変化を追いやすくしたものです。

今回は、前回で補完できなかった、コンポーネント周りのもっと突っ込んだ操作を含めた解説ができたらと思います。また、前回まではローカルサーバ上で操作していましたが、実践的な力を身に付けていくにはやはりサーバを用いた環境で制御していくべきなので、ReactはReact Nativeで制御していきます。

Reactも18でかなり大掛かりな仕様変更がありましたが、一旦はReact17基準で書いています(順々に、React19標準に書き換え中です)。React17最大の変更点としては、各種コンポーネントが内部関数化したことでreturn処理は原則1回のみになったことです。

つまり変数はオブジェクトリテラル内で処理し、リテラル内のJSXに対しても丸括弧(公式でもparenthesis、つまりは丸括弧としか定義していない)、記号としては()で返せるようになっています。これにより、JSXに対してreturnを使って逐一単一タグにして返さないといけない制約から解放されることになり、分岐やループが大幅に書きやすく、かつ読み取りやすくなりました。

またReact19になってからは、オブジェクトではなく各コンポーネントが内部関数化しています。微妙な変更点ではあるのですが、これにより一段とコンポーネント内部と外部の区別が容易になりました。

※ReactNativeはReactより関数コンポーネントへの採用が遅れたため、それ以前の記法(クラスコンポーネント)が主流となっていた時期もありましたが、2025年現在、クラスコンポーネントはサポート外の記法となっていますし、却って混乱を招くので全く覚える必要ありません。

※今回学習する内容は

  • 5章 コンポーネント制御(電卓)
  • 6章 ルーティング制御(買い物かご)
  • 7章 スタイル制御(写真検索システム)
  • 8章 TypeScript(Todoアプリ)

となります。

※昨今はReactでもTypeScriptでの記述が主流になってきていますが、8章まではそこに触れていません(基本を押さえないままTypeScriptに入ろうとすると余計にこんがらがってしまうと思うので)。演習8でTypeScriptについて触れています。

また、React学習者の壁となっているのが各種フックの使い分けだと思われるので、この記事は、とりわけフックの解説に重点を置いています。useContextフック、useCallbackフック、useReducerフックを演習6で、useMemoフック、useTransitionフック、useDeferredValueフックを演習7で、useRefフックを演習7と演習8で紹介しています。

●フックの存在意義

その前に、なぜReactはフックというものが大事なのかということです。それはReactにはあくまでJavaScriptが基本線という理念があるからで、他のJSフレームワークのようにあまり特殊な処理用メソッドやコントロールフローを用意していません。

ですが、JavaScriptだけでは対応困難なデータ処理も多数あります。フックはそんな対応困難なデータ処理を補助するための機能です。

そしてこのフックの基本が、変数を同期対象にする(state化する)ためのuseStateフックと、またそのstate化された変数に対し、連動的なエフェクト処理機能(かつて公式が副作用と誤訳していた部分)であるuseEffectフックです。

そしてReactは、実はパフォーマンスを度外視してしまえばuseStateフックとuseEffectフックだけで大体のシステムを作れてしまったりします(その証拠に、関数コンポーネントリリース初期は上記の2フックしか用意されていなかった)。なので、仕組みに慣れないうちはこの2つのフックだけで作ってみるといいです。

そのうち、あれもしたいこれもしたい、ここの処理が無駄に感じる、ここをもっと効率化したい…といった願望が生まれてくるはずです。そんな要望で生まれたのが、それ以外のパフォーマンス最適化フックや状態管理です。

つまり、Reactで大事な心がけは適材適所でフックを使うこと、逆に使用意義のない場面でフックを使うことは逆効果ということです。

また、バージョンが変わるたびに同じフックの役割や仕様が勝手に変更される(これはMeta社の悪い癖として、世界中から指摘されている)ことにも注意を払いましょう。随時公式ドキュメントに目を通しておくことが大事です。

import構文の共通ルール

その前に、JavaScriptのimport構文のルールを把握しておく必要があるはずです。

  • A:import Fuga from './Hoge'
  • B:import {Fuga} from './Hoge'

この2つの違いを把握しておかないと後々エラーを多発することになります。AはHogeという外部ファイルをFugaという名称で定義するという意味です(敢えて別名にする必要はないので、普通はインポートファイル名と同一です)。対してBはHogeという外部ファイルの中から定義されたFugaというオブジェクトを利用するという意味です。なので、Reactの例でいえば

import React,{useState,useEffect,useContext} from 'React'

これはReactという外部ファイルをReactという名称で利用する、かつuseState、useEffect、useContextというオブジェクトを利用するという命令になります。ちなみにReact19からはReactという外部ファイルをそのままインポートする記述は無用です。

演習5 コンポーネント制御(電卓)

ここではReactに対し、それぞれ子コンポーネントを用いて、親コンポーネントで簡易なシステムを構築していきたいと思います。そして、ここでは簡易な電卓を作成していきます。それにはプッシュキーとそれを押下したタイミングでの制御が必要となりますが、その際にプッシュキーの部品を子コンポーネント化することで、効率よくシステムを構築することができます。

その前に、なぜ親子のコンポーネントに分割、階層化するかですが、結論からいえば冗長な記述を回避するためです。

●アプリケーションのインストール

Reactのアプリケーションインストールですが、Viteに対応しているので、これでサクッと作ってしまいましょう。

# npm init vite@latest

これでReactを選択、ついでにTypeScriptを制御しておきましょう(Reactのコンポーネントファイルはtsxとなりますが、使用しなければいいだけです)。

●コンポーネントの基本構造

Reactのコンポーネントの呼び出しは以下のようになっており、main.tsxが基本となります。ファイルのインポートとコンポーネントタグの記述を行います。

main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx' //アプリケーションを制御するコンポーネントを呼び出す

//rootに紐づいている
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

今回は演習目的なので、一旦は以下のように<StrictMode>を外しておいて下さい(このままだとuseEffectなど状態制御関係のフックが二重処理されるので)。そのままにしておくとuseRefの知識必須となります。

jsx
//ストリクトモードを使用しない
createRoot(document.getElementById('root')!).render(
    <App />
)

このアプリケーションコンポーネントはindex.htmlに紐づいています。

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div><!-- ここに紐づく -->
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

●親子のコンポーネントを同一ファイルに記述する場合の注意

Reactは親子関係にあるコンポーネントを同一ファイルで処理することも多いですが、関数コンポーネントで親子のコンポーネントを制御する場合は、外部に出力する必要がある親コンポーネントだけexport default Parentとしてください。子コンポーネントまで下手にexport default Childとしてしまうと、制御エラーとなります。

※この部分も関数化のお陰で明瞭になりました。

function Child(){
  //子コンポーネントは外部に出力しないので、そのまま記述
}

function Parent(){
  //親コンポーネントは外部に出力するので、後で関数だけを記述する
}

export deafault Parent //親コンポーネントを出力する

●Reactでコンポーネント制御する

Reactのコンポーネント制御は他のJSフレームワークより流れが解りやすいです。

ただ、Reactの場合の注意点として、子コンポーネントから親コンポーネントへの値のやりとりするemitメソッドに当たるものは存在しないという特徴があります。ですが、イベントを含めたコールバック関数を伝播させることができるので、恰も親コンポーネントがイベントを受け取ったように振る舞わせることで、値の同期が可能となります。

●親コンポーネントから子コンポーネントへの値の受け渡し

子コンポーネントに値を受け渡すには定義関数の引数にpropsと記述するだけで、子コンポーネントでpropsオブジェクトから値を取得できるようになります。なおpropsとはpropatiesの略で、属性、所有のことです。つまりは親コンポーネントが所有している変数という意味合いで捉えれば、子コンポーネントはその親コンポーネントが持っている変数を参照しているだけだとわかるでしょう。

●Reactでの親コンポーネントの記述

では、親子コンポーネントの働きを解説するため、具体的に電卓を作っていきます。なお、ファイル名は以下のようになっています。

  • 親コンポーネント Calc.js
  • 子コンポーネント Setkeys.js //プッシュキー

●親コンポーネントの制御

親コンポーネントは以下のようになっています。

Calc.js
import React, { useState } from "react";
import Setkey from "./Setkey";

function Calc(){
	const[ d_sum, setSum ] = useState(0)
	const[ d_str, setStr ] = useState("")
	
	const state = {
		vals:[
			[['7','7'],['8','8'],['9','9'],['div','÷']],
			[['4','4'],['5','5'],['6','6'],['mul','×']],
			[['1','1'],['2','2'],['3','3'],['sub','-']],
			[['0','0'],['eq','='],['c','C'],['add','+']],
		],
    lnum: null, //被序数
    cnum: 0, //序数
    sum: 0, //合計
    sign: "", //記号
    str:"", //文字列
	}
  //子コンポーネントからの値を受け取るためのメソッド(後述)
  const onReceive = (str,sum)=>{
    setStr(str)
    setSum(sum)
  }
	return (
		<>
            {/*以下が子コンポーネントへのリンクタグ*/}
			<Setkey state={state} onReceiveState={(c,s)=>{onReceive(c,s)}}/>
			<div>
				<p>打ち込んだ文字:{d_str}</p>
				<p>合計:{d_sum}</p>
			</div>
		</>
	)
}

export default Calc

このような記述となります。

●子コンポーネントを紐づける

親コンポーネントから子コンポーネントを紐づけるにはJSX内にコンポーネントを記述することと、コンポーネントファイルを定義することで親子関係のコンポーネントが構成されます。

Calc.js
import { useState } from "react";
import Setkeys from "./Setkeys"; //子コンポーネントファイルの呼出

function Calc(){
   //中略
	return (
			<>
                {/*子コンポーネント*/}
				<Setkey state={state} onReceiveState={(c,s)=>{onReceive(c,s)}}/>
				<div>
					<p>打ち込んだ文字:{d_str}</p>
					<p>合計:{d_sum}</p>
				</div>
			</>
	)
}

export default Calc

●子コンポーネントの制御

子コンポーネントは以下のようになっています。ここでは電卓のプッシュキー表示とそのキーごとのイベントを制御しています。

setkeys.js
import {useState,useEffect} from "react";
function Setkey (props){
  const { state,onReceiveState } = props
  const [d_vals,setVals] = useState([])
  //展開直後の変数処理
  useEffect(()=>{
    const vals = {
      lnum: state.lnum,
      cnum: state.cnum,
      sum: state.sum,
      sign: state.sign,
      str: state.str,
    }
    setVals(vals)
  },[])
    //プッシュキーのイベントメソッド
	const getChar = (chr,strtmp)=>{
		let lnum = d_vals.lnum //被序数
		let cnum = d_vals.cnum //序数
		let sum = d_vals.sum //合計
		let sign = d_vals.sign
		let str = d_vals.str + strtmp

		if(chr.match(/[0-9]/g)!== null){
			let num = parseInt(chr)
			cnum = cnum * 10 + num //数値が打ち込まれるごとに桁をずらしていく
		}else if(chr.match(/(c|eq)/g) == null){
			if(lnum != null){
				lnum = calc(sign,lnum,cnum)
			}else{
				if(chr == "sub"){
					lnum = 0
				}
				lnum = cnum
			}
			sign = chr
			cnum = 0
		}else if( chr == "eq"){
			lnum = calc(sign,lnum,cnum)
			sum = lnum
		}else{
			lnum = null
			cnum = 0
			sum = 0
			str = ''
		}
        d_vals.lnum = lnum
        d_vals.cnum = cnum
        d_vals.sum = sum
        d_vals.sign = sign
        d_vals.str = str
        setVals(d_vals) //変数の同期用
        onReceiveState(str,sum) //親コンポーネントに変数を転送
	}
		
	//計算処理
	const calc = (mode,lnum,cnum)=>{
		switch(mode){
			case "add": lnum = cnum + lnum
			break;
			case "sub": lnum = lnum - cnum
			break;
			case "mul": lnum = lnum * cnum
			break;
			case "div": lnum = lnum / cnum
			break;
		}
		return lnum
	}
  return (
		<>
			{state.vals.map((val,key)=>{
					return (
						<div key={key}>
						{
							val.map((v,i)=>{
								return(
								<React.Fragment key={i}>
								<button className="square" onClick={()=>{getChar(v[0],v[1])}}>
									{v[1]}
								</button>
								</React.Fragment>
								)
							})
						}
						</div>
					)
				})
			}
		</>
  )
}
export default Setkey

●親コンポーネントから子コンポーネントへの値の受け渡し

親コンポーネントから子コンポーネントへ値を受け渡すには以下のように記述し、右側のオブジェクトリテラルに転送したい変数を記述し、左側のコールバック関数に格納します。

<子コンポーネント コールバック関数={子コンポーネントに転送したい変数} />

具体的には以下の部分になります。

	const states = {
		vals:[
			[['7','7'],['8','8'],['9','9'],['div','÷']],
			[['4','4'],['5','5'],['6','6'],['mul','×']],
			[['1','1'],['2','2'],['3','3'],['sub','-']],
			[['0','0'],['eq','='],['c','C'],['add','+']],
		],
        lnum: null, //被序数
        cnum: 0, //序数
        sum: 0, //合計
        sign: "", //記号
        str:"", //文字列
	}
	return (
			<>
                {/*オブジェクトリテラル内に親コンポーネント上に用意された変数states*/}
				<Setkey state={states}  />
		    </>
	)

●親コンポーネントの変数を子コンポーネントで受け取る

この変数は子コンポーネントで以下のように受け取ります。propsプロパティをコンポーネントの引数に代入し、それを以下のように展開します。展開した値をuseEffectフックで処理し、useStateフックで展開すれば、valsにプッシュキーを展開することができます。

SetKey.js
function Setkey(props){
	const { state,onReceiveState } = props //親コンポーネントからの値
  const [d_vals,setVals] = useState([]) //子コンポーネント上での変数設定
 //親コンポーネントから受け取った値を処理する
  useEffect(()=>{
    const vals = {
      lnum: state.lnum,
      cnum: state.cnum,
      sum: state.sum,
      sign: state.sign,
      str: state.str,
    }
    setVals(vals)
  },[])
 //中略
  return (
		<>
			{state.vals.map((val,key)=>{
					 (
						<div key={key}>
						{
							val.map((v,i)=>{
								(
								<button className="square" onClick={()=>{getChar(v[0],v[1])}}>
									{v[1]}
								</button>
								)
							})
						}
						</div>
					)
				})
			}
		</>
  )
}

export default Setkey

●子コンポーネントから親コンポーネントにデータを受け渡す

子コンポーネントから親コンポーネントにデータを受け渡す場合は、子コンポーネントのメソッドの引数を用意しておき、親コンポーネントに用意されたコールバック関数を実行する際に、インラインメソッドの引数から値を取り出すことができます。

具体的にはこの親コンポーネントにある、変数fugaが子コンポーネントに用意されたコールバック関数getCharStateの変数hogeになります。

  • 子 : getHogeState(hoge) //コールバック関数に引数をセット
  • 親 : getHogestate = { (fuga)=>getHoge(fuga)} //親コンポーネントのコールバック関数に用意されたインライン関数の引数から変数hogeを取り出し、それをメソッドで実行。

この引数をそのまま、メソッドに代入することもできます。

<Setkey onReceiveState=>{(c,s)=>{onReceive(c,s)}} />

それが変数cと変数sの部分です。

Calc.js
  //子コンポーネントから受け取った値を親コンポーネントで展開
  const onReceive = (str,sum)=>{
    setStr(str)
    setSum(sum)
  }
  
	return (
			<>
				<Setkey state={states} onReceiveState={(c,s)=>{onReceive(c,s)}}/>
				<div>
					<p>打ち込んだ文字:{d_str}</p>
					<p>合計:{d_sum}</p>
				</div>
			</>
	)
}
Setkey.js
  return (
		<>
			{state.vals.map((val,key)=>{
					(
						<div key={key}>
						{
							val.map((v,i)=>{
								return(
								<React.Fragment key={i}>
								<button className="square" onClick={()=>{getChar(v[0],v[1])}}>
									{v[1]}
								</button>
								</React.Fragment>
								)
							})
						}
						</div>
					)
				})
			}
		</>
  )
}

●useStateフックを使用する際の注意

useStateフックを専門的に説明すると、state変数を追加させるためのフックです。このstate変数というものは、React固有の機能としてメモリに記憶しておく変数で、値の再レンダリング(更新)にはsetHogeというセッタ関数を用いてレンダリングを実行し、同期処理を実現します。この一連のデータ同期処理をReactではバインドと呼んでいるようです。

したがって、useStateフックでバインドしているstate変数はあくまで参照しかできない上に、関数コンポーネントは自由自在にメソッド内で変数を扱える(クラスコンポーネントのように、逐一外部から呼び出す必要がない)ため、同一の変数名だとread-onlyエラーに引っかかることになります。そのため、state変数はとりあえずd_hogeとして、メソッド内の差分を格納した変数はhogeとしています。

演習6 ルーティング(買い物かご)

今までは親子コンポーネントの説明はしていますが、あくまで単一のページのみの制御でした。ですが、世の中のWEBページやアプリケーションは複数のページを自在に行き来できます。それを制御しているのがルーティングという機能です。

フレームワークにおけるルーティングとはフレームワークのように基本となるリンク元のコンポーネントがあって、パスの指定によってコンポーネントを切替できるというものです。もっと専門的な言葉を用いれば、SPA(SINGLE PAGE APPLICATION)というものに対し、URIを振り分けて各種コンポーネントファイルによって紐付けられたインターフェースをブラウザ上に表示させるというものです。

そしてReactではreact-router-domというライブラリが必須となります。フレームワークにリンクタグとリンク先を表示するタグが存在し、toというパス記述用のプロパティが存在しています(react-router-dom6はelementというプロパティを実装)。

また、データ転送用、データ受取用のライブラリが用意されており、それらを受け渡しと受け取り、そして更新のタイミングで実行していきます。

また演習6における買い物かごのデータ構造は以下のようになっています。

App.jsx
■src
   - ■css(デザイン制御用、表示は割愛)
   - ■cart
       - Products.jsx //商品一覧(ここに商品を入れるボタンがある)
       - Cart.jsx //買い物かご(ここに商品差し戻し、購入のボタンがある)
       - Detail.jsx //詳細ページ(商品一覧からリンクされている)
       - ShopContext.jsx //ストアオブジェクトの格納(ここのデータを処理していく)
       - GlobalState.jsx //トップコンポーネント(ここにルーティング部分も記述する)
       - reducers.js //リデューサファイル(共通処理用)

●Reactでルーティング制御する

reactでルーティング制御をするにはreact-router-domというライブラリが必須となります。事前にインストールしておきましょう。Reactの場合はこれをimportするだけでルーティングが使えるようになります。ただし注意点としてreact-router-dom6以降とそれ以前では全く記述が異なり、後方互換性も持っていないので注意が必要です。本記事はreact-router-dom6で記述していきます。

#npm install react-router-dom

●Reactのルーティングの仕組み

Reactのルーティングの仕組みは、それぞれのリンクタグに対し、リンクの表示先をRouteタグで表示するというものです。なので、表示先をシェアする関係にあるのでrouterというタグで表示を切換制御しています。

●Reactにおけるルーティング制御の記述

以下がReactにおけるルーティングの基本例となります。注意点は、react-router-domから呼び出したBrowserRouter内にナビ部分とビュー部分の双方を記述する必要があります。

●ナビ部分

ナビ部分はLinkタグに記述された部分で、toプロパティがページの遷移先となります。

※react-router-dom6からはaタグを用いると、ルーティングが制御されていないという警告が出ます。したがって、ページ遷移はできるもののパラメータの転送は全くできなくなるので使用しないようにしましょう。

Cart.jsx
import {BrowserRouter as Router, Route, Routes } from "react-router-dom"
	<header className="main-navigation">
    	<nav>
			<ul>
				<li><Link to="/Cart">Cart({cnt.cart}</Link></li>
				<li><Link to="/">Products({cnt.articles}</Link></li>
			</ul>
		</nav>
	</header>

●ブラウザ

SPAでリンクされたコンポーネントを表示させるブラウザはBrowserRouterタグに記述された領域(今回はRouterタグと簡略化して記述)であり、Routeタグに直接ルーティング情報を記述していく形となります。ここに、今回遷移させたい3つのコンポーネントを記述する必要があります。そのRouteタグでは、pathプロパティに遷移先のURI、elementプロパティにコンポーネントタグそのものを記述する形となります。

Cart.jsx
import {BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Products from "./Products";
import Detail from "./Detail";
<Router future={{ v7_relativeSplatPath: false }}>
	<header className="main-navigation">
    	<nav>
			<ul>
				<li><Link to="/Cart">Cart({cnt.cart}</Link></li>
				<li><Link to="/">Products({cnt.articles}</Link></li>
			</ul>
		</nav>
	</header>
   <Routes>
       <Route path="/" element={<Products />} />
	   <Route path="/cart" element={<Cart />} />
   </Routes>
</Router>

これでコンポーネント同士のリンク準備はできたのですが、データの受け渡しが必要になります。その手段として、次に解説するuseContextフックを活用していきます。

●データをやりとりする

Reactでデータをやりとりするのは、そこまで難解さはありません。なぜならコンポーネント間で自由にやりとりができるuseContextフックが兄弟コンポーネントであっても使えるからです。したがって、先程のMainNavigation.jsの先にGlobalState.jsという子コンポーネントを作成しておいて、そこに受け渡し用の変数を格納しておけば、あとの変数のやりとりは全部useContextフックがデータを受け渡ししてくれるようになります。

兄弟コンポーネントとは親子とは違い、階層化されていないコンポーネントのことです。また、アメリカでのbrotherとは、絆が深いたとえとして用いられるsisterと違い、それと対称的なイメージが強い(つまり仲間なんだけど、そこまで密接な関係ではない)表現です。

●useContextフック

今回、中心的な活躍を遂げるフックがuseContextフックで、これはコンテクスト化されたデータオブジェクトを利用するためのフックです。
そして、データオブジェクトをコンテクスト化させるcreateContextメソッドを併せて使用します。

簡潔に言うとuseContextフックは親から孫要素や兄弟要素など、直接的なつながりを持たないコンポーネントにデータを受け渡しさせるために実装されました。従来のpropsプロパティからだと、たとえば親から孫へ要素を受け渡したい場合、必ず親→子→孫という風に、データを逐一リレーさせないといけなく記述に冗長性がありましたが、useContextフックを用いることで、渡したいコンポーネントに対して、ダイレクトに受け渡すことが可能になります。

また、コンテクスト化されたデータの転送はJSX上のproviderタグに設定します。

JSXには

tsx
const Hoge = createContext()
<Hoge.Provider value={転送したいオブジェクト} >
{/*コンテクストを受け取りたいコンポーネントを記述*/}
</Hoge.Provider>

とすることで、任意のコンテクストオブジェクトを転送することができ、Providerタグでラッピングされたどのコンポーネントからでもデータを受け取ることができます。

そのデータを受取るコンポーネントがuseContextフックとなり、引数の中にコンテクストオブジェクトを代入します。

Parent.js
import {createContext} from "react";
import Brother from "Child.tsx"; 
export ParentContext = createContext() //任意のコンテクスト作成
const Parent = ()=>{
   const val = "hoge"
   return(
       <ParentContext.Provider value={val}>
          <Brother />  
       </Parentcontext.Provider>
   )
}
Brother.tsx
import React,{useContext} from "React"
import {ParentContext} = from './Parent'
const Child = ()=>{
}
export default Child = ()=>{
    const ctx = useContext(ParentContext); //コンテクストを受け取る
    return(
        <>
        <p>{ ctx.val }</p>
        </>
    )
}

コンテクストとは何なのか?

ここでコンテクストとは一体何なのかという疑問が浮かぶことですが、Reactではコンテクスト化されたデータオブジェクトを指すことが多いです。

コンテクスト化されたデータオブジェクトとは、同属性を持ったデータと考えるといいでしょう。

なお、英単語contextとは血族という意味であり、よくプログラムはparent、child、siblingsなど人で関連性を比喩します。それでcontextもすんなりとイメージできるでしょう(派生的に系統、属性、前後関係といった意味もあります)。ただ、無理に日本語訳する必要はないので、コンテクストというひとつの業界用語として覚えてしまった方がいいと思います。

●useContextフックで兄弟コンポーネントを制御

useContextフックの基本を説明したところで、本題に入っていきます。親子コンポーネントの応用で、SPAにおける兄弟コンポーネントに対してもコンテクスト化することでデータを受け渡すことが容易になります。なお、受け渡しの対象となるのはcreateContext()メソッドで作成したコンテクスト名となり、それぞれ値を継承させていますが、それぞれ系統の異なるコンポーネントに受け渡すことはできません。また、子孫名.Provider要素はReact.Fragmentの代用にもなるのでkeyプロパティを仕込むこともできます。

それをルーティング先で使用する場合はトップコンポーネントにデータを用意しておくことで、ルーティング先に対して、自在にデータを受け渡し、同期を取ることができます。

App.jsx
import GlobalState from './component6/GlobalState';
function App() {
  return (
	<GlobalState />
  )
}
export default App;

このGlobalState.jsxがアプリケーションのトップコンポーネントです。ShopConextがコンテクストオブジェクトとなります。

GlobalState.jsx
import React ,{ useState, useReducer,useContext,useEffect,useCallback } from "react"
import {BrowserRouter as Router, Routes,Route,Link } from "react-router-dom"
import ShopContext from "./ShopContext"; //変数情報
import { shopReducer } from "./reducers.js";
import Products from "./Products";
import Cart from "./Cart";

function GlobalState(props){
	const context = useContext(ShopContext) //このフックでcontextを送出する
	const [products,setProducts] = useState(context.products)
	const [money,setMoney] = useState(context.money)
	const [total,setTotal] = useState(context.total)
	const [cnt,setCnt] = useState({cart:0,articles:0})
	//useReducerフックを用いることで、cartを同時に扱うことができる
	const [store, dispatch] = useReducer(shopReducer, context)
	//買い物かごに追加
	const act = (action,dif)=>{
		dispatch({type:action,dif:dif});
	}
	//カウント処理
	const countItemNumber = useCallback((item)=>{
		return item.reduce((count,curItem)=>{
			return count + curItem.quantity //買い物かごの個数
		},0)
	},[store.cart])
	useEffect(()=>{
		const cnt_tmp = {...cnt} //分割代入で個数管理のオブジェクトを展開
		cnt_tmp.cart = countItemNumber(store.cart) //買い物かごの個数
		cnt_tmp.s = countItemNumber(store.articles) //買い物かごの個数
		setCnt(cnt_tmp)
	},[store])
	return(
		<ShopContext.Provider
			value={{
				products: store.products,
				cart: store.cart,
				articles: store.articles,
				money: store.money,
				total: store.total,
				act: act, //各種処理
			}}
		>
			<Router>
				<header className="main-navigation">
					<nav>
						<ul>
							<li><Link to="/Cart">Cart{cnt.cart}</Link></li>
							<li><Link to="/">Products{cnt.articles}</Link></li>
						</ul>
					</nav>
				</header>
				<Routes>
					<Route path="/" element={<Products />} />
					<Route path="/Cart" element={<Cart />} />
				</Routes>
			</Router>
		</ShopContext.Provider>
	)
}
export default GlobalState

●各コンポーネントの記述

ルーティング先の各コンポーネントの記述はこのようになっています。このルーティング先は全部、ShopContextというコンテクストによって紐付けられているので、このコンテクストからJSX及びデータの値を取得しています。

また、同系統コンテクストからイベントとデータを同期するには
onClick= {()=>context.methodHoge(data)}

とすれば、いけるようです(dataは任意のデータオブジェクト)。

Products.js
import React,{useContext} from "react";
import ShopContext from "./ShopContext";

function Products(){
	const ctx = useContext(ShopContext);
	return(
		<>
			<main className="products">
				<ul>
				{ctx.products.map(product=> (
					<li key={product.id}>
						<div>
							<strong>{product.title}</strong> - {product.price}円  【残り{product.stock}個						</div>
						<div>
						{product.stock > 0 && <button onClick={()=>ctx.act("add_cart",product)}>かごに入れる</button>}
						</div>
					</li>
				))}
				</ul>
			</main>
		</>
	)
}
export default Products

カート画面は以下のようになっています。

Cart.jsx
import { useContext } from "react";
import ShopContext from "./ShopContext";

function Cart(){
	const ctx = useContext(ShopContext); //受け取ったcontextオブジェクト
	return(
		<>
			<main className="cart" >
			{ctx.cart?.length <= 0 && 
				<p>No Item in the Cart!</p>
			}
			<ul>
			{ctx.cart.map((cartitem,idx)=>
				(
				  <li key={idx}>
					<div>
					  <strong>{ cartitem.title }</strong> - { cartitem.price }円
					  ({ cartitem.quantity })
					</div>
					<div>
					  <button onClick={()=>ctx.act("remove_product",cartitem.id)}>買い物かごから戻す(1個ずつ)</button>
					</div>
				  </li>
				)
			)}
			</ul>
			<h3>合計: {ctx.total}</h3>
			<h3>残高: {ctx.money}</h3>
			{ ctx.money - ctx.total >= 0 &&
				<button onClick={()=>ctx.act("buy_it",null)}>購入</button>
			}
			</main>
		</>
	)
}
export default Cart;

コンテクストファイル

ShopContext.jsxは以下のとおりですが、ここでストアオブジェクトをコンテクスト化しておくのがセオリーなので、コンテクストファイルと呼ぶ事例も多いです。

ShopContext.jsx
import {createContext } from "react"
const values = {
	products:[
		{ id: "p1", title: "花王 バブ ゆず", price: 60 ,stock: 10 },
		{ id: "p2", title: "バスクリン きき湯", price: 798 , stock: 3 },
		{ id: "p3", title: "アース 温素 琥珀の湯", price: 980, stock: 2 },
		{ id: "p4", title: "白元アース いい湯旅立ちボトル", price: 398, stock: 6	},
		{ id: "p5", title: "クラシエ 旅の宿", price: 598, stock: 7 }
	],
	cart: [],
	articles: [],
	money: 10000, 
	total: 0, //残額
} 
const ShopContext = createContext(values)
export default ShopContext;

●※ルーティング制御しながらuseContextフックでデータを受け渡す際の注意点

SPAでuseContextフックを活用する場合、注意しないといけない点があります。

  • 1:ProviderでラップするタグはちょうどBrowserRouterタグを覆うようにすること
    ルーティングの対象となるのはBrowserRouterタグ内なので、そこにuseContextタグを用いる場合は、そのタグ全体を覆うように記述しましょう。
App.js
const App = ()=>{
   return(
      <HogeContext.Provider value={fuga:fuga}>
            <BrowserRouter>
                {/* 中略 */}
            </BrowserRouter>
      </HogeContext.Provider>
   )
}

このルールを守らないと、データを受け渡ません。

また、ルーティング先で同期処理を行いたい場合は

  • useContextでuseStateのセッタ関数を受け渡す
    このようにルーティング送出元コンポーネントで設定していたuseStateフックのセッタ関数、すなわちsetHogeを受け渡すとデータの同期が可能です。
App.js
const App = ()=>{
   return(
      <HogeContext.Provider value={fuga:fuga,setFuga:setFuga}>
            <BrowserRouter>
                {/* 中略 */}
            </BrowserRouter>
      </HogeContext.Provider>
   )
}
Hoge.js
    ctx = useContext(Hoge.context)
    useEffect(()=>{
         const piyo = [...ctx.fuga] //useContextから継承したオブジェクト。スプレッド構文で受け取る
         ctx.setFuga(piyo) //更新対象に用いるメソッドごと受け取れば同期がとれる
    },[])

ただし、今回はuseReducerフックを使用するので、セッタ関数ごと受け渡す処理は行いません。

【特】useReducerフック

このシステムではuseReducerフックを使用しており、このフックをいかに使いこなせるかが、React初級者と中級者の境目とも言われています。そして、このuseReducerを使いこなせない学習者が多いときくので、徹底的に解説していきます。

useReducerフックは公式の説明において、「リデューサをコンポーネントに追加していくためのファイル」と説明していますが、何のことかさっぱりわからないでしょう。なので、リデューサについても公式説明を追っていきます。すると、すべてのstate更新ロジックを集約するための機能と説明されています。これで、useReducerフックは処理を集約化するためのフックということがわかるはずです。

つまり、この買い物かごシステムなどのように、処理するデータが複数(商品格納、買い物かご、所持金、合計金額など処理しなければいけない変数が色々ある)、またはコンポーネントや内容によって処理を分岐したい場合もあります。そんなときにuseReduerフックを使うと効果絶大で、Reactが数あるJSフレームワークの中でもとりわけステート管理に強いといわれる所以です。

※reduceとは累算を意味し、監視対象のデータ群(これをストアオブジェクトという)に対し、更新対象のデータ(差分データ)を分割代入していくことになります。

そして、このuseReducerフックのお陰で、もっとややこしかったReduxをほとんど使用しなくて済むようになりました。useReducerフックを実装した背景にはそういう理由もあるわけです。

useReducerの式

公式は以下の通りです。

vue
   const[store, dispatch中身はactionとdata] = useReducer(reducer,initial)

このようになっており、構造上はuseStateと似ているのですが、useStateとの違いは

  • アクションを格納できるプロパティを持っている(処理の分岐ができる)
  • 複数のデータを一度に処理できる
    前述したように、このような特長があります。

そしてstoreに代入するのは更新対象データとなるストアオブジェクト、initialに代入するのはshopReducerで処理するために用いるストアデータの初期値、すなわちコンテクストとなります。それからdispatchは和訳で配送のことであり、配送物には当然、宛先と荷物が必要なので、更新処理をするために必要なデータ(つまりは差分データ。プロパティは省略可)と、その処理分別のための宛先、つまりは処理イベントの種類を格納するtype(公式ではアクションという言葉を用いている)を準備します。またdispatchはリデューサファイルを呼び出すための関数で、名前は任意ですが、無難にdispatchとした方がいいでしょう。

残る変数reducerがこのフックの要となるリデューサを呼び出すメソッドを設定します。

簡潔にuseReducerフックの働きを分解するとこうなります。

[監視対象のストアオブジェクト,処理種別→{type:処理の分岐名,(payload):差分データ}] = useReducer(共通処理,初期値)

これはJSにおける分割代入のルールを踏襲しており、ストアデータに対し、差分を分割代入するという流れで覚えるといいでしょう。

実例を使った流れ

では、実例のプログラムを通して具体的に見ていきます。useReducerフックの戻り値にあるstoreには差分の更新対象となるストアオブジェクト、dispatchには記述された処理分類のtypeとそれぞれの差分データdifが格納されることになります。

まずは、このリデューサに紐づいたイベントを発生させます。商品一覧ファイル上にはコンテクスト化されたクリックイベント上のメソッド、actが紐づいています。また、そのactの引数にはadd_cartという分岐イベント名、productという具体的な差分ファイルが格納されています。

Products.tsx
		{product.stock > 0 && <button onClick={()=>ctx.act("add_cart",product)}>かごに入れる</button>}

トップコンポーネントにはその外部からのイベントに紐づいていたメソッドactの具体的処理が記述されており、その中にはdispatchというリデューサを利用するためのメソッドが設定されています。これはuseReducerフックが用意しているdispatchメソッドなので、リデューサファイルと直結させることができます。

※以前はトップコンポーネントで処理を分岐させることも多かったのですが、昨今ではイベントに直結したメソッドは、そのままリデューサへ中継するだけの処理が多いです。

GlobalState.js
import { shopReducer } from "./reducers.js"; //reducer関数を格納した処理用ファイル
function GlobalState(){
	const [store, dispatch] = useReducer(shopReducer, context )
 	//リデューサへの中継
	const act = (action,dif)=>{
		dispatch({type:action,dif:dif}); //リデューサファイルを呼び出す(処理の分岐actionと差分difを用意する)
	}
}

一見、データ更新に必要なストアデータの情報が不足しています。ですが、dispatchはリデューサファイル上の処理メソッドと直結しており、その際には、必ずuseReducerフックで設定したストアオブジェクトを第1引数として保持してくれています。

リデューサファイルの役割

リデューサファイル内の処理用メソッドは、(ここではshopReducer)初期設定上は1つだけです。でもそれではuseReducerフックの本領をまるで発揮できないので、ここに処理分岐用のメソッドを設定します。

また引数ですが、前述したように第一引数は必ずストアオブジェクトとなり、第二引数で、トップコンポーネント上に設定したdispatchメソッドで用意した任意の値を取得できます。

reducers.js
//useReducerに紐づいた処理振り分けの関数(paramにはアクションの種類typeと差分difを設定している)
export const shopReducer = (state,param) => {
	switch (param.type) {
	case "add_cart":
	  return addProductToCart(param.dif,state); //買い物かごに追加
	case "remove_product":
	  return removeProductFromCart(param.dif,state); //買い物かごから戻す
	case "buy_it":
	  return buyIt(state); //購入する
	default:
	  return state; //何もしない
	}
}

あとはリデューサファイル上に、設定した関数で具体的な処理を行います。

リデューサファイル

共通処理用のリデューサファイル、reducer.jsの全容はこうなっています。各種処理メソッドはVue演習に使ったロジックの使い回しで、最後の戻り値を返す部分だけ変更すれば、どんなフレームワークにも換装できたりします。

※各処理に対し戻り値を返すようにreturnを使っているのはuseReducerフックを使用しているためで、記述は必須です。実際に差分を用いて処理した更新後のストアデータを引数にして返すことで、戻り値を確定させることができます。

reducers.js
const addProductToCart = (product, state) => {
	let cartIndex = null
	const stat = state
	//買い物かごの調整
	const updatedCart = [...stat.cart];
	const updatedItemIndex = updatedCart.findIndex(
	item => item.id === product.id
	);
	if (updatedItemIndex < 0) {
	updatedCart.push({ ...product, quantity: 1,stock: 0 });
		cartIndex = updatedCart.length -1 //カートの最後尾
	} else {
	const updatedItem = { ...updatedCart[updatedItemIndex] }
	updatedItem.quantity++;
	updatedCart[updatedItemIndex] = updatedItem;
		cartIndex = updatedItemIndex //加算対象のインデックス
	}
	//商品在庫の調整
	const updatedProducts = [...stat.products] //商品情報
	const productid = updatedCart[cartIndex].id //在庫減算対象の商品
	const productIndex = updatedProducts.findIndex(
		p => productid === p.id
	)
	const tmpProduct = { ...updatedProducts[productIndex] }
	tmpProduct.stock-- //在庫の減算
	updatedProducts[productIndex] = tmpProduct
	//合計金額の調整
	const total = stat.total 
	const sum = getSummary(updatedCart,total)
	stat.cart = updatedCart
	stat.products = updatedProducts
	stat.total = sum
	state = {...state,stat}
	return state
};

//カートから商品の返却
const removeProductFromCart = (productId, state) => {
  const updatedCart = [...state.cart];
  const updatedItemIndex = updatedCart.findIndex(item => item.id === productId);
  const updatedItem = { ...updatedCart[updatedItemIndex] }
  updatedItem.quantity--
  if (updatedItem.quantity <= 0) {
    updatedCart.splice(updatedItemIndex, 1);
  } else {
    updatedCart[updatedItemIndex] = updatedItem;
  }
	//商品在庫の調整
	const updatedProducts = [...state.products] //商品情報
	const productIndex = updatedProducts.findIndex(
		p => p.id === productId
	)
	const tmpProduct = { ...updatedProducts[productIndex] }
	tmpProduct.stock++ //在庫の加算
	updatedProducts[productIndex] = tmpProduct
	let summary = getSummary(updatedCart,state.total)
	const stat = state
	stat.total = summary;
	stat.cart =  updatedCart;
	stat.products = updatedProducts;
	state = {...state,stat}
	return state
};

//購入手続
const buyIt = (state)=>{
	let cart = [...state.cart];
	let money = state.money
	let total = state.total
	let articles = state.articles; //所持品
	
	for( let val of cart){
		const articlesIndex = articles.findIndex(
			a => a.id === cart.id
		)
		if (articlesIndex < 0) {
			articles = [...state.cart];
		} else {
			const tmpArticles = { ...articles[articlesIndex] }
			tmpArticles.quantity++;
			articles[articlesIndex] = tmpArticles;
		}
	}
	let summary = getSummary(cart,total)
	let rest = money - summary
	cart.splice(0)
	summary = 0
	const stat = state;
	stat.total =  summary;
	stat.money = rest;
	stat.cart =  cart;
	stat.articles = articles;
	state = {...state,stat};
	return state
}
const getSummary = (cart,total)=>{
	const sum = cart.reduce((total,{price = 0,quantity})=> total + price * quantity,0)
	return sum
}

export const shopReducer = (state,ev) => {
	switch (ev.type) {
	case "add_cart":
	  return addProductToCart(ev.dif,state);
	case "remove_product":
	  return removeProductFromCart(ev.dif,state);
	case "buy_it":
	  return buyIt(state);
	default:
	  return state;
	}
};

これで具体的なデータの流れが把握できたのではないかと思われます。ですが、ここではまだ、別のフックを使用しています。それがusecallbackフックでこれも頻用されますが、原理はそこまで難しくありません。

●useCallbackフック

このシステムではuseCallbackフックを使用しています。useCallbackフックは、公式には「再レンダー間で関数定義をキャッシュできるフック」と説明しています。端的にいえば頻繁に繰り返し行う動作において処理そのものを記憶させておくフックで、基本はパフォーマンス改善のためのフックですが、関数定義という部分が着眼点です。つまり、処理されるデータではなく、そのデータを処理そのものをキャッシュ化するフックです。

ここではナビに表示される、買い物かごの商品個数を計算しています。なお、計算処理が必要なのは個数が変化したタイミングだけでいいので、以下のように記述することで、買い物かごの個数が変化した場合のみ合計個数の再計算処理を行うようにしています(lengthだと同一商品が複数になった場合、対応できない)。

※なお、useEffectフックだと逐一反応してしまうのでパフォーマンスが低下します。

	//カウント処理
	const countItemNumber = useCallback((item)=>{
		return item.reduce((count,curItem)=>{
			return count + curItem.quantity //買い物かごの個数
		},0)
	},[store.cart])
	useEffect(()=>{
		const cnt_tmp = {...cnt} //分割代入で個数管理のオブジェクトを展開
		cnt_tmp.cart = countItemNumber(store.cart) //買い物かごの個数
		cnt_tmp.s = countItemNumber(store.articles) //買い物かごの個数
		setCnt(cnt_tmp)
	},[store])

似たような働きをするフックでuseMemoフックもありますが、これはデータそのものをメモ化しておくフックであり、演習7で紹介します。

●詳細画面を作成する(パラメータのやりとり)

Reactでも詳細画面を作成する場合、パラメータのやりとりが必須になってきます。

Products.js
	<Link to={`/detail/${product.id}`}><strong>{product.title}</strong></Link>

また、グローバルステートのRouterタグ内は以下のように追記しておきます。同様に:idがパラメータ名となります。

js.GlobalState.js
import DetailPage from "./Detail" //コンポーネントを追記
const GlobalState = ()=>{
   /*略*/
   return(
   {/*略*/}
			<Router>
				<header class="main-navigation">
				<nav>
					<ul>
					<li><Link to="/Cart">Cart{cnt.cart}</Link></li>
					<li><Link to="/Products">Products{cnt.articles}</Link></li>
					</ul>
				</nav>
			</header>
				<Routes>
					<Route path="/" element={<Products />} />
					<Route path="/Cart" element={<Cart />} />
                    <Route path="/Detail" element={<Detail />}>
				</Routes>
			</Router>
   {/*略*/}
   )
}

●パラメータを取得する

パラメータを取得する場合はuseParamsを活用できます(このメソッドはreact-router-domからなので注意)。そして、このuseParamsはオブジェクトをそのまま抽出できるので便利です。ただしReactならではの注意点があり、JSXに展開する場合、useEffectだとDOM生成後に作動するものなので、未定義の変数エラーに悩まされることになります。なので、カスタムコンポーネント化して展開するのが一般的な方法ですが、以下のようにuseCallback関数を併用すれば、JSX生成前に変数を評価できます。

Detail.js
import {useState,useCallback,useEffect,useContext} from "react"
import ShopContext from "./ShopContext";
import { useParams } from "react-router-dom"

function Detail(){
	const [item,setItem] = useState({})
	const ctx = useContext(ShopContext)
	const { id } = useParams() //useParamsはオブジェクトをそのまま抽出できる
	//初期動作
	useEffect(()=>{
		getCallback(); //処理をコールバックする
	},[ctx])
    //コールバック関数を利用する
	const getCallback = useCallback(()=>{ 
		const sel = ctx.products.find((item)=> item.id == id) //一致するアイテム取得
		setItem(sel)
	},[id])
	return(
		<>
			<ul>
				<li>{item.title}</li>
			</ul>
		</>
	)
}
export default Detail

このようにuseEffectフックとuseCallbackフックを使用すれば、DOM生成前に処理を実行して結果を記憶できるので、きちんと変数を展開できたりします(useEffectフックだけだとDOM生成後しか機能しないので、d_itemが未定義変数エラーになります)。

●クエリを使ってデータをやりとりする

では、URLに埋め込まれたクエリパラメータ(get)を利用して、同じ操作をしてみます。react-router-dom6からはuseSearchParamsが使えるので、かなり楽になりました。

Products.js
<NavLink to={`/detail?id=${product.id}`}><strong>{product.title}</strong></NavLink>
GlobalState.js
<Route path="/detail" component={DetailPage} exact/>
Detail.js
import { useSearchParams } from "react-router-dom"
	const query = useSearchParams()
	const getCallback = useCallback(()=>{
		const selid = query.get('id')
		const item = context.products.find((item)=> item.id == selid)
		setItem(item)
	},[query])
}

●戻るボタンを実装する

ルーティング先から前ページに戻る場合は、react-router-dom6以降はuseNavigateを使用するようになっています。このメソッドは引数に遷移先のパスを明示的に記述できるので、非常に便利です(useHistoryは廃止)。

js
import {useNavigate } from 'react-router-dom'
/*中略*/
  const navigate = useNavigate()
  navigate('/') //ホームに戻る

演習7 スタイル制御(写真検索システム)

JSフレームワークの魅力はスタイル属性もリアルタイムに制御できることです。そこで、Vue、React、Angularで先程とは大きく書き直した写真検索システムにfont-awesomeのアイコンを使って気に入った画像を「いいね!」(ハートアイコンを赤に着色)できるようにしてみました。

なお、font-awesomeを使用する場合は、予めプロジェクトにインストールしておく必要があります。

●Reactでスタイルを制御する

では、Reactでも記述していきます。Reactの場合はVueよりは単純で、通常のJavascriptのように、明示的にオブジェクトリテラル内の変数に値を代入するだけで対応できます。

ですが、いろいろと対応しないといけない問題があります。ひとまずは、ReactでもFont-awesomeをインストールしておきましょう。

React-lesson7.js
import React,{ useState, useMemo } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHeart } from "@fortawesome/free-solid-svg-icons";
import CitiesJson from '../../json/city.json';
import StatesJson from '../../json/state.json';
import './css/style.css';

const World = ()=>{
	//定義
	const countries= [
		{id:1,code:"US",name:"United States"},
		{id:2,code:"JP",name:"Japan"},
		{id:3,code:"CN",name:"China"},
	]
	//const selecteditem = []
	const word = "" //プロパティの定義
	const active = "red"
	const cities = CitiesJson
	const states = StatesJson
	const act = ''
	const [r_list_states, setListStates] = useState([]) //連動プルダウンのエリアオプション
	const [r_sel_country,setSelCountry] = useState('') //選択した国
	const [r_sel_state,setSelState] = useState('') //選択したエリア
	const [r_word,setWord] = useState('') //選択した文字
	const [r_hit_cities_by_state,setHitCitiesByState] = useState([]) //エリアの検索結果
	const [r_hit_cities_by_word,setHitCitiesByWord] = useState([]) //文字の検索結果
	const [r_hit_cities,setHitCities] = useState([]) //複合検索結果

	//スタイル制御
	const colorSet = (id)=>{
		const hit_cities = [...r_hit_cities]
		let selecteditem = hit_cities.find((v)=>{ return v.id === id })
		let ar_tmp = []
		hit_cities.map((item,idx)=>{
			if(item.id === id){
				if(selecteditem.act === active){
					selecteditem.act = ''
				}else{
					selecteditem.act = active
				}
			}
		})
		setHitCities(hit_cities)
	}
	//国名を選択
	const selectCountry = (e)=>{
		const sel_country = e.target.value
		//エリアの絞り込み
		const opt_states = states.filter((v)=>{ return v.country === sel_country})
		setListStates(opt_states)
		setSelCountry(sel_country)
	}
	//エリアを選択
	const selectState = (e)=>{
		const sel_state = e.target.value //選択したエリア
		//エリアから都市絞り込み
		const hit_cities_by_state = cities.filter((v)=>{ return v.state === sel_state })
		setHitCitiesByState(hit_cities_by_state)
		setSelState(sel_state)
	}
	//フリーワード検索
	const searchWord = (e)=>{
		const word = e.target.value
		const hit_cities_by_word = cities.filter((v)=>{
			const item = v.name.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
			return item.includes(word) && word
		})
		setHitCitiesByWord(hit_cities_by_word)
		setWord(word)
	}
	//検索結果の同期
	useMemo(()=>{
			const len_state = r_hit_cities_by_state.length
			const len_word = r_hit_cities_by_word.length
			let hits = []
			if(len_state > 0 && len_word > 0 ){
				hits = require('lodash').intersection(r_hit_cities_by_state, r_hit_cities_by_word)
			}else if(len_state > 0){
				hits = r_hit_cities_by_state
			}else if(len_word > 0){
				hits = r_hit_cities_by_word
			}else{
				hits = []
			}
			setHitCities(hits)
	},[r_sel_state,r_word])

	//クリア
	const clear = ()=>{
		setSelCountry('')
		setSelState('')
		setWord('')
		setHitCitiesByState([])
		setHitCitiesByWord([])
	}
	//レンダリングを行う記述
	return (			
	  <>
			<label> 国の選択 </label>
			<select id="sel1" onChange={selectCountry} value={r_sel_country}>
				<option value="">-- 国名を選択 --</option>
				{countries.map((item)=>
						<option key={item.id.toString()} value={item.code}>{item.name}</option>
					)
				}
			</select>
			<label> エリアの選択</label>
			<select id="sel2" onChange={selectState} value={r_sel_state}>
				<option>-- エリアを選択 --</option>
				{
					r_list_states.map((item,key)=>
					(
							<option key={item.id} value={item.code}>
								{item.name}
							</opt>
					)
				}
			</select>
			<br/>
			<h4>検索文字を入力してください</h4>
			<input type="text" id="word" value={r_word} onChange={searchWord}/>
			<button type="button" onClick={ clear }>clear</button>
			<div>ヒット数{r_hit_cities.length}</div>
		  <ul className="ulDatas">
				{
					r_hit_cities.map((item,key)=>
						(
    					<li className="liData" key={item.id}>
	     					<label className="lbHit">
									{ item.name }
									<label onClick={()=>{colorSet(item.id)}}>
										<FontAwesomeIcon icon={faHeart} style={{color:item.act}} />
									</label>
								</label>
    						<br />
    	 					<img className="imageStyle" src={`../../img/${item.src}`} alt={ item.name } />
    					</li>
						)
					})		
				}
		  </ul>
		</>
	)
}

export default World

●Font-awesomeを利用する

Reactの場合はfasがどうもうまく使えないようなので、使用したいアイコンを一つ一つ明示しておく必要がありそうです。

js
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; //Font-awesomeの利用
import { faHeart } from "@fortawesome/free-solid-svg-icons"; //ハートアイコン
/*中略*/
const HitsCity = (pr)=>{
const {colorSetstate,item } = pr //prはpropsのこと
	return(
    <li className={styles.liData} key={item.id}>
	     <label className={styles.lbHit}>
					{ item.name }
					<label onClick={()=>{colorSetstate()}}>
<FontAwesomeIcon icon={faHeart} style={{color:item.act}} />
					</label>
			</label>
    		<br />
    	 <Images />
    </li>
	)
}

これでアイコンがリストごとに表示されます。同様に、クリックイベントはlabelタグに置く方が無難でしょう。また、クリックイベントは子コンポーネントから伝播させているので、イベント伝播用のコールバック関数(colorSetstate)を用意しておきます。また、コールバック関数の場合はuseContextフックが利用できないようなので、propsオブジェクトでデータを受け渡すようにしましょう。

また、スタイル制御の部分ですが、Vueだとクラス名を記述してそれを有効、無効で切り換えてスタイルを動的に制御していましたが、Reactの場合はスタイルタグをそのまま反映させることができます(逆にいえば、スタイルの有効、無効切り換えは面倒です)。

そして、この記述部分ですが、これはvueのような二重ブラケットではなく、オブジェクトリテラルの中に、スタイルを記述しているため、見かけ二重に見えているだけです。つまり

js
<FontAwesomeIcon icon={faHeart} style={
 {color:item.act}
}/>

と同じことです。そして、item.actにcolorプロパティの値を代入するだけで、動的制御が可能になります。ですが、もう一つ落とし穴があります。

●オブジェクトから値を制御する

第4章のデータの修正にもあった通り、関数コンポーネントでオブジェクトの値を修正するには、分割代入が必須で、これを実施しないとオブジェクトの値をリアクティブ制御できません。

また、データの更新ですが、このように分割代入で検索結果のオブジェクトを展開し、スタイル制御用のプロパティの値を代入してから差し替えるという方法を用いることで、任意のインデックスに紐付いたオブジェクトの値を差し替えることが可能になります。

js
	const active = "red" //スタイル色
	//スタイル制御
	//スタイル制御
	const colorSet = (id)=>{
		const hit_cities = [...r_hit_cities] 
		let selecteditem = hit_cities.find((v)=>{ return v.id === id })
		let ar_tmp = []
		hit_cities.map((item,idx)=>{
			if(item.id === id){
				if(selecteditem.act === active){
					selecteditem.act = ''
				}else{
					selecteditem.act = active
				}
			}
		})
		setHitCities(hit_cities) //同期をとる
	}

●useMemoフック

この写真検索システムにはuseMemoというフックを使用しています。このフックはuseEffectやuseCallbackと似ているのですが、どちらかというとVueの監視プロパティっぽい働きをする、時間やメモリを消費した大掛かりな処理結果を記憶しておき、処理を円滑に実行するためのフックです。

ここでは一定の処理によって更新され、常時表示しておきたい値(ここではr_hit_citiesという検索結果情報)に対し、対象となるデータに紐づけ処理を実行させるもので、これを実装することによって、何度、検索条件が再レンダリングされようが検索結果が変更された場合のみ論理積計算を実行することになるので、処理能率が大幅に改善(今回は画像ファイルも表示制御するので、それなりにメモリを消費する)されます。

js
import React,{ useState, useEffect, useMemo } from "react";
useMemo(()=>{
  //実行したい処理
},[監視したいデータ]}

※また、このように参照結果を返したいだけなら、戻り値を設定することもできます。

js
import { useState, useEffect, useMemo } from "react";
const r_hit_cities  = useMemo(()=>{
  //実行したい処理
  return hits //処理によって算出された値
},[監視したいデータ]}

※ちなみにこのシステムをuseEffectフックにしても動作しますが、逐一再レンダリングに反応してしまう(結果の変更有無にかかわらず)のでパフォーマンスは大きく落ちます。

ですが、次に挙げるフックを使用した場合、useMemoは使用できません。

【特】状態変化の制御フック

では、React18から導入された状態変化に関する制御用フックを活用して、データを検索している間、「検索中です」というメッセージを画面上に表示させてみようと思います。

useTransitionフック

React18から導入された状態変化を管轄する代表的なフックで、状態変化の自動制御を行います。transitionとは移り変わり、すなわち遷移のことです。

公式

const [isPending,startTransition ] = useTransition();

基本となる記述が上記の書き方で、isPendingには処理中の判定フラグ、startTransitionはその具体的処理を記述します。

具体的に、プログラムに組み込んでみます。

Lesson7.js
import React,{ useState, useEffect,useMemo,useTransition } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHeart } from "@fortawesome/free-solid-svg-icons";
import CitiesJson from './json/Cities.json';
//import StatesJson from './json/state.json';
//import './css/style.css';

function Lesson7 (){
	const [isPending,startTransition ] = useTransition(); //トランジションの設定

   /*中略*/
	//検索結果の同期
	useEffect(()=>{
		startTransition(()=>{
			const len_state = r_hit_cities_by_state.length
			const len_word = r_hit_cities_by_word.length
			let hits = [];
			if(len_state > 0 && len_word > 0 ){
				hits = require('lodash').intersection(r_hit_cities_by_state, r_hit_cities_by_word)
			}else if(len_state > 0){
				hits = r_hit_cities_by_state
			}else if(len_word > 0){
				hits = r_hit_cities_by_word
			}else{
				hits = []
			}
			setHitCities(hits)
		})
	},[r_sel_state,r_word,startTransition])
	
	const updateAction = async()=>{
	}

	//レンダリングを行う記述
	return (			
	  <>
			<label> 国の選択 </label>
			<select id="sel1" onChange={selectCountry} value={r_sel_country}>
				<option value="">-- 国名を選択 --</option>
				{countries.map((item)=>
						<option key={item.id.toString()} value={item.code}>{item.name}</option>
					)
				}
			</select>
			<label> エリアの選択</label>
			<select id="sel2" onChange={selectState} value={r_sel_state}>
				<option>-- エリアを選択 --</option>
				{
					r_list_states.map((item,key)=>{
						(
							<option key={item.id} value={item.code}>
								{item.name}
							</option>
						)
					})
				}
			</select>
			<br/>
			<h4>検索文字を入力してください</h4>
			<input type="text" id="word" value={r_word} onChange={searchWord}/>
			<button type="button" onClick={ clear }>clear</button>
			{isPending ? <p>検索中です</p>:(
			<>
			<div>ヒット数{r_hit_cities.length}</div>
		    <ul className="ulDatas" >
				{
					r_hit_cities.map((item,key)=>{
						return(
					<li className="liData" key={item.id}>
	 					<label className="lbHit">
									{ item.name }
									<label onClick={()=>{colorSet(item.id)}}>
										<FontAwesomeIcon icon={faHeart} style={{color:item.act}} />
									</label>
								</label>
						<br />
	 					<img className="imageStyle" src={`../../img/${item.src}`} alt={ item.name } />
					</li>
						)
					})		
				}
			</ul>
			</>)}
		</>
	)
}

export default Lesson7;

startTransitionメソッドで囲まれている間の処理中、isPendingがTrueを返し、その間に検索中というメッセージが表示され、処理が終了した段階で検索結果が再表示されます。一見難解に思えますが、何も難しく考える必要なく、要はメインプログラムの処理中に別の処理を見せている(公式で言及している優先順位の低い処理)だけで、それを自動で切替制御してくれるのがuseTransitionフックです。このフックが生まれた背景は待機時間中に利用者がページを離れる現象を回避するためだそうで、webプログラムやアプリケーションなどで用いると効果的です。

ちなみに、useTransitionの動作検証を行うには最低でも2000件ぐらいデータが必要です(数百件ぐらいだと一瞬で処理してしまうので、表示する間もありません)。今回はjsonファイルに2000件のデータ(検証用なので同一データを複製しただけ)を用いています。

※注意点として、useTransitionにおいて優先度の低い処理を終え、フラグがtrueになるタイミングは、あくまで処理結果を表示させる準備ができた段階です。したがって、すぐに検索結果全件表示されるという保証はありません。そのように、データの信頼性を高めるため遅延制御を実行するのが、次に挙げるuseDeferredValueフックです。

また、useMemoやuseCallbackといった処理系フック内部でuseTransactionは動かせません(cannot call startTransition while renderingというエラーが出ます)。

useDeferredValueフック

先程は待機時間中に別画面を表示する制御でしたが、今度は検索結果に対し待機(処理が終わるまで検索結果を表示させない)するフックを使ってみます。それが同じくReact18から登場し、React19で改良が加えられたuseDeferredValueフックで、deferとは先送りのことです。公式では抽象的なたとえをしていますが、要は結果を確定するまでの途中経過を隠してしまおう(データの信頼性を高めよう)という狙いを持ったフックです。先程のuseTranstionフックが第三者のためのユーザインターフェイス改善目的だとすると、今回は堅牢性、正確性が求められるwebシステム系で効果を発揮することになります。

※jQueryにあったpromiseとdeferの働きを覚えているなら、それと同じようなものだと認識してもいいと思います。

const 処理結果 = useDeferredValue(遅延させたい変数,初期値)

このように記述し、処理結果にはループ式など全処理が完了するまで表記を待機させたい変数を設定しておきます。また、React19からは第二引数に初期値設定もできるようになっています(React18時点では引数は一つ)。

js
import React,{ useState, useEffect,useMemo,useDeferredValue } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHeart } from "@fortawesome/free-solid-svg-icons";
import CitiesJson from './json/Cities.json';
//import StatesJson from './json/state.json';
//import './css/style.css';

function Lesson (){
  /*以下は要所を抜粋*/

	const [r_hit_cities_by_state,setHitCitiesByState] = useState([]) //エリアの検索結果

	const result = useDeferredValue(r_hit_cities,[]); //処理が終わるまで検索結果を表示させない

	//国名を選択
	const selectCountry = (e)=>{
		const sel_country = e.target.value
		//エリアの絞り込み
		const opt_states = states.filter((v)=>{ return v.country === sel_country})
		setListStates(opt_states)
		setSelCountry(sel_country)
	}
	//エリアを選択
	const selectState = (e)=>{
		const sel_state = e.target.value //選択したエリア
		//エリアから都市絞り込み
		const hit_cities_by_state = cities.filter((v)=>{ return v.area === sel_state })
		setHitCitiesByState(hit_cities_by_state)
		setSelState(sel_state)
	}
	//フリーワード検索
	const searchWord = (e)=>{
		const word = e.target.value
		const hit_cities_by_word = cities.filter((v)=>{
			const item = v.name.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
			return item.includes(word) && word
		})
		setHitCitiesByWord(hit_cities_by_word)
		setWord(word)
	}
	useEffect(()=>{
			const len_state = r_hit_cities_by_state.length
			const len_word = r_hit_cities_by_word.length
			let hits = [];
			if(len_state > 0 && len_word > 0 ){
				hits = require('lodash').intersection(r_hit_cities_by_state, r_hit_cities_by_word)
			}else if(len_state > 0){
				hits = r_hit_cities_by_state
			}else if(len_word > 0){
				hits = r_hit_cities_by_word
			}else{
				hits = []
			}
			setHitCities(hits)
	},[r_sel_state,r_word,startTransition])
	
	const updateAction = async()=>{
	}

	//クリア
	const clear = ()=>{
		setSelCountry('')
		setSelState('')
		setWord('')
		setHitCitiesByState([])
		setHitCitiesByWord([])
	}
	//レンダリングを行う記述
	return (			
	  <>
			<label> 国の選択 </label>
			<select id="sel1" onChange={selectCountry} value={r_sel_country}>
				<option value="">-- 国名を選択 --</option>
				{countries.map((item)=>
						<option key={item.id.toString()} value={item.code}>{item.name}</option>
					)
				}
			</select>
			<label> エリアの選択</label>
			<select id="sel2" onChange={selectState} value={r_sel_state}>
				<option>-- エリアを選択 --</option>
				{
					r_list_states.map((item,key)=>
					(
							<option key={item.id} value={item.code}>
								{item.name}
							</option>
					)
				}
			</select>
			<br/>
			<h4>検索文字を入力してください</h4>
			<input type="text" id="word" value={r_word} onChange={searchWord}/>
			<button type="button" onClick={ clear }>clear</button>
			<div>ヒット数{r_hit_cities.length}</div>
		    <ul className="ulDatas" >
				{
					result.map((item,key)=>
				 (
					<li className="liData" key={item.id}>
	 					<label className="lbHit">
									{ item.name }
									<label onClick={()=>{colorSet(item.id)}}>
										<FontAwesomeIcon icon={faHeart} style={{color:item.act}} />
									</label>
								</label>
						<br />
	 					<img className="imageStyle" src={`../../img/${item.src}`} alt={ item.name } />
					</li>
					)		
				}
			</ul>
		}
	)
}

export default Lesson7;

これでuseDeferredValueの戻り値resultに値が返されるまで、検索結果は表示されなくなります。

useRef(その1)

では、検索結果をクリアした際に検索フォームにフォーカスが戻る制御をuseRefで行ってみます。

useRefはレンダー時に取得が不要な値を参照するためのフックと、公式に記載されています。もっと簡単に言えばレンダー時に取得が必要な値がバインドの必要なデータだとすれば、それを補足するために役立つフックと認識すればいいでしょう。

今回は検索条件をクリアした後に検索フォームに、フォーカスさせる制御となります(他に色々できるが、基本は単一の部品しか結びつけられない)。

Lesson7.jsx
import { useState, useEffect,useMemo,useTransition,useRef } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHeart } from "@fortawesome/free-solid-svg-icons";
import CitiesJson from '../json/city.json';
import StatesJson from '../json/state.json';
//import './css/style.css';

function Lesson7 (){
	const inputRef = useRef(null);
	const [isPending,startTransition ] = useTransition(); //トランジションの設定
	//定義
	const countries= [
		{id:1,code:"US",name:"United States"},
		{id:2,code:"JP",name:"Japan"},
		{id:3,code:"CN",name:"China"},
	]
	//const selecteditem = []
	const word = "" //プロパティの定義
	const active = "red"
	const cities = CitiesJson
	const states = StatesJson
	const act = ''
	const [r_list_states, setListStates] = useState([]) //連動プルダウンのエリアオプション
	const [r_sel_country,setSelCountry] = useState('') //選択した国
	const [r_sel_state,setSelState] = useState('') //選択したエリア
	const [r_word,setWord] = useState('') //選択した文字
	const [r_hit_cities_by_state,setHitCitiesByState] = useState([]) //エリアの検索結果
	const [r_hit_cities_by_word,setHitCitiesByWord] = useState([]) //文字の検索結果
	const [r_hit_cities,setHitCities] = useState([]) //複合検索結果


	/*中略*/

	//クリア
	const clear = ()=>{
		setSelCountry('')
		setSelState('')
		setWord('')
		setHitCitiesByState([])
		setHitCitiesByWord([])
		inputRef.current.focus() //参照先のフォームにフォーカスする
	}
	//レンダリングを行う記述
	return (			
	  <>
			<label> 国の選択 </label>
			<select id="sel1" onChange={selectCountry} value={r_sel_country}>
				<option value="">-- 国名を選択 --</option>
				{countries.map((item)=>
						<option key={item.id.toString()} value={item.code}>{item.name}</option>
					)
				}
			</select>
			<label> エリアの選択</label>
			<select id="sel2" onChange={selectState} value={r_sel_state}>
				<option>-- エリアを選択 --</option>
				{
					r_list_states.map((item,key)=>
						(
							<option key={item.id} value={item.code}>
								{item.name}
							</option>
						)
					)
				}
			</select>
			<br/>
			<h4>検索文字を入力してください</h4>
			<input type="text" id="word" value={r_word} onChange={searchWord} ref={inputRef}/>
			<button type="button" onClick={ clear }>clear</button>
			{isPending ? <p>検索中です</p>:(
			<>
			<div>ヒット数:{r_hit_cities.length}</div>
		    <ul className="ulDatas" >
				{
					r_hit_cities.map((item,key)=>(
					<li className="liData" key={item.id}>
						<label className="lhHit">
							{ item.name }
							<label onClick={()=>colorSet(item.id)}>
								<FontAwesomeIcon icon={faHeart} style={{color:item.act}} />
							</label>
						</label>
						<br />
							<img className="imageStyle"
							src={`./assets/img/${item.area}_${item.name.toLowerCase()}.jpg`} alt={ item.name } />
						<br />
					</li>
					))		
				}
			</ul>
			</>)
			}
		</>
	)
}

export default Lesson7;

ちなみに、このようなrefで紐づけた部品を子コンポーネントに移動させ、任意に機能を追加させるのがuseComprativeHandleフックです(モーダルやアコーディオンを作れるみたいなので、いずれやってみたいとは思います)。

演習8:TypeScript(Todoアプリ)

では、ReactでもTypeScriptで記述していきたいと思います。Reactは割と早い段階からTypeScriptへの順応を始めていたので、以下のように書き換えるだけで対応してくれます。また、拡張子はtsxという独自の規格となります。ちなみにFcはFunction Componentの略でReactでTypeScriptを使用するための関数型定義宣言です。

※昨今のReact18では別に使用しなくても問題ないようです。更にReact19からはコンポーネント自体がメソッド化されているので、tsxファイルにさえすればts使用を自由選択できます。

tsx.App
import React,{ Fc } from 'react';

function App()=>{
    /*ここに処理を書いていく*/
}
export default App

むしろ、ReactのTypeScriptにおいてネックとなっていくのはフックの使用がかなり煩雑になるという点で、特にuseContextフックあたりは相当厳格になります。

Todoアプリの構成

Todoアプリの構成は以下のようになっています。元になった記事は以下の通り(元の記事はVueで作成)で、最低限の機能だけ動くようにしてReactで再構築しています。

txt
■Src
   ■components
      - Apptodo.tsx
      - App.tsx
      - DetailTodo.tsx
      - EditTodo.tsx
      - Todo.tsx
      - TodoItem.tsx
   ■types
      - TodoType.ts
   App.tsx
   Reducers.js //各種制御

Appコンポーネント

App.tsxは以下のように記述しています。ここで一番注意せねばならないのはuseContextフックの型指定で、今までのJSXならば、createContextメソッドの引数は不要でしたが、TypeScriptでコンテキストを作成する場合はコンポーネントに受け渡す対象となるオブジェクトに対して、型を一致させないといけません。

//useContext用に一致させたい型
const defaultContext: Reducer={
  addTodo: ()=> {},
  updTodo: ()=> {},
  delTodo: ()=> {},
  todos:[ ],
}
//初期値に型を一致させないとエラーとなる
export const TodoContext = createContext<Reducer>(defaultContext) 
const App: FC =()=>{
  const context = useContext<Reducer>(TodoContext)
return(
   <TodoContext.Provider value={
       {
        addTodo: addTodo,
        updTodo: updTodo,
        delTodo: delTodo,
        todos: todos,
       }
    }>
 )
}
export default App;
App.tsx
import { Fc,createContext,useContext,useReducer } from 'react';
import {BrowserRouter as Router, Route,Routes } from 'react-router-dom'
import Todo from './components/Todo'
import AddTodo from './components/AddTodo'
import EditTodo from './components/EditTodo'
import DetailTodo from './components/DetailTodo'
import todoReducer from './reducers.js'
import {Data,Reducer} from './types/todotype.ts'
import './Styles.css'
//useContext用に一致させたい型
const defaultContext: Reducer={
  addTodo: ()=> {},
  updTodo: ()=> {},
  delTodo: ()=> {},
  todos:[ ],
}
export const TodoContext = createContext<Reducer>(defaultContext)
function App(){
  const context = useContext<Reducer>(TodoContext)
  const iniData:Data = []
  const [todos,dispatch] = useReducer(todoReducer,iniData)
  const addTodo = (dif:Data)=>{
    dispatch({type:"ADD_TODO",data:{dif:dif}})
  }
  const updTodo = (id:char,dif:Data)=>{
    dispatch({type:"UPD_TODO",data:{id:id, dif:dif}})
  }
  const delTodo = (id:char)=>{
    dispatch({type:"DEL_TODO",data:{id:id}})
  }
  return (
    <TodoContext.Provider value={
       {
        addTodo: addTodo,
        updTodo: updTodo,
        delTodo: delTodo,
        todos: todos,
       }
    }>
      <Router>
        <Routes>
          <Route path="/" element={<Todo/>} />
          <Route path="/AddTodo" element={<AddTodo/>} />
          <Route path="/EditTodo/:id" element={<EditTodo/>} />
          <Route path="/DetailTodo/:id" element={<DetailTodo/>} />
        </Routes>
      </Router>
    </TodoContext.Provider>
  );
}
export default App;

Todoリストを制御する

Todoリストを制御するのは一覧を作成するTodo.tsxと各Todoを作成するTodoItem.tsxになります。

Todo.tsx
import {Fc,useContext} from 'react';
import {NavLink } from 'react-router-dom'
import TodoItem from './TodoItem'
import {TodoContext} from '../App' //紐付けたコンテクスト
function Todo(){
  const context = useContext(TodoContext) //コンテクストを呼び出す
	return(
		<>
        <h2>TODO一覧</h2>
        {context.todos.map((item,i)=>{
          return(
            <TodoItem key={i} todo={item} />
          )
        })}
        <NavLink to="/AddTodo">新規作成</NavLink>
		</>
	)
}

export default Todo;

各Todoの制御

子コンポーネントも同様にコンテクストから取得できますが、変数todoは親コンポーネントからのpropから取得しています。

TodoItem.tsx
import {FC,useContext} from 'react';
import {NavLink } from 'react-router-dom'
import {TodoContext} from '../App'

//各ToDOアイテムの子コンポーネント
function TodoItem(props){
  const context = useContext(TodoContext)
  const {todo} = props
	return(
		<>
      <div className="card">
        <div>
          <NavLink to={`/DetailTodo/${todo.id}`}>
            <span className="title" >{todo.title}</span>
          </NavLink>
            <span className="status">{todo.str_status}</span>
        </div>
        <div className="body">formatDate</div>
        <div className="action">
          <NavLink to={`/EditTodo/${todo.id}`}>
            <button onClick={()=>{context.updTodo(todo.id)}} >修正</button>
          </NavLink>
          <button onClick={()=>{context.delTodo(todo.id)}} >削除</button>
        </div>
      </div>
		</>
	)
}

export default TodoItem;

CRUD制御する

ではReactでCRUD制御をしていきます。そして、このCRUD機能はかなりTypeScriptの恩恵を受けることができます。なぜなら、空のオブジェクトデータに対しても、値の記述に対して制御をかけられるからで、より厳格に値の制御ができます。

Todoを新規登録する

新規登録するための制御は以下のようになっています。そして、Reactにおいて注意しなければいけないのが、リアルタイムにフォームを制御するためには、useStateフックが必須となります。ですが、フォーム数に比例してuseStateフックをずらずらと並べるのは効率が悪いので、下記リンク開発系ブログの記述を参考にしました。

また、各種フォーム用の制御オブジェクトに対し、初期値を設定しておかないと型不一致のエラーが発生するので、useStateフックに初期値initialを代入しています。

それから、useEffectフックやuseCallbackフックを利用する際には依存関係を厳格化しておかないと警告が出るため、書き方がかなり煩雑になっています。どの変数を代入しておくか悩みますが、依存関係というのがヒントで、この変数が再代入されることによって、計算される値が入れ替わる部分が目の付け所となります。したがって、コールバック関数のgetRandomIdは変数date(useRefフックの値は引数で制御されています)、useEffectフックはuseRefによって用意されたinputRefと、見落としがちですがコールバック関数そのものとなります。

※React18から採用された開発モードだとuseEffectフックが二回発動されます。肯定的な意見が多いですが、開発モードのときだけコンテクストを持つ全コンポーネントのuseEffectフックが干渉してしまう(このTodoアプリの場合AooTodo.tsxだけ対応しても二重に登録されてしまう)ので、自分としてはこれはどうなのかかとは思うんですが…。もし、これの実装を徹底したいならば、デプロイ時でも同様の挙動を実施すべきです。

AddTodo.tsx
import { FC,useState,useContext,useEffect,useMemo,useCallback,useRef } from 'react';
import {TodoContext} from '../App'
import {useNavigate } from 'react-router-dom'
import {Params,Data,Reducer} from './types/todotype.ts'

function AddTodo(){
  const date = useMemo(()=>{
    return new Date()
  },[])
  const initial:Params = {
    title: '',
    description: '',
    str_status:'waiting'
  }
  const [d_item,setItem] = useState<Data>(initial)
  const context = useContext<Reducer>(TodoContext)
  const inputRef = useRef<HTMLInputElement>(null)
  const navigate = useNavigate()
  const didLogRef = useRef(false)
  const getRandomId = useCallback((ref)=>{
    if (ref.current?.value === "") {
      const randomid = date.getTime()
      ref.current.value = randomid
    }
  },[date])
  useEffect(()=>{
    if(didLogRef.current === false){
      getRandomId(inputRef)
      didLogRef.current = true
    }
  },[inputRef,getRandomId])
  
  //更新
  const hundle = (e)=>{
    const {name,value} = e.target
    setItem({...d_item,[name]:value})
  }
  //更新ボタン押下イベント
  const onSubmit = ()=>{
    const id =inputRef.current.value
    context.addTodo({...d_item,id})
    navigate('/') //トップページに遷移
  }
	return (
    <>
      <h2>TODOの作成</h2>
        <div>
         <input name="id" type="text" ref={inputRef} readOnly />
        </div>
        <div>
          <label> タイトル</label>
          <input type="text" id="title" name="title" value={d_item.title} onChange={hundle} />
        </div>
        <div>
          <label></label>
          <textarea id="description" name="description" value={d_item.description} onChange={hundle} />
        </div>
        <div>
          <label>ステータス</label>
          <select id="status" name="str_status" onChange={hundle} >
            <option value="waiting">waiting</option>
            <option value="working">working</option>
            <option value="completed">completed</option>
            <option value="pending">pending</option>
          </select>
        </div>
        <button onClick={()=>onSubmit()}>作成する</button>
    </>
	)
}

export default AddTodo;

useRefフック(React18以降での用途)

このシステムではuseRefフックを使用しており、ランダムで作成されるid番号をフォーム上に参照させています。useRefフックはレンダリングに依存しないデータの参照を簡潔にするものです。また、このuseRefフックですが、React17まではステートを管理するのにも用いることができました。

import { Fc,useState,useContext,useEffect,useRef } from 'react';
  const inputRef = useRef<HTMLInputElement>(null)
  const getRandomId = ()=>{
    if (inputRef.current?.value === "") {
      inputRef.current.value = date.getTime() //タイムスタンプをもとに数値化
    }
  }
  useEffect(()=>{
    getRandomId() //画面遷移後に値を作成する
  },[])
  return(
         <input type="text" ref={inputRef} readOnly  />
  )

ところが、React18においてステート管理のためにuseRefを用いるのは、18の目玉機能でもあるトランジションに悪影響を及ぼすためバッドノウハウとなったようです。

では、このuseRefはReact18ではお役御免になるのかと思ったのですが、以下の記事(英文)を読めば、任意に初期値を設定したりする場合(つまり、ステートを管理するために用いるのではなく、乱数などの初期値をフォーム上に設定したりするのに用いる、つまりはrefプロパティと似た働きを持つ)に適しているとの回答でした。

したがって、今回はTodoリストを管轄するid(任意に乱数を発生させ、その値をユニークキーとする)の設定に用いています。

公式サイトでも、React18からはレンダー時には不要な値を参照するためのフックと紹介されるようになっています。レンダー時には不要な値、つまりは同期対象としない値、裏返せば再代入させてはいけない値ということです。

※任意のidを作成するuseIdフックというものもありますが、乱数生成は機械任せとなるので、今回のようにid生成に任意の条件(今回はタイムスタンプから作成)を付したい場合はuseRefが適切です。

修正ページを作成する

修正ページは以下のように制御します。登録ページとの違いはページ遷移直後に、各種フォームに値を反映させる必要があるので、useEffectフックとuseCallbackフックを使用して、コンストラクタ代わりの動作を再現しています。一方で、型は取得した変数から継承できるので、useStateフックの初期値を代入する必要はありません。

EditTodo.tsx
import React,{ FC,useState,useContext,useEffect,useCallback,useMemo } from 'react';
import {TodoContext} from '../App'
import {useParams,useNavigate } from 'react-router-dom'
import {Data,Reducer} from './types/todotype.ts'

function EditTodo(){
  const [d_item,setItem] = useState<Data>([])
  const context = useContext<Reducer>(TodoContext)
  const navigate = useNavigate()
  const params = useParams() //パラメータからid取得
  const params_id = useMemo(()=>{
     return Number(params.id) 
  },[params])
  const getCallback = useCallback( (ctx)=>{
    const item = ctx.todos.find((item:Data)=>item.id === params_id)
    setItem(item)
  },[params_id])
  useEffect(()=>
    getCallback(context)
   ,[getCallback,context])
  //フォームのリアルタイム更新
  const hundle = (e)=>{
    const {name,value} = e.target
    setItem({...d_item,[name]:value})
  } 
  //更新ボタン押下イベント
  const onSubmit = ()=>{
    const data = {
      title:d_item.title,
      description:d_item.description,
      str_status:d_item.str_status,
    }
    context.updTodo(params_id,data)
    navigate('/') //トップページに遷移
  }
	return (
    <>
      <h2>TODOを編集する</h2>
      {
        (params_id !== d_item.id)?
          <div>ID:{params.id}のTODOが見つかりませんでした{d_item.id}</div>
        :
          <>
          <div>
            <label>タイトル</label>
            <input type="text" id="title" name="title" value={d_item.title} onChange={hundle}/>
          </div>
          <div>
            <label>説明</label>
            <textarea id="description" name="description" value={d_item.description} onChange={hundle}/>
          </div>
          <div>
          <label>ステータス</label>
            <select id="status" name="str_status" onChange={hundle}>
              <option value="waiting">waiting</option>
              <option value="working">working</option>
              <option value="completed">completed</option>
              <option value="pending">pending</option>
            </select>
          </div>
          </>
      }
      <button onClick={()=>onSubmit()}>更新する</button>
    </>
	)
}

export default EditTodo

各種制御

useReducerフックによって制御された各種メソッドは以下のようになっています。ただ、注意点としてReactで削除制御する場合、spliceメソッドがうまく機能しない(値の再代入や登録や修正のように画面の遷移が行われないので、変更を検知しないそうです)ので、filterを使います。

※安易にuseNavigateを使用しようとしてもフック上では使用できませんとエラーが出ます。

Reducers.js
const addTodo = (state,data)=>{
  state.push(data.dif)
  return state
}
//データ更新
const updTodo = (state,data)=>{
  const idx = state.findIndex((item)=>item.id === data.id)
  state[idx] = {...state[idx],...data.dif}
  return state
}
//データ削除
const delTodo = (state,data)=>{
  const rest = state.filter((item)=>item.id !== data.id)
  return rest
}

export const todoReducer =(state,action):Todo[]=>{
  switch(action.type){
    case "ADD_TODO": return addTodo(state,action.data)
    case "UPD_TODO": return updTodo(state,action.data)
    case "DEL_TODO": return delTodo(state,action.data)
    default: return action.data
  }
}

export default todoReducer;

型定義について

型定義のファイルは以下のようになります。割と柔軟に任意の型を作成できるので定義最適な型を作り、anyで代用している部分を補完していくといいと思います。

todotype.ts
//ベースとなる型(|でor条件を作成できる)
export type Status = 'waiting'|'working'|'completed'|'pending'

export type Data = {
  id: number,
  title: string,
  description: string,
  str_status: Status, //上で定義した任意のパラメータ
}

//アレンジされた型
//任意のオブジェクトから独自の型定義を作成したい場合(ここではDataオブジェクトのうち、title、description、sutatusの3つのプロパティを用いたParams型を作成している)
export type Params = Pick<Data,'title'|'description'|'status'>

//基本の型定義を組み合わせたい場合(TodosはData型の集合オブジェクトであると定義)
export interface Todos{
  todos: Data{}
}

//メソッドもこのように定義できる(useContextで受け渡すには必須)
export type Reducer = {
  addTodo: (dif:Data)=> void,
  updTodo: (id:number,dif:Data)=> void,
  delTodo: (id:number)=>void,
  todos: Data
}
1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?