在使用JS框架React进行数据处理和汇总的版本

这个是将前一篇文章的续作按照各自的框架进行分类,以便更容易地追踪时代潮流的变化。

    jQuery愛好家のためのVue.js、React、Angular入門

我希望这次能够进行更加深入的操作说明,包括上次无法完成的组件部分。此外,为了提高实践能力,我们将使用服务器环境来进行控制,因此React将在React Native中进行控制。

尽管 React 在 18 版进行了相当大规模的重大更改,但我们目前还是以 React 17 标准进行编写。

由于ReactNative在版本16.8(函数组件)上的采用较晚,所以在此之前(类组件)的记法流行一段时间,但到2023年为止,类组件已经成为一种过时(少数派)的记法,因此本文将只解释函数组件。

※本次学习的内容是

    • 5章 コンポーネント制御(電卓)

 

    • 6章 ルーティング制御(買い物かご)

 

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

 

    8章 TypeScript(Todoアプリ)

是的。

另外,最近React中使用TypeScript编写已成为主流,但在第8章之前我们并未涉及它(如果直接进入TypeScript而没有掌握基础,可能会更加困惑)。我们将在第8章的练习中介绍TypeScript。

此外,在本文中,我们在第六章介绍了useContext钩子、useCallback钩子和useReducer钩子,在第七章介绍了useMemo钩子,在第八章介绍了useRef钩子。

导入语法的普遍规则

在那之前,应该要了解JavaScript的import语法的规则。

    • A:import Fuga from ‘./Hoge’

 

    B:import {Fuga} from ‘./Hoge’

不了解以上两者的区别会导致之后出现许多错误。A的意思是在外部文件中定义一个名为Fuga的Hoge(通常与导入文件名相同,不需要特意改名)。而B的意思是使用外部文件Hoge中定义的名为Fuga的对象。所以以React的例子来说,

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

这是一个命令,使用名为React的外部文件,并利用useState、useEffect和useContext对象。

练习5 组件控制(计算器)

在这个例子中,我们将使用React来构建一个简单的系统,使用子组件来构建父组件。我们将创建一个简易计算器。为了实现这一目标,我们需要控制按钮的按下时机,通过将按钮组件转换为子组件,我们可以更高效地构建系统。

在这之前,为什么要将父母组件进行分割和分层呢?简单来说,这是为了避免冗长的描述。

●安装应用程序

您可以使用npx命令进行React应用程序的安装。首先,创建一个新的目录来创建任何应用程序所需的新目录(与Vue不同,即使进行覆盖也不会有任何提示,所以请注意)。

html # mkdir 任意のアプリケーション名

在这里需要注意的是不要切换到任何应用程序。接下来,请继续输入以下命令(适用于Rhel系列)。

html # npx create-react-app 任意のアプリケーション名

如果npx不能正常工作,请参考以下的Qiita内文章。

    React: Create React Appでプロジェクトがつくれない

●组件的基本结构

React组件的调用如下所示,Layout.js是基本的组件。只需要导入文件和编写组件标签,比Vue少一步。

※Layout.js与client.js有关联。

import React from "react";
import ReactDOM from "react-dom";
import Layout from "./components/Layout";
const root = document.getElementById('app');
ReactDOM.render(<Layout />, root); //このLayoutコンポーネントを呼び出している
import React from "react";
import Calc from "./Calc"; //親コンポーネントを呼び出し

//JSX内にケバブケースでコンポーネントを記述する
export default class Layout extends React.Component {
  render() {
    return (
      <>
	  <Calc />{/*親コンポーネント*/}
      </>    
    );
  }
}

●在将亲子组件写入同一文件时需要注意的事项

在React中,处理具有父子关系的组件经常在同一个文件中进行。然而,当使用函数组件来控制父子组件时,只需将需要被外部调用的父组件导出为”export default Parent”即可。如果不小心将子组件也导出为”export default Child”,可能会导致控制错误。

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

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

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

在React中控制组件

React的组件控制比其他JS框架更容易理解。

然而,React有一个与子组件向父组件传递值的emit方法相对应的特点是不存在的。但是,由于可以传播包括事件在内的回调函数,所以可以通过让父组件行为看似接收到事件的方式来实现值的同步。

从父组件传递值到子组件

只需要把值传递给子组件,可以在定义函数的参数中写上props,然后子组件就可以从props对象中获取值。而props是properties的缩写,意思是属性、拥有的意思。也就是说,如果将其理解为父组件拥有的变量,那么可以知道子组件只是在引用父组件拥有的变量。

●在React中编写父组件

为了解释父子组件的功能,我们将具体制作一个计算器。请注意,文件名如下所示。

    • 親コンポーネント Calc.js

 

    子コンポーネント Setkey.js //プッシュキー

●控制亲组件

父组件如下所示。

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

const 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中编写组件,并通过定义组件文件来构造父子关系的组件。

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

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

export default Calc

● 子组件的控制

子组件如下所示。

import React,{useState,useEffect} from "react";
const 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中,就可以展开推送键。

const 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)=>{
					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

从子组件向父组件传递数据

如果要将数据从子组件传递给父组件,您可以在子组件的方法中准备参数,并在执行父组件提供的回调函数时,可以从内联函数的参数中取出值。

具体而言,这里的父组件中的变量fuga 就是子组件中提供的回调函数getCharState 的变量hoge。

    • 子 : getHogeState(hoge) //コールバック関数に引数をセット

 

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

<子组件的链接标签 子组件到回调函数=>{(通过子组件调用的变量《fC》=>父组件上的处理方法(父组件想要处理的变量《fP》)}} />

所以,如果将fC设置为fp,就可以直接将子组件的值传递给父组件。具体来说,以下是子组件中的

<在接收状态中设置密钥=>{(c,s)=>{当接收到(c,s)时执行onReceive}} />

这是变量c和变量s的部分。

  //子コンポーネントから受け取った値を親コンポーネントで展開
  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>
			</>
	)
}
  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>
					)
				})
			}
		</>
  )
}

●嵌入JSX的规则

当使用React循环时,如之前所提到的,我们可以使用**Array.prototype.map(item,key)**,但是也可以像这次一样使用双重循环。不过,如果不好好整理思路,很容易变得混乱。因此,我将解释其中要注意的要点。

    • 変数、メソッドはオブジェクトリテラル{}で囲む

 

    • returnで返せるタグは単一のエレメントで、returnで返すタグは()で囲む

 

    • 複数のタグを返したい場合でdivタグを用いたくない場合は、空タグ<>かプロパティを設定できる空タグタグのいずれかを用いるとよい。

 

    mapメソッドでループさせる場合はkeyプロパティを設定する(ユニークなkeyを付与しないと警告が出る)

当你掌握了这四项规则

   return (
        <>
            {state.vals.map((val,key)=>{
                return (
                    <div key={key}>
                    {
                        val.map((v,i)=>{
                            return(
								<React.Fragment key={i}>
								<button className="square" onClick={()=>{getCharState(v[0],v[1])}}>
									{v[1]}
								</button>
								</React.Fragment>
                            )
                        })
                    }
                    </div>
                )
            })
			}
        </>
    )
  }

我想你可能能够理解这个描述。首先,整个描述被空标签<>所包围,并且循环部分由方法组成,所以用对象字面量{}进行了包围。对象字面量中的内容需要按照每列四个组织推送键。

タグを設定しています。また、この

タグの中身には二重ループ用のmapメソッドを埋め込んでいるので、やはりオブジェクトリテラル{}で囲む必要があります。
そのval.mapメソッドの中身にあるのはbuttonタグですが、これは4行分実行されるため、そのままだと単一エレメントを返すルールを持つJSXの記述違反になり、エラーとなります。そこで方法として<>も考えられるのですが、空タグ<>はプロパティ設定できないため、keyプロパティの設定ができず、以下の警告が表示されることになります。Each child in a list should have a unique “key” prop

ですが、今回は別にオブジェクトを操作しないのでkeyプロパティは重要視されません。そこで、代わりにタグを用いると、プロパティも設定できるので、keyタグを便宜上設置することができます。
GitHubフォーラム内の議題

Unable to eliminate ‘Each child in a list should have a unique “key” prop’ warning

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

演習5のまとめ
このようにコンポーネントの分割の目的は冗長なエレメントを集約し、テンプレート化することで無駄な記述を回避するためです。また、それにあたって変数の受け渡しの処理が必要になります。
要約するとこうなります。

同一ファイルにコンポーネントが複数あっても問題ない。また、その場合exportして良いのは親コンポーネントだけ。
親から子への値の受け渡しはpropsプロパティを設定し、プロトタイプから受け取る。

子から親への受け渡しはコールバック関数を用いる。処理の同期をとるには、コールバック関数を受け渡す。
子から親への値の受け渡しは、子のコールバック関数に引数を代入し、親のインライン関数の引数から受け取る。

演習6 ルーティング(買い物かご)
今までは親子コンポーネントの説明はしていますが、あくまで単一のページのみの制御でした。ですが、世の中のWEBページやアプリケーションは複数のページを自在に行き来できます。それを制御しているのがルーティングという機能です。
フレームワークにおけるルーティングとはフレームワークのように基本となるリンク元のコンポーネントがあって、パスの指定によってコンポーネントを切替できるというものです。もっと専門的な言葉を用いれば、SPA(SINGLE PAGE APPLICATION)というものに対し、URIを振り分けて各種コンポーネントファイルによって紐付けられたインターフェースを表示するというものです。
そしてReactではreact-router-domというライブラリが必須となります。フレームワークにリンクタグとリンク先を表示するタグが存在し、toというパス記述用のプロパティが存在しています(react-router-dom6はtoプロパティが廃止され、componentというプロパティを実装)。
また、データ転送用、データ受取用のライブラリが用意されており、それらを受け渡しと受け取り、そして更新のタイミングで実行していきます。

●Reactでルーティング制御する
reactでルーティング制御をするにはreact-router-domというライブラリが必須となります。事前にインストールしておきましょう。Reactの場合はこれをimportするだけでルーティングが使えるようになります。

元にしたプログラム(codesandbox)
React Hooks and Context Shopping Cart

#npm install react-router-dom

●Reactのルーティングの仕組み
Reactのルーティングの仕組みは、それぞれのリンクタグに対し、リンクの表示先をRouteタグで表示するというものです。なので、表示先をシェアする関係にあるので、Switch(vue-route-dom6はRouter)というタグで表示を切換制御しています。
Reactの場合のデータ構造
■component
– ■css(デザイン制御用、表示は割愛)
– ■pages
– MainNavigation.js //子コンポーネント(ナビ部分。ここにルーティング用のリンクを記述)
– Products.js //商品一覧(ここに商品を入れるボタンがある)
– Cart.js //買い物かご(ここに商品差し戻し、購入のボタンがある)
– ShopContext.js //オブジェクト情報の格納
– GlobalState.js //親コンポーネント
– Reducers.js //共通の処理制御用
– router.js //ルーティング制御用

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

●ビュー部分
ビュー部分はBrowserRouteタグに記述された領域であり、Routeタグに直接ルーティング情報を記述していく形となりますので、複数のタグを記述する必要があります。pathプロパティに遷移先のURI、componentプロパティにインポート先のコンポーネントを記述します。また、exactプロパティを記述する(デフォルトはtrue)ことで完全一致のパスしかルーティングされなくなります。
そして、これらをSwitchタグで囲むことで、後述するナビ部分のリンクタグに従って切換できるので、複数ページのルーティングが可能になります。
import React from ‘react’
import {BrowserRouter as Router, Route, Switch } from “react-router-dom”
import ProductsPage from “./pages/Products”
import CartPage from “./pages/Cart”
import MainNavigation from “./MainNavigation”

const App = props =>{
return(

)
}
export default App

●ナビ部分
リンク部分はタグによって制御され、toプロパティが遷移先となります。
import React,{ useState,useEffect,useContext,useCallback} from “react”
import ShopContext from “./context/shop-context”
import { NavLink } from “react-router-dom”
import “./MainNavigation.css”

const MainNavigation = props =>{
const context = useContext(ShopContext)
const [cnt,setCnt] = useState([]);
const context = useContext(ShopContext)
const [cnt,setCnt] = useState([]);
const cartItemNumber = useCallback(()=>{
return context.cart.reduce((count,curItem)=>{
return count + curItem.quantity //買い物かごの個数
},0)
},[context.cart])
const articlesItemNumber = useCallback(()=>{
return context.articles.reduce((count,artItem)=>{
return count + artItem.quantity //購入の個数
},0)
},[context.articles])

useEffect(()=>{
const cnt_tmp = […cnt]
cnt_tmp.cart = cartItemNumber()
cnt_tmp.articles = articlesItemNumber()
setCnt(cnt_tmp)
},[context])
return(

)
}
export default MainNavigation

●react-router-dom6以降の場合
react-router-dom6以降の場合は、以下の書き方でないとエラーとなります。elementプロパティは直接コンポーネントを指定できるようになります。
import {BrowserRouter as Router, Route, Routes } from “react-router-dom”

} />
} />

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

●useContextフック
今回、中心的な活躍を遂げるフックがuseContextフックですが、これは簡潔に言うと、親から孫要素に値を受け渡しできるものです。従来のpropsなどだと、たとえば、親から孫へ要素を受け渡したい場合、必ず親→子→孫という風に、値をリレーさせないといけませんでしたが、useContextフックを用いることで、渡したい要素に受け渡すことが可能になります。
また、JSXには
<コンテクスト名.Provider value={転送したいオブジェクト} />
とすることで、任意のコンテクストをどのコンポーネントにも転送することができます。

Parent.js
import React,{createContext} from “react”;
export ParentContext = createContext() //任意のコンテクスト作成
const Parent = ()=>{
const val = “hoge”
return(
{/*任意のJSXやコンポーネント*/}
)
}

Child.js
import React,{useContext} from “React”
import {ParentContext} = fro ‘./Parent’
const Child = ()=>{
}
export default Child = ()=>{
const ctx = useContext(ParentContext)
return(
<>

{ ctx.val }

</>
)
}

●記述例
たとえば、前述した第5章の電卓の場合は以下のようになります。propsとの違いは子コンポーネントの値を親コンポーネントに転送する場合ですが、制御用のメソッドごとコンテクストに置いておくといいでしょう。

Calc.js
import React, { useState,createContext } from “react”;
import Setkey from “./Setkey”;
export const SetkeyContext = createContext()
const Calc = ()=>{
const[ d_sum, setSum ] = useState(0)
const[ d_str, setStr ] = useState(“”)
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:””, //文字列
}
const onReceive = (str,sum)=>{
setStr(str)
setSum(sum)
}

return (

打ち込んだ文字:{d_str}

合計:{d_sum}

)
}

export default Calc

Setkey.js
import React,{useState,useEffect,useContext} from “react”;
import {SetkeyContext} from “./Calc”

const Setkey = ()=>{
const ctx = useContext(SetkeyContext) //useContextフックで値を受け取る
const [d_vals,setVals] = useState([])
useEffect(()=>{
const state = ctx.states //コンテクストで受け渡された変数states
const vals = {
lnum: state.lnum,
cnum: state.cnum,
sum: state.sum,
sign: state.sign,
str: state.str,
}
setVals(vals)
},[])
const getChar = (chr,strtmp)=>{
/*中略*/
ctx.onReceive(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 (
<>
{ctx.states.vals.map((val,key)=>{
return (

{
val.map((v,i)=>{
return(

)
})
}

)
})
}
</>
)
}

export default Setkey

●useContextフックで兄弟コンポーネントを制御
useContextフックの基本を説明したところで、本題に入っていきます。親子コンポーネントの応用で、以下のルーティング制御における兄弟コンポーネントに対しても、系統化することでデータを受け渡すことが可能になります。なお、受け渡しの対象となるのはcreateContext()メソッドで作成した系統名となり、それぞれ値を継承させていますが、それぞれ系統の異なるコンポーネントに受け渡すことはできません。また、子孫名.Provider要素はReact.Fragmentの代用にもなるのでkeyプロパティを仕込むこともできます。
それをルーティング先で使用する場合は後述するグローバルステートにデータを用意しておくことで、ルーティング先に対して、自在にデータを受け渡し、同期を取ることができます。

親コンポーネントはApp(Layout.jsにある紐づけ先)との紐づけだけにしておく

js.ShoppingCart.js
import React from ‘react’
import GlobalState from “./context/GlobalState”

const App = props =>{
return(

)
}
export default App

GlobalState.js
import React ,{ useState, useReducer,useContext } from “react”
import {BrowserRouter as Router, Route, Switch } from “react-router-dom”
import ShopContext from “./shop-context” //変数情報
import { shopReducer, ADD_PRODUCT, REMOVE_PRODUCT, BUY_IT } from “./reducers”
import ProductsPage from “../pages/Products”
import CartPage from “../pages/Cart”
import MainNavigation from “../MainNavigation”

const GlobalState = props =>{
const context = useContext(ShopContext) //このフック
const [products,setProducts] = useState(context.storage)
const [money,setMoney] = useState(context.money)
const [total,setTotal] = useState(context.total)
//useReducerフックを用いることで、cartを同時に扱うことができる
const [aryState, dispatch] = useReducer(shopReducer, {
products: products,
money: money,
total: total,
cart: [],
articles: [],
})

//買い物かごに追加
const addProductToCart = product =>{
dispatch({type: “ADD_PRODUCT”, product: product})
}
//買い物かごから削除
const removeProductFromCart = productId =>{
dispatch({type: “REMOVE_PRODUCT”,productId: productId})
}
//買い物かごから全商品を所持品に移し替え
const buyIt = articles =>{
dispatch({type: “BUY_IT” ,articles: articles,money: money})
}

return(

)
}
export default GlobalState

●各コンポーネントの記述
ルーティング先の各コンポーネントの記述はこのようになっています。このルーティング先は全部、ShopContextというコンテクストによって紐付けられているので、このコンテクストからJSX及びデータの値を取得しています。
※このuseContextフックにおいては、Consumerラップは不要です。
また、同系統コンテクストからイベントとデータを同期するには
onClick= {()=>context.methodHoge(payload)}
とすれば、いけるようです(payloadは任意のデータオブジェクト)。

Products.js
import React,{useContext} from “react”
import ShopContext from “../context/shop-context”

import “./Products.css”
const ProductsPage = props =>{
const context = useContext(ShopContext)
return(
<>

    • {context.products.map(product => (

    • {product.title} – {product.price}円 【残り{product.stock}個】
      {product.stock > 0 && }

))}

</>
)
}
export default ProductsPage

ShopContext.jsは以下のとおりです

ShopContext.js
import React from “react”

export default React.createContext({
storage:[
{ 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, //残額
})

●※ルーティング制御しながらuseContextフックでデータを受け渡す際の注意点
react-router-domでルーティングをしながらuseContextフックを活用する場合、いくつか注意しないといけない点があります。

1:ProviderでラップするタグはちょうどBrowserRouterタグを覆うようにすること
ルーティング先の対象となるのはBrowserRouterタグ内なので、そこにuseContextタグを用いる場合は、そのタグを覆うように記述しましょう。

App.js
const App = ()=>{
return(

{/* 中略 */}

)
}

このルールを守らないと、データを受け渡ません。また、ルーティング先で同期処理を行いたい場合は

useContextでuseStateの戻り値2つを受け渡すと便利
このようにルーティング元コンポーネントで設定していたuseStateフックの戻り値(更新前、更新対象)を2つとも渡して、ルーティング先で、スプレッド構文で受け取れば、簡単に同期が取れます(スプレッド構文で展開して、分割代入することを忘れないで下さい。全削除する場合でも同様です)。

App.js
const App = ()=>{
return(

{/* 中略 */}

)
}

Hoge.js
ctx = useContext(Hoge.context)
useEffect(()=>{
const piyo = […ctx.fuga] //useContextから継承したオブジェクト。スプレッド構文で受け取る
ctx.setFuga(piyo) //更新対象に用いるメソッドごと受け取れば同期がとれる
},[])

●useCallbackフック
このシステムではuseCallbackフックを使用しています。useCallbackフックは端的にいえば、繰り返し行う動作において処理を記憶させておくフックで、基本はパフォーマンス向上のためのフックです。ここではナビに表示される、買い物かごの商品個数を計算しています。この計算処理が必要なのは個数が変化したタイミングだけでいいので、このようにナビに記述することで、個数が変化した場合のみ合計個数の再計算処理を行うようにしています(lengthだと同一商品が複数になった場合、対応できない)。
//買い物かごの個数が変化したタイミングで計算
const cartItemNumber = useCallback(()=>{
return context.cart.reduce((count,curItem)=>{
return count + curItem.quantity //買い物かごの個数
},0)
},[context.cart])
useEffect(()=>{
const cnt_tmp = […cnt] //分割代入で個数管理のオブジェクトを展開
cnt_tmp.cart = cartItemNumber() //買い物かごの個数
setCnt(cnt_tmp)
},[context])

●useReducerフック
このシステムではuseReducerフックを使用しています。useReducerフックは複雑に分岐された処理を集約化するためのフックです。この買い物かごなどのように、処理するデータが複数(商品格納、買い物かご、所持金、合計金額など処理しなければいけない変数が色々ある)、またはステータスによって処理を分岐したい場合に重宝します。

vue
const[aryState, dispatch] = useReducer(shopReducer,initial)

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

ステータスを格納できるプロパティを持っている(処理の分岐ができる)

複数のデータを一度に処理できる
こういう特長があります。

そしてaryStateに代入するのは更新対象となるデータ、initialに代入するのはshopReducerで処理するために用いるデータ情報の初期値となります。また、dispatchは更新処理をするためのstateメソッドを格納し、更新処理の種類を示すtype、データを格納するpayloadの2つのプロパティがあります(payloadプロパティは省略可で、普通は記述しません)。それをonClickなどのイベントの引数に代入して設定します(階層化してるので、useContextフックによって引き渡された変数を同期させています)。
残る変数shopReducerはreducerメソッドといい、aryStateを具体的に更新するメソッド(ここでは買い物かごに追加)を記述します。
具体的に見ていきます。useReducerの第一引数にあるメソッドの中身が記述されており、stateにはaryStateによって更新対象となるデータ、actionにはdispatchに記述されたtypeと対象となるデータが格納されています。なので、action.typeがdispatchのtypeプロパティと合致しているので、処理を振り分けることができるわけです。
ざっくばらんにuseReducerフックの働きを分解するとこうなります。
[更新後の値,{type:処理の分岐名,(payload):差分データ}] = useReducer(処理,初期値)

GlobalState.js
const [aryState, dispatch] = useReducer(shopReducer, {
products: products,
money: money,
total: total,
cart: [],
articles: [],
})

reducers.js
//useReducerに紐づいた処理振り分けの関数
export const shopReducer = (state, action) => {
switch (action.type) {
case “ADD_PRODUCT”: //買い物かごに追加
return addProductToCart(action.product, state);
case “REMOVE_PRODUCT”: //買い物かごから削除
return removeProductFromCart(action.productId, state);
case “BUY_IT”: //買い物かごから購入
return buyIt(action.articles, state);
default:
return state;
}
};

ちなみに制御用プログラム、reducer.jsの全容はこうなっています。各種処理メソッドはVueのものの使い回しで、最後の戻り値を返す部分だけ変更すれば、どんなフレームワークにも換装できたりします。

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 //加算対象のインデックス } stat.cart = updatedCart //商品在庫の調整 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
stat.products = updatedProducts
//合計金額の調整
const total = stat.total
const sum = getSummary(updatedCart,total)
stat.total = sum
console.log(stat) //totalは0のままになってしまう
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)
state = {…state, total: summary}
state = { …state, cart: updatedCart }
state = {…state, products: updatedProducts }
return state
};

//購入手続
const buyIt = (articles,state)=>{
let cart = state.cart
let money = state.money
let total = state.total
let updatedArticles = […articles] //所持品
let tmp_cart = […state.cart]

for( let cart of tmp_cart){
let articlesIndex = articles.findIndex(
a => a.id === cart.id
)
if (articlesIndex < 0) { updatedArticles.push(cart); } else { const tmpArticles = { …articles[articlesIndex] } tmpArticles.quantity++; updatedArticles[articlesIndex] = tmpArticles; } } let summary = getSummary(cart,total) let rest = money – summary tmp_cart.splice(0) summary = 0 state = {…state, total: summary} state = {…state, money: rest} state = {…state, cart: tmp_cart} state = {…state, articles: updatedArticles} return state } const getSummary = (cart,total)=>{
const sum = cart.reduce((total,{price = 0,quantity})=> total + price * quantity,0)
return sum
}

export const shopReducer = (state, action) => {
switch (action.type) {
case “ADD_PRODUCT”:
return addProductToCart(action.product, state);

case “REMOVE_PRODUCT”:
return removeProductFromCart(action.productId, state);

case “BUY_IT”:
return buyIt(action.articles, state);

default:
return state;
}
};

●詳細画面を作成する(パラメータのやりとり)
Reactでも詳細画面を作成する場合、パラメータのやりとりが必須になってきます。ただ、Reactの場合はVue、そして後述するAngularより便利な点があり、操作するのがhtmlではなくてJSXであるため、toプロパティに変数や関数をダイレクトに埋め込むことができます。

Products.js
{product.title}

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

js.GlobalState.js
import DetailPage from “../pages/Detail” //コンポーネントを追記
const GlobalState = ()=>{
/*略*/
return(
{/*略*/}

{/*略*/}
)
}

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

Detail.js
import React,{useState,useContext} from “react”
import ShopContext from “../context/shop-context”
import { useParams } from “react-router-dom”

const DetailPage = ()=>{
const context = useContext(ShopContext)
const [d_item,setItems] = useState([])
const { id } = useParams() //useParamsはオブジェクトをそのまま抽出できる
//カスタムコンポーネント作成し、そこで準備する
const ShowItem = ()=>{
const selid = `p${id}` //取得したパラメータを検索用idに修正
const item = products.find((item)=> item.id == selid) //一致するアイテム取得
return(

  • {item.title}

)
}
return(
<>

</>
)
}
export default DetailPage

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

js.Detail.js
import React,{useState,use,useContext,useEffect,useCallback} from “react”
import ShopContext from “../context/shop-context”
import { useParams } from “react-router-dom”

const DetailPage = ()=>{
const context = useContext(ShopContext)
const [d_item,setItem] = useState([])
const { id } = useParams()
//アイテム取得処理を記憶させておくことで、JSX展開前に処理が可能になる
const getCallback = useCallback(()=>{
const products = […context.products]
const selid = `p${id}`
const item = products.find((item)=> item.id == selid)
setItem(item)
},[id])
useEffect(()=>{
getCallback()
},[])
return(
<>

  • {d_item.title}

</>
)
}
export default DetailPage

●クエリを使ってデータをやりとりする
では、URLに埋め込まれたクエリパラメータ(get)を利用して、同じ操作をしてみます。ただ、Reactの場合はクエリパラメータを取得するのは少し手間がかかります。

Products.js
{product.title}

GlobalState.js

パラメータを取得する場合はuseLocationというフックを使用して、そこからsearchオブジェクトを取得、それをURLSearchParamsメソッドでクエリ情報を取得、そこからgetメソッドを使用するという手間を踏みます。

Detail.js
import { useLocation } from “react-router-dom”

const DetailPage = ()=>{
const context = useContext(ShopContext)
const [d_item,setItem] = useState([])
const { search } = useLocation() //searchオブジェクトを取得
const getCallback = useCallback(()=>{
const query = new URLSearchParams(search) //クエリ情報を取得
const selid = query.get(‘id’) //get情報を取得
const item = context.products.find((item)=> item.id == selid)
setItem(item)
},[search])
use

※react-router-dom6以降ならuseSearchParamsフックが新たに実装されたので、そこからgetメソッドですぐに取得できます。

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

●戻るボタンを実装する
ルーティング先から前ページに戻る場合はuseHistoryフックのgoBackメソッドを用います。

Detail.js
import { useLocation,useHistory } from “react-router-dom” //useHistoryを追記
/*略*/
const DetailPage = ()=>{
const context = useContext(ShopContext)
const [d_item,setItem] = useState([])
const {search} = useLocation()
const history = useHistory()
useMemo(()=>{
const query = new URLSearchParams(search)
const selid = query.get(‘id’)
const item = context.products.find((item)=> item.id == selid)
setItem(item)
},[search])
return(
<>

  • {d_item.title}


</>
)
}
export default DetailPage

※ react-router-dom6の場合
useHistoryが廃止され、useNavigateを使用するようになっています。このメソッドは引数に遷移先のパスを明示的に記述できるので、非常に便利です。

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

演習6のまとめ

■Reactのまとめ

react-router-domライブラリが必須。
リンクはタグでリンク元を、タグでリンク先を表示する。
ルーティング情報はRouteタグのプロパティに記述する、その際Switchタグでルーティング先を切り替える。また、ルーティング情報はRouterタグで制御される。
兄弟コンポーネントへのデータのやりとりはuseContextフックを用いると効率的。その際はグローバルステートにデータを格納する。
パラメータを送信するにはJSXのイベントに直接書き込む。データを受け取る場合、パラメータはuseParams、クエリパラメータはuseLocationフックをそれぞれ使用する。
一つ前の画面に戻る場合はuseHistoryフックのgoBackメソッドを使用する。イベントはonClickで実行する。

演習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 (
<>









検索文字を入力してください


ヒット数:{r_hit_cities.length}件
    • {
    • r_hit_cities.map((item,key)=>{
    • return(

    •     

)
})
}

</>
)
}

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(

  •     

)
}

これでアイコンがリストごとに表示されます。同様に、クリックイベントはlabelタグに置く方が無難でしょう。また、クリックイベントは子コンポーネントから伝播させているので、イベント伝播用のコールバック関数(colorSetstate)を用意しておきます。また、コールバック関数の場合はuseContextフックが利用できないようなので、propsオブジェクトでデータを受け渡すようにしましょう。
また、スタイル制御の部分ですが、Vueだとクラス名を記述してそれを有効、無効で切り換えてスタイルを動的に制御していましたが、Reactの場合はスタイルタグをそのまま反映させることができます(逆にいえば、スタイルの有効、無効切り換えは面倒です)。
そして、この記述部分ですが、これはvueのような二重ブラケットではなく、オブジェクトリテラルの中に、スタイルを記述しているため、見かけ二重に見えているだけです。つまり

js

と同じことです。そして、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と似ているのですが、どちらかというとVueの監視プロパティっぽい働きをする、処理された値を記憶しておくフックで、一定の処理によって更新され、常時表示しておきたい値(ここではr_hit_citiesという検索結果情報)に対し、対象となるデータに紐づけ処理を実行させるもので、これを実装することによって、検索結果が変更された場合のみ、論理積計算を実行することになるので、処理能率が改善されます。

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

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

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

※ちなみにこのシステムをuseEffectフックにしても動作しますが、逐一検索に反応してしまうので、パフォーマンスはだいぶ落ちます。

演習8:TypeScript(Todoアプリ)
では、ReactでもtypeScriptで記述していきたいと思います。Reactは割りと早い段階からTypeScriptへの順応を始めていたので、以下のように書き換えるだけで対応してくれます。また、拡張子はtsxという独自の規格となります。ちなみにFcはFunction Componentの略でReactでTypeScriptを使用するための関数型定義宣言です。
※昨今のReact18では別に使用しなくても問題ないようですが…。

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

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

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

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

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(defaultContext)
const App: FC =()=>{
const context = useContext(TodoContext)
return(

)
}
export default App;

また、ルーティングはreatct-router-dom6を使用しており、今までとは大幅に記述方法が変わっている(後方互換性もない)ので注意です。

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(defaultContext)
const App: FC =()=>{
const context = useContext(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 (

} />
} />
} />
} />

);
}
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’ //紐付けたコンテクスト
const Todo: FC = ()=>{
const context = useContext(TodoContext) //コンテクストを呼び出す
return(
<>

TODO一覧

{context.todos.map((item,i)=>{
return(

)
})}
新規作成 </>
)
}

export default Todo;

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

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

//各ToDOアイテムの子コンポーネント
const TodoItem: FC = (props)=>{
const context = useContext(TodoContext)
const {todo} = props
return(
<>

{todo.title}
{todo.str_status}
formatDate

</>
)
}

export default TodoItem;

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

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

stateのオブジェクト型にして、stateの更新処理を共通化(1つに)する

また、各種フォーム用の制御オブジェクトに対し、初期値を設定しておかないと型不一致のエラーが発生するので、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’

const AddTodo: FC = () => {
const date = useMemo(()=>{
return new Date()
},[])
const initial:Params = {
title: ”,
description: ”,
str_status:’waiting’
}
const [d_item,setItem] = useState(initial)
const context = useContext(TodoContext)
const inputRef = useRef(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 (
<>

TODOの作成



广告
将在 10 秒后关闭
bannerAds