前記事の続編をそれぞれのフレームワーク毎に分類し、時流の変化を追いやすくしたものです。
今回は、前回で補完できなかった、コンポーネント周りのもっと突っ込んだ操作を含めた解説ができたらと思います。また、前回まではローカルサーバ上で操作していましたが、実践的な力を身に付けていくにはやはりサーバを用いた環境で制御していくべきなので、ReactはReact Nativeで制御していきます。
Reactも18でかなり大掛かりな仕様変更がありましたが、一旦はReact17基準で書いています。
※ReactNativeはReactよりバージョン16.8(関数コンポーネント)への採用が遅れたため、それ以前の記法(クラスコンポーネント)が主流となっていた時期もありましたが、2023年現在、クラスコンポーネントは古い記法(少数派)となっているので、当記事は関数コンポーネントのみで解説していきます。
※今回学習する内容は
- 5章 コンポーネント制御(電卓)
- 6章 ルーティング制御(買い物かご)
- 7章 スタイル制御(写真検索システム)
- 8章 TypeScript(Todoアプリ)
となります。
※また、昨今はReactでもTypeScriptでの記述が主流になってきていますが、8章まではそこに触れていません(基本を押さえないままTypeScriptに入ろうとすると余計にこんがらがってしまうと思うので)。演習8でTypeScriptについて触れています。
また、この記事ではuseContextフック、useCallbackフック、useReducerフックを6章で、useMemoフックを7章で、useRefフックを8章で紹介しています。
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というオブジェクトを利用するという命令になります。
演習5 コンポーネント制御(電卓)
ここではReactに対し、それぞれ子コンポーネントを用いて、親コンポーネントで簡易なシステムを構築していきたいと思います。そして、ここでは簡易な電卓を作成していきます。それにはプッシュキーとそれを押下したタイミングでの制御が必要となりますが、その際にプッシュキーの部品を子コンポーネント化することで、効率よくシステムを構築することができます。
その前に、なぜ親子のコンポーネントに分割、階層化するかですが、結論からいえば冗長な記述を回避するためです。
●アプリケーションのインストール
Reactのアプリケーションインストールですが、npxコマンドで行うことができます。まずは任意のアプリケーションを作成していくための新規ディレクトリを作成しておきます(Vueと違って、上書きしても何も質問されないので注意)。
html # mkdir 任意のアプリケーション名
ここで注意なのは、任意のアプリケーションに移動しないことです。引き続き、以下のコマンドを打ちます(Rhel系のb)。
html # npx create-react-app 任意のアプリケーション名
※npxがうまく動かない場合は以下のQiita内記事を参考にしてください。
●コンポーネントの基本構造
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とはpropatiesの略で、属性、所有のことです。つまりは親コンポーネントが所有している変数という意味合いで捉えれば、子コンポーネントはその親コンポーネントが持っている変数を参照しているだけだとわかるでしょう。
●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にしておけば、子コンポーネントの値をそのまま親コンポーネントに返すことができます。具体的には下記子コンポーネントにおける
<Setkey onReceiveState=>{(c,s)=>{onReceive(c,s)}} />
の変数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タグを用いたくない場合は、空タグ
<>
かプロパティを設定できる空タグ<React.Fragment>
タグのいずれかを用いるとよい。 - mapメソッドでループさせる場合はkeyプロパティを設定する(ユニークなkeyを付与しないと警告が出る)
この4つのルールを把握すると
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>
)
})
}
</>
)
}
この記述も読み解けるのではないかと思います。まず、全体に対し空タグ<>
で囲んでおり、それに対し、ループ部分はメソッドなのでオブジェクトリテラル{}
で囲んでいます。そのオブジェクトリテラルの中身ですが、プッシュキーを4列ずつ並べる必要があるので、
<div>
タグの中身には二重ループ用のmapメソッドを埋め込んでいるので、やはりオブジェクトリテラル{}
で囲む必要があります。
そのval.mapメソッドの中身にあるのはbuttonタグですが、これは4行分実行されるため、そのままだと単一エレメントを返すルールを持つJSXの記述違反になり、エラーとなります。そこで方法として<>
も考えられるのですが、空タグ<>はプロパティ設定できないため、keyプロパティの設定ができず、以下の警告が表示されることになります。
Each child in a list should have a unique "key" prop
ですが、今回は別にオブジェクトを操作しないのでkeyプロパティは重要視されません。そこで、代わりに<React.Fragment>
タグを用いると、プロパティも設定できるので、keyタグを便宜上設置することができます。
GitHubフォーラム内の議題
●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(
<BrowserRouter>
<MainNavigation/>
<Switch>
<Route path="/" component={ProductsPage} exact />
<Route path="/cart" component={CartPage} exact />
</Switch>
</BrowserRouter>
)
}
export default App
●ナビ部分
リンク部分は<NavLink>
タグによって制御され、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(
<header className="main-navigation">
<nav>
<ul>
<li>
<NavLink to="/">Products</NavLink>
</li>
<li>
<NavLink to="/cart">Cart ({cnt.cart})</NavLink>
</li>
</ul>
</nav>
</header>
)
}
export default MainNavigation
●react-router-dom6以降の場合
react-router-dom6以降の場合は、以下の書き方でないとエラーとなります。elementプロパティは直接コンポーネントを指定できるようになります。
import {BrowserRouter as Router, Route, Routes } from "react-router-dom"
<Routes>
<Route path="/" element={<ProductsPage />} />
<Route path="/cart" element={<CartPage />} />
</Routes>
●データをやりとりする
Reactでデータをやりとりするのはそこまで難解さはありません。なぜならコンポーネント間で自由にやりとりができるuseContextフックが兄弟コンポーネントであっても使えるからです。したがって、先程のMainNavigation.jsの先にGlobalState.jsという子コンポーネントを作成しておいて、そこに受け渡し用の変数を格納しておけば、あとの変数のやりとりは全部useContextフックがデータを受け渡ししてくれるようになります。
●useContextフック
今回、中心的な活躍を遂げるフックがuseContextフックですが、これは簡潔に言うと、親から孫要素に値を受け渡しできるものです。従来のpropsなどだと、たとえば、親から孫へ要素を受け渡したい場合、必ず親→子→孫という風に、値をリレーさせないといけませんでしたが、useContextフックを用いることで、渡したい要素に受け渡すことが可能になります。
また、JSXには
<コンテクスト名.Provider value={転送したいオブジェクト} />
とすることで、任意のコンテクストをどのコンポーネントにも転送することができます。
import React,{createContext} from "react";
export ParentContext = createContext() //任意のコンテクスト作成
const Parent = ()=>{
const val = "hoge"
return(
<ParentContext.Provider value={val}>
{/*任意のJSXやコンポーネント*/}
</Parentcontext.Provider>
)
}
import React,{useContext} from "React"
import {ParentContext} = fro './Parent'
const Child = ()=>{
}
export default Child = ()=>{
const ctx = useContext(ParentContext)
return(
<>
<p>{ ctx.val }</p>
</>
)
}
●記述例
たとえば、前述した第5章の電卓の場合は以下のようになります。propsとの違いは子コンポーネントの値を親コンポーネントに転送する場合ですが、制御用のメソッドごとコンテクストに置いておくといいでしょう。
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 (
<SetkeyContext.Provider value={{states:states,onReceive:onReceive}}>
<Setkey />
<div>
<p>打ち込んだ文字:{d_str}</p>
<p>合計:{d_sum}</p>
</div>
</SetkeyContext.Provider>
)
}
export default Calc
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 (
<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
●useContextフックで兄弟コンポーネントを制御
useContextフックの基本を説明したところで、本題に入っていきます。親子コンポーネントの応用で、以下のルーティング制御における兄弟コンポーネントに対しても、系統化することでデータを受け渡すことが可能になります。なお、受け渡しの対象となるのはcreateContext()
メソッドで作成した系統名となり、それぞれ値を継承させていますが、それぞれ系統の異なるコンポーネントに受け渡すことはできません。また、子孫名.Provider要素はReact.Fragmentの代用にもなるのでkeyプロパティを仕込むこともできます。
それをルーティング先で使用する場合は後述するグローバルステートにデータを用意しておくことで、ルーティング先に対して、自在にデータを受け渡し、同期を取ることができます。
- 親コンポーネントはApp(Layout.jsにある紐づけ先)との紐づけだけにしておく
import React from 'react'
import GlobalState from "./context/GlobalState"
const App = props =>{
return(
<GlobalState />
)
}
export default App
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(
<ShopContext.Provider
value={{
products: aryState.products,
cart: aryState.cart,
articles: aryState.articles,
money: aryState.money,
total: aryState.total,
addProductToCart: addProductToCart,
removeProductFromCart: removeProductFromCart,
buyIt: buyIt,
}}
>
<Router>
<MainNavigation/>
<Switch>
<Route path="/" component={ProductsPage} exact />
<Route path="/cart" component={CartPage} exact />
</Switch>
</Router>
</ShopContext.Provider>
)
}
export default GlobalState
●各コンポーネントの記述
ルーティング先の各コンポーネントの記述はこのようになっています。このルーティング先は全部、ShopContextというコンテクストによって紐付けられているので、このコンテクストからJSX及びデータの値を取得しています。
※このuseContextフックにおいては、Consumerラップは不要です。
また、同系統コンテクストからイベントとデータを同期するには
onClick= {()=>context.methodHoge(payload)}
とすれば、いけるようです(payloadは任意のデータオブジェクト)。
import React,{useContext} from "react"
import ShopContext from "../context/shop-context"
import "./Products.css"
const ProductsPage = props =>{
const context = useContext(ShopContext)
return(
<>
<main className="products">
<ul>
{context.products.map(product => (
<li key={product.id}>
<div>
<strong>{product.title}</strong> - {product.price}円 【残り{product.stock}個】
</div>
<div>
{product.stock > 0 && <button onClick={()=>context.addProductToCart(product)}>かごに入れる</button>}
</div>
</li>
))}
</ul>
</main>
</>
)
}
export default ProductsPage
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タグを用いる場合は、そのタグを覆うように記述しましょう。
const App = ()=>{
return(
<HogeContext.Provider value={fuga:fuga}>
<BrowserRouter>
{/* 中略 */}
</BrowserRouter>
</HogeContext.Provider>
)
}
このルールを守らないと、データを受け渡ません。また、ルーティング先で同期処理を行いたい場合は
- useContextでuseStateの戻り値2つを受け渡すと便利
このようにルーティング元コンポーネントで設定していたuseStateフックの戻り値(更新前、更新対象)を2つとも渡して、ルーティング先で、スプレッド構文で受け取れば、簡単に同期が取れます(スプレッド構文で展開して、分割代入することを忘れないで下さい。全削除する場合でも同様です)。
const App = ()=>{
return(
<HogeContext.Provider value={fuga:fuga,setFuga:setFuga}>
<BrowserRouter>
{/* 中略 */}
</BrowserRouter>
</HogeContext.Provider>
)
}
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フックは複雑に分岐された処理を集約化するためのフックです。この買い物かごなどのように、処理するデータが複数(商品格納、買い物かご、所持金、合計金額など処理しなければいけない変数が色々ある)、またはステータスによって処理を分岐したい場合に重宝します。
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(処理,初期値)
const [aryState, dispatch] = useReducer(shopReducer, {
products: products,
money: money,
total: total,
cart: [],
articles: [],
})
//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のものの使い回しで、最後の戻り値を返す部分だけ変更すれば、どんなフレームワークにも換装できたりします。
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プロパティに変数や関数をダイレクトに埋め込むことができます。
<NavLink to={`/detail/${product.id.replace("p","")}`}><strong>{product.title}</strong></NavLink>
また、グローバルステートのRouterタグ内は以下のように追記しておきます。同様に:idがパラメータ名となります。
import DetailPage from "../pages/Detail" //コンポーネントを追記
const GlobalState = ()=>{
/*略*/
return(
{/*略*/}
<Router>
<MainNavigation/>
<Switch>
<Route path="/detail/:id" component={DetailPage} exact/>
<Route path="/" component={ProductsPage} exact />
<Route path="/cart" component={CartPage} exact />
<Route path="/articles" component={ArticlesPage} exact />
</Switch>
</Router>
{/*略*/}
)
}
●パラメータを取得する
パラメータを取得する場合はuseParamsというフックを活用できます(このフックはreact-router-domからなので注意)。そして、このuseParamsはオブジェクトをそのまま抽出できるので便利です。ただし、Reactならではの注意点があり、JSXに展開する場合、useEffectだとDOM生成後に作動するものなので、未定義の変数エラーに悩まされることになります。なので、コンポーネント化して展開するのが一般的な方法ですが…。
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(
<li>{item.title}</li>
)
}
return(
<>
<ul>
<ShowItem />
</ul>
</>
)
}
export default DetailPage
どうしてもコンポーネント化したくない場合はuseCallbackフックを使用すれば、DOM生成前に処理を実行して結果を記憶できるので、きちんと変数を展開できたりします(useEffectフックだけだとDOM生成後しか機能しないので、d_itemが未定義変数エラーになります)。
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(
<>
<ul>
<li>{d_item.title}</li>
</ul>
</>
)
}
export default DetailPage
●クエリを使ってデータをやりとりする
では、URLに埋め込まれたクエリパラメータ(get)を利用して、同じ操作をしてみます。ただ、Reactの場合はクエリパラメータを取得するのは少し手間がかかります。
<NavLink to={`/detail?id=${product.id}`}><strong>{product.title}</strong></NavLink>
<Route path="/detail" component={DetailPage} exact/>
パラメータを取得する場合はuseLocationというフックを使用して、そこからsearchオブジェクトを取得、それをURLSearchParamsメソッドでクエリ情報を取得、そこからgetメソッドを使用するという手間を踏みます。
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メソッドですぐに取得できます。
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メソッドを用います。
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(
<>
<ul>
<li>{d_item.title}</li>
</ul>
<button onClick={()=>history.goBack()}>戻る</button>
</>
)
}
export default DetailPage
※ react-router-dom6の場合
useHistoryが廃止され、useNavigateを使用するようになっています。このメソッドは引数に遷移先のパスを明示的に記述できるので、非常に便利です。
import {useNavigate } from 'react-router-dom'
/*中略*/
const navigate = useNavigate()
navigate('/') //ホームに戻る
演習6のまとめ
■Reactのまとめ
- react-router-domライブラリが必須。
- リンクは<NavLink to="パス">タグでリンク元を、<Route>タグでリンク先を表示する。
- ルーティング情報は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をインストールしておきましょう。
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)=>{
return (
<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">
{
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 World
●Font-awesomeを利用する
Reactの場合はfasがどうもうまく使えないようなので、使用したいアイコンを一つ一つ明示しておく必要がありそうです。
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のような二重ブラケットではなく、オブジェクトリテラルの中に、スタイルを記述しているため、見かけ二重に見えているだけです。つまり
<FontAwesomeIcon icon={faHeart} style={
{color:item.act}
}/>
と同じことです。そして、item.actにcolorプロパティの値を代入するだけで、動的制御が可能になります。ですが、もう一つ落とし穴があります。
●オブジェクトから値を制御する
第4章のデータの修正にもあった通り、関数コンポーネントでオブジェクトの値を修正するには、分割代入が必須で、これを実施しないとオブジェクトの値をリアクティブ制御できません。
また、データの更新ですが、このように分割代入で検索結果のオブジェクトを展開し、スタイル制御用のプロパティの値を代入してから差し替えるという方法を用いることで、任意のインデックスに紐付いたオブジェクトの値を差し替えることが可能になります。
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という検索結果情報)に対し、対象となるデータに紐づけ処理を実行させるもので、これを実装することによって、検索結果が変更された場合のみ、論理積計算を実行することになるので、処理能率が改善されます。
import React,{ useState, useEffect, useMemo } from "react";
useMemo(()=>{
//実行したい処理
},[監視したいデータ]}
※また、このように参照結果を返したいだけなら、戻り値を設定することもできます。
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では別に使用しなくても問題ないようですが…。
import React,{ Fc } from 'react';
Const App: Fc = ()=>{
/*ここに処理を書いていく*/
}
export default App
むしろ、ReactのTypeScriptにおいてネックとなっていくのはフックの使用がかなり煩雑になるという点で、特にuseContextフックあたりは相当厳格になります。
Todoアプリの構成
Todoアプリの構成は以下のようになっています。元になった記事は以下の通り(元の記事はVueで作成)で、最低限の機能だけ動くようにしてReactで再構築しています。
https://qiita.com/azukiazusa/items/205ae0cc5378419b1337
■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;
また、ルーティングはreatct-router-dom6を使用しており、今までとは大幅に記述方法が変わっている(後方互換性もない)ので注意です。
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)
const App: FC =()=>{
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になります。
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(
<>
<h2>TODO一覧</h2>
{context.todos.map((item,i)=>{
return(
<TodoItem key={i} todo={item} />
)
})}
<NavLink to="/AddTodo">新規作成</NavLink>
</>
)
}
export default Todo;
各Todoの制御
子コンポーネントも同様にコンテクストから取得できますが、変数todoは親コンポーネントからのpropから取得しています。
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(
<>
<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だけ対応しても二重に登録されてしまう)ので、自分としてはこれはどうなのかかとは思うんですが…。
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<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フックは要素への参照を簡潔にするものです。また、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を用いるのはバッドノウハウになったようです。
https://qiita.com/uhyo/items/6a3b14950c1ef6974024
では、このuseRefはReact18ではお役御免になるのかと思ったのですが、以下の記事(英文)を読めば、任意に初期値を設定したりする場合(つまり、ステートを管理するために用いるのではなく、乱数などの初期値をフォーム上に設定したりするのに用いる)に適しているとの回答でした。したがって、今回はTodoリストを管轄するidの設定に用いています。
React v18: useRef — What, When and Why?
修正ページを作成する
修正ページは以下のように制御します。登録ページとの違いはページ遷移直後に、各種フォームに値を反映させる必要があるので、useEffectフックとuseCallbackフックを使用して、コンストラクタ代わりの動作を再現しています。一方で、型は取得した変数から継承できるので、useStateフックの初期値を代入する必要はありません。
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'
const EditTodo: FC = () => {
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を使用しようとしてもフック上では使用できませんとエラーが出ます。
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で代用している部分を補完していくといいと思います。
//ベースとなる型(|で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
}
演習9:Next.js ※書きかけ
React一般普及の背景にはNext.jsの存在が大きいですが、Next.jsはある程度Reactをマスターしてからの方がいいです。また、Next.jsもバージョンによって色々とディレクトリ、ファイル構成が異なるので注意が必要です。
以下書きかけ