React入門(フック編)
はじめに
仕事でReactを用いたフロントエンド開発を行うことになったため、Reactについての学習をゼロから開始しました。
学んだ内容を概念ごとに分割して記事を作成していきます。
本記事の対象読者は、プログラミングやアプリケーション構築の基礎知識はあるけど、Reactは未経験という方です。
フックとは
フックとは関数コンポーネントでステート管理やライフサイクルメソッドを利用するために利用する機能のことです。
フックの関数名は全てuseXxxxxの形に統一されています。
主なフックとして以下のものを紹介します。
useState: コンポーネント内の状態管理を行う。
useEffect: DOMの書き換え、変数代入、API通信などUI構築以外の副作用の処理を行う
コンポーネントのレンダリング後に実行される。useContext: コンポーネントを跨いだ変数、関数の管理を行う。
useForm: ReactHookFormを使用してフォームの状態管理を行う。
useQuery: API呼び出しの非同期処理を簡易化する。
主にGETメソッドに使用される。uesMutation: API呼び出しの非同期処理を簡易化する。
主にPOST,PUT,DELETEメソッドに使用される。
フックはコンポーネントのトップレベルでのみ使用可能となっていて、ループや条件分岐などのネストされた場所では使用できないため注意が必要です。
UseState
useStateは関数コンポーネントが内部で保持する「状態」を管理するためのフックです。
propsは呼び出し元から任意のタイミングで更新されるため、画面表示などで直接使用すべきではありません。
また、propsの値を直接変更することはできないため、propsで取得した値をstateで管理して、適宜変更しながらコンポーネント内で使用します。
基本構文
useEffectの基本構文は以下のようになります。
const [variable, setVariable] = useState(initialState)
variableにはstateとして管理したい変数名
initialStateには初期値が格納されます。
setVariableはvariableの値を更新する際に使用するセッター関数です。
stateの値は直接変更は行わず、全てセッター関数を通して行います。
実装例
以下にuseStateを使用した関数コンポーネントの実装例を示します。
import { Button, Grid } from "@mui/material";
import React, { useState } from "react";
export function TestPage(props: { count: number }): JSX.Element {
const [count, setCount] = useState(props.count);
return (
<>
<Grid item container justifyContent="center" sx={{ mt: 10, mb: 10 }}>
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
onClick={() => setCount((prev) => prev + 1)}
type="button"
>
カウント
</Button>
</Grid>
<Grid item xs={6}>
<p>{count}</p>
</Grid>
</Grid>
</>
);
}
上記の例はpropsで受け取ったcountを関数コンポーネント内でstateで管理を行い、画面表示に利用しています。
また、カウントボタンをクリックした場合はsetCount関数を使用してstateで管理しているcountの値を更新しています。
備考:Reactでの状態管理にはuseStateの他にReduxに基づいたuseReducerというフックも存在しますが、説明は割愛します。
オブジェクトのステートの管理
フォームを使用する場合は、フォームに関する値は一つのオブジェクトに束ねることが望ましいため、ステートでオブジェクトの管理が必要になります。
ステートでオブジェクトの管理を行う場合は以下のように実装します。
ステートを管理する親プロパティ(制御コンポーネント)
function SearchStuff(): JSX.Element {
// 検索条件のステート
const [searchStuffInfo, setSearchStuffInfo] = useState<SearchStuffInfo>({
stuffName: "",
stuffCalMax: 10000,
stuffCalMin: 0,
stuffPMax: 10000,
stuffPMin: 0,
stuffFMax: 10000,
stuffFMin: 0,
stuffCMax: 10000,
stuffCMin: 0,
});
...
return (
<>
<SearchStuffLayout
searchStuffInfo={searchStuffInfo}
setSearchStuffInfo={setSearchStuffInfo}
...
/>
</>
);
}
export default SearchStuff;
ステートの値を更新するフォームが存在する子プロパティ
function SearchStuffLayout(props: {
searchStuffInfo: SearchStuffInfo; // 検索条件
setSearchStuffInfo: any;
...
}): JSX.Element {
// 使用用途用確認
const title = "食材検索";
return (
<>
{/* 食材名 */}
<Grid container item justifyContent="flex-start" xs={10} sx={{ mt: 2 }}>
<Grid item>
<TextField
type="text"
name="stuffName"
id="search-stuff-name"
label="食材名"
value={props.searchStuffInfo.stuffName}
variant="outlined"
onChange={(event) => {
props.setSearchStuffInfo((searchStuffInfo: any) => ({
...searchStuffInfo,
[event.target.name]: event.target.value,
}));
}}
/>
</Grid>
...
</Grid>
</Grid>
</>
);
}
export default SearchStuffLayout;
ステートの更新時は...searchStuffInfoによりオブジェクトを個々の要素に分解し、set関数に値を引き渡しています。
更新対象の値以外の値は元々オブジェクトに格納されている値がそのまま使用されるため変更されません。(元の値と同じ値で更新をかけるイメージ)。
更新対象のプロパティ名は、[event.target.name](算出プロパティ名)により更新がかかった要素の名前(event.target.name)をそのままプロパティ名として、値として入力値(event.target.value)を与えています。
算出プロパティを使用するために、Stateで管理するオブジェクトのプロパティ名とformのname属性は一致させておくことが望ましいです。
上記の例だと、
[event.target.name]にはstuffNameが入り、
event.target.valueには入力フォームで入力した値が設定されます。
入れ子のステートの管理
Stateで管理する値が入れ子構造になっている場合は以下のようにハンドラーを分ける必要があります。
import { useState } from 'react'
function StateNest() {
// 入れ子のStateを作成
const [form, setForm] = useState({
name: '名前',
address: {
prefecture: '都道府県',
city: '市町村'
}
});
// 一段目の要素を更新するハンドラー
const handleForm = e => {
setForm({
...form,
[e.target.name]: e.target.value
});
};
// 二段目の要素を更新するハンドラー
const handleNestForm = e => {
setForm({
...form,
address: {
...form.address,
[e.target.name]: e.target.value
};
});
};
}
これはスプレッド構文による複製が一段目の複製のみであることによるものです。入れ子になっている要素はオブジェクトを参照しているだけの状態になっている(複製された別のオブジェクトではなく、同一のオブジェクトになる)ため、ネスト要素に対しては各階層ごとに追加で要素を複製する必要があります。
このようなことが起こるため、可能な限りステートにはネストした要素を含まないようにすることが望ましいです。
配列型Stateの更新
配列型のStateを更新する際は、メソッドの処理結果の新しい配列を戻り値とするメソッド(concat,map,filterなど)を使用して作成した新しい配列をset関数でステートに設定するようにし、呼び出し元のオブジェクトを直接更新するメソッド(push,splice,popなど)は使用しないようにします。
useEffect
useEffectはコンポーネントの状態が変化(propsかstateが変化)したタイミングで実行すべき処理を定義するためのフックです。
Reactの外部の機能との連携を行う時(APIによるデータ取得など)以外は極力使用しないようにします。
基本構文
useEffectの基本構文は以下のようになります。
import { Button, ButtonGroup, Grid } from "@mui/material";
import React, { useEffect, useState } from "react";
export function TestPage(): JSX.Element {
const [count, setCount] = useState(0);
const [title, setTitle] = useState("");
useEffect(() => {
setTitle(`${count}回クリックされました`);
}, [count]);
return (
<>
<Grid item container justifyContent="center" sx={{ mt: 10, mb: 10 }}>
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
onClick={() => setCount((prev) => prev + 1)}
type="button"
>
カウント
</Button>
</Grid>
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
onClick={() => setCount(0)}
type="button"
>
リセット
</Button>
</Grid>
<Grid item xs={6}>
<p>{title}</p>
</Grid>
</Grid>
</>
);
}
第一引数:副作用関数(関数型)
第二引数:useEffectの実行契機となる依存変数(配列)
第二引数で指定した依存変数が更新されたタイミングで副作用関数が実行されることになります。
第二引数がから配列の場合は初回描画時のみ処理が実行されることになります。
実装例
以下にuseEffectを使用した関数コンポーネントの実装例を示します。
import { Button, ButtonGroup, Grid } from "@mui/material";
import React, { useEffect, useState } from "react";
export function TestPage(): JSX.Element {
const [count, setCount] = useState(0);
const [title, setTitle] = useState("");
useEffect(() => {
setTitle(`${count}回クリックされました`);
}, [count]);
return (
<>
<Grid item container justifyContent="center" sx={{ mt: 10, mb: 10 }}>
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
onClick={() => setCount((prev) => prev + 1)}
type="button"
>
カウント
</Button>
</Grid>
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
onClick={() => setCount(0)}
type="button"
>
リセット
</Button>
</Grid>
<Grid item xs={6}>
<p>{title}</p>
</Grid>
</Grid>
</>
);
}
上記の例は以下のような処理を行っています。
1.カウントボタンをクリックするとcountが更新
2.countが更新されるとuseEfectが更新を検知して副作用関数を実行
3.副作用関数によりtitleが更新
4.再描画がかかり画面が更新される。
注意事項
useEffectを使用する際は依存配列の選定に注意が必要です。
上記の例で依存配列にtitleを加えてしまうと、
1. useEffectによりtitleが更新
2. titleの更新によりuseEffectが実行
3. 1.戻って無限ループ
という流れで無限ループが発生してしまう可能性があります。
useContext
useContextとはContext機能を利用してコンポーネントを跨いだ変数、関数の管理を行う機能です。
複数コンポーネントで共通して使用する変数や関数をpropsでのバケツリレーを行うことなく使用できるようになります。
contextとは
Reactコンポーネントのツリーに対して「グローバル」とみなすデータについて利用するように設計されているものです。
コンポーネントツリー間におけるデータの橋渡しについて、すべての階層ごとに渡す必要性がなくなり、各階層でのデータのアクセスが容易になります。
一方で、コンポーネントの再利用をより難しくする為、使用は慎重に行う必要があります。
基本的にアプリ全体で使用する値以外には使用しないようにします。
基本構文
useContextは3つの段階を踏んで実現されます。
親要素
export const ExampleContext createContext(initialContext)
export function Test() {
...
return (
...
<ExampleContext.Provider value={count}>
...
</ExampleContext.Provider>
)
}
子要素
useContext(ExampleContext)
exampleには任意のコンテキスト名を使用します。
親要素でcreateContextによりコンテキストの作成と
returnでコンテキストを使用するコンポーネントの上位コンポーネントとしてコンテキストを設定します。
コンテキストを使用する子コンポーネントでuseContexを使用してコンテキストの値を取得します。
実装例
以下にuseContextを使用した実装例を示します。
親要素
import React, { createContext, useState } from "react";
import { TestChild } from "./TestChild";
export const CountContext = createContext(0);
export const HobbyContext = createContext("");
export function TestPage(): JSX.Element {
const [count, setCount] = useState(0);
return (
<>
<CountContext.Provider value={count}>
<TestChild />
</CountContext.Provider>
</>
);
}
子要素
import React, { createContext } from "react";
import { TestGrandChild } from "./TestGrandchild";
export const CountContext = createContext(0);
export const HobbyContext = createContext("");
export function TestChild(): JSX.Element {
return (
<>
<TestGrandChild />
</>
);
}
孫要素
import { Grid } from "@mui/material";
import React, { createContext, useContext } from "react";
export const CountContext = createContext(0);
export const HobbyContext = createContext("");
export function TestGrandChild(): JSX.Element {
const count = useContext(CountContext);
return (
<>
<Grid item container justifyContent="center" sx={{ mt: 10, mb: 10 }}>
<Grid item xs={6}>
<p>{count}</p>
</Grid>
</Grid>
;
</>
);
}
上記の例は親要素でcountについてのコンテキストを作成し、孫要素からcountのコンテキストを使用している例です。
複数のコンテキストを使用する場合は、親要素でコンテキストの中でコンテキストを作成するようにネストしてコンテキストを使用することで可能になります。
useForm
useFormはReactHookFormを使用したフォームの状態管理を行うためのフックです。
React Hook Formとはフォームの入力データの状態管理やバリデーションを簡易化するための仕組みのことです。
基本構文
const methods = useForm<AddDishInfo>({
defaultValues: jsonPatch.deepClone(props.addDishInfo),
resolver: yupResolver(addDishLayoutSchema),
mode: "all",
});
...
return (
<>
<FormProvider {...methods}>
...
</FormProvider>
<>
)
useForm関数でReact Hook Formで使用するメソッド群を作成します。
<AddDishInfl>は型引数でフォームで管理するオブジェクトの方がAddDishInfo型であることを示しています。
defaultValueではフォームに設定するデフォルト値を設定しています。
デフォルト値にpropsから取得した値を設定する場合は、propsが参照型オブジェクトで、直接変更を行うと意図しないバグの原因となりかねないため、deepcloneでpropsとは独立した新しいオブジェクトを作成してデフォルト値の設定に使用します。
上記の例ではバリデーションにyupを使用しているため、resolverで対象のyupコードを指定してバリデーションを追加しています。
modeはバリデーションの実行タイミングの指定を行っています。
FormProvider
FormProviderはReact Hook Form のメソッド(例:register、handleSubmit など)を簡単にフォーム全体で使用できるようにするためのコンポーネントです。個別の入力コンポーネントでも、直接 useForm フックを呼ばずにフォームの状態管理やバリデーションを行えるようになります。
FormProviderで展開したReact Hook Form のメソッドは子コンポーネント側でuseFormContext()を使用することで使用可能になります。
以下に子コンポーネント側での取得例を示します。
子コンポーネント
import { useFormContext } from 'react-hook-form';
const UsernameInput = () => {
const { register } = useFormContext(); // FormProviderからregisterを取得
return (
<div>
<label>Username</label>
<input {...register('username', { required: true })} />
</div>
);
};
const PasswordInput = () => {
const { register } = useFormContext(); // FormProviderからregisterを取得
return (
<div>
<label>Password</label>
<input type="password" {...register('password', { required: true })} />
</div>
);
};
カスタムフック
カスタムフックとは、ユーザーが自作するフックのことです。
コンポーネントを横断して再利用したいフックを利用したコードを切り出しておくために使用します。
戻り値には、呼び出し元のコンポーネントで使用したい値やコードを指定します。
カスタムフックであっても命名は基本的にuseXxxxxの形にします。
テキストフィールドとReact Hook Formを組み合わせる
以下に例としてテキストフィールドの作成時にReactHookFormの挙動を簡単に追加するためのカスタムコンポーネントを示します。
カスタムコンポーネント
import { TextField, TextFieldProps } from "@mui/material";
import { Control, FieldValues, Path, useController } from "react-hook-form";
export default function FormTextField<TFieldValues extends FieldValues>(
// TextFieldPropsはTextFieldを拡張するときに使うTextFieldの元々の要素の集合体
props: TextFieldProps & {
control: Control<TFieldValues>;
name: Path<TFieldValues>;
}
): JSX.Element {
// props を分割して name と control を取得し、残りのプロパティを textFieldProps にまとめる
const { name, control, ...textFieldProps } = props;
const {
field: { ref, ...rest },
fieldState,
} = useController({ name, control });
return (
<FormProvider {...methods}>
...
<TextField
inputRef={ref}
{...rest}
{...textFieldProps}
error={fieldState.error ? true : false}
helperText={fieldState.error?.message}
/>
</FormProvider>
);
}
カスタムコンポーネントの呼び出し元
// react-hook-formの設定
const methods = useForm<AimSettingInfo>({
resolver: yupResolver(aimSettingLayoutSchema),
mode: "all",
});
return (
...
<FormTextField
name="targetChangeWeight"
control={control}
label="目標の体重変化量(kg)"
variant="outlined"
required
fullWidth
/>
...
)
yupでのバリデーションコード
import * as yup from 'yup'
const aimSettingLayoutSchema = yup.object().shape({
targetChangeWeight: yup
.number()
.typeError('半角数字を入力してください。')
.required('目標の体重変化量を入力してください。'),
targetPeriod: yup
.number()
.typeError('半角数字を入力してください。')
.min(0,'0以上の整数を入力してください。')
.required('目標の体重変化期間を入力してください。')
.integer('整数を入力してください。'),
metabolic: yup
.number()
.typeError('半角数字を入力してください。')
.min(0,'0以上の値を入力してください。')
.required('現在の基礎代謝を入力してください。'),
targetP: yup
.number()
.typeError('半角数字を入力してください。')
.min(0,'0以上の値を入力してください。')
.max(1000,'100以下の値を入力してください。')
.required('カロリー取得のタンパク質の割合を入力してください。')
.test('targetC','タンパク質、脂質、糖質の合計が100%になるよう値を入力してください。', function() {
if(this.parent.targetC + this.parent.targetP + this.parent.targetF !== 100){
return (false)
}
return true
}),
targetF: yup
.number()
.typeError('半角数字を入力してください。')
.min(0,'0以上の値を入力してください。')
.max(1000,'100以下の値を入力してください。')
.required('カロリー取得のタンパク質の割合を入力してください。')
.test('targetC','タンパク質、脂質、糖質の合計が100%になるよう値を入力してください。', function() {
if(this.parent.targetC + this.parent.targetP + this.parent.targetF !== 100){
return (false)
}
return true
}),
targetC: yup
.number()
.typeError('半角数字を入力してください。')
.min(0,'0以上の値を入力してください。')
.max(1000,'100以下の値を入力してください。')
.required('カロリー取得のタンパク質の割合を入力してください。')
.test('targetC','タンパク質、脂質、糖質の合計が100%になるよう値を入力してください。', function() {
if(this.parent.targetC + this.parent.targetP + this.parent.targetF !== 100){
return (false)
}
return true
}),
})
export default aimSettingLayoutSchema
上記カスタムコンポーネントは以下のようなフックとプロパティを組み合わせて作成されています。
name:TextFieldで入力を行うプロパティの名前
controll:フォーム全体の状態管理を行うオブジェクト。
useFormメソッドによりし、カスタムコンポーネントに引き渡す。
TextFieldProps:TextFileldを作成する際に設定すべき値群。
useController
特定の入力フィールドに対する状態と制御のメソッドを提供するフック。
フォームの一部の入力フィールドのバリデーションとエラーメッセージの表示が容易になります。
実行結果として以下の2つのオブジェクトが返却されます。
fieldオブジェクト
以下のような入力フィールドを制御するためのプロパティが含まれています。
ref:フィールドの参照情報。TextFieldにinputRef={ref}として引き渡しています。
onChange:入力フィールドの値更新時に呼び出される関数
onBlure:入力フィールドがフォーカスを失ったときに呼び出される関数
value:入力フィールドの現在値
上記カスタムコンポーネントではref以外の値をrestとしてまとめて受け取り、TextField上でそのまま展開しています。fieldStateオブジェクト
エラーメッセージやフィールドの状態を所有する。
error:フィールドのエラー状態(バリデーションエラーを含む)
fieldState.error?.messageによりエラーがある場合のみ
メッセージを受け取ることができる。