はじめに †
- React Router 1.基礎編 の続き
- こんなアプリを作った
- New Page で、新しいページの入力フォームが表示される
- Register ボタンを押すと、記事が保存されて、表示画面に遷移する。メニューには、この記事へのリンクができる
- もう一度 New Page を押すと、別のページの入力欄が表示される
- Register ボタンを押すと、別に記事が保存される
- 表示画面で Edit リンクをクリックすると、編集画面に遷移する
- 編集画面
ここで使われている技術 †
画面遷移 | React Router |
URLパラメータの取り扱い | /article/123 で 123番の記事を表示する |
画面間でのデータ共有 | Recoil |
入力フォーム | React Hook Form |
- Recoil について
- 基本的な使い方 (Atom/Selector) は 1.基礎編で述べている
- ここでは、その応用編の AtomFamily? を使ってみる
- React Hook Form は、入力フォームの他に、エラーチェック・エラー表示についても述べる
Recoil AtomFamily? †
import {atom, atomFamily, selector, selectorFamily} from 'recoil';
// CAUTION: Don't miss spell of "key" and "default".
//
// If you incorrectly type dafault as "deault", the whole app will stop working (the browser shows white screen)
// and Recoil reports an incorrect following error message :
// "A component suspended while responding to synchronous input.
// This will cause the UI to be replaced with a loading indicator.
// To fix, updates that suspend should be wrapped with startTransition."
export const counterAtom = atom({
key: 'counterAtom',
default: 0
});
export const idsAtom = atom({
key: 'idsAtom',
default: []
});
export const articleAtom = atomFamily({
key: 'articleAtom',
default: null
});
export const artilceSelector = selectorFamily({
key: "artilceSelector",
get: (id) => ({ get }) => {
const atom = get(articleAtom(id));
return atom;
},
set: (id) => ({set}, item) => {
set(articleAtom(id), item);
set(idsAtom, ids => {
if (ids.includes(id)) {
return ids;
}
return [...ids, id];
});
}
});
export const articleListSelector = selector({
key: 'articleListSelector',
/**
* Get all articles.
* @param get a function to get atom.
*/
get: ({get}) => {
const ids = get(idsAtom);
return ids.map(id => get(articleAtom(id)));
},
/**
* Set all articles.
* @param get a function to get atom.
* @param set a function to sett atom.
* @param reset a function to reset atom.
*/
set: ({get, set, reset}, item) => {
console.log(item);
set(articleAtom(item.id), item);
set(idsAtom, ids => [...ids, item.id]);
}
});
- recoil の atomFamily は、同一型のオブジェクトを複数管理できる
- atomFamily は、articleAtom(id) のように、idをキーに、同一のオブジェクトを複数管理する
- ということで id を管理するための atom を用意するのが定石らしい
export const idsAtom = atom({
key: 'idsAtom',
default: []
});
- atomFamilyは、atomのように直接画面コンポーネントから useRecoilState?() で参照できないので、atom 一つだけを取り出す selectorFamily を作る
export const artilceSelector = selectorFamily({
key: "artilceSelector",
get: (id) => ({ get }) => {
const atom = get(articleAtom(id));
return atom;
},
set: (id) => ({set}, item) => {
set(articleAtom(id), item);
set(idsAtom, ids => {
if (ids.includes(id)) {
return ids;
}
return [...ids, id];
});
}
});
- get は、id を引数にとって、引数に get 関数を渡される関数が定義される。get を使って、atom からデータを取り出すことができる
- set は、id を引数にとって、{get, set, rest} を第一引数に、登録するオブジェクトを第二引数に取る関数が定義される
- set では、articleAtom(id) の作成/更新と、idを管理する idsAtom を更新している
- そのほかに、article の一覧を返す articleListSelector? を作った。こっちは普通の selector
React Router での URLパラメータ †
ルーティング設定 †
import './App.css';
import {RouterProvider, Route, createBrowserRouter, createRoutesFromElements} from 'react-router-dom';
import './index.css';
import BaseLayout from './BaseLayout';
import TopPage from './TopPage';
import ArticlePage from './ArticlePage';
import EditPage from './EditPage';
import AboutPage from './AboutPage';
import NotFoundPage from './NotFoundPage';
const routes = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<BaseLayout/>}>
<Route path="" element={<TopPage/>}/>
<Route path="article/:id" element={<ArticlePage/>}/>
<Route path="about" element={<AboutPage/>}/>
<Route path="edit/:id" element={<EditPage/>}/>
<Route path="*" element={<NotFoundPage/>}/>
</Route>
)
// 第二引数を省略すると React Router の URL は、サイト直下 https://example.com/article になる
// 第二引数で、ベースURLを指定すると basename下 https://example.com/myapp/article になる
//, {basename: '/myapp'}
)
function App() {
return (
<>
<RouterProvider router={routes} />
</>
);
}
export default App;
- 可変個所は <Route path="article/:id" element={<ArticlePage?/>}/> のように、:{パラメータ名} で定義する
- 任意のパラメータの場合は :id? のように ? をつける
リンクの作成 †
import {NavLink} from 'react-router-dom';
import {useRecoilValue} from "recoil";
import {counterAtom} from "./logic/state";
import {articleListSelector} from "./logic/state";
export default function MenuPage() {
const count = useRecoilValue(counterAtom);
const articleList = useRecoilValue(articleListSelector);
return (
<>
<ul>
<li><NavLink to="/">Top</NavLink></li>
<li><NavLink to="/about">About us</NavLink></li>
<li><NavLink to="/edit/0">New Page</NavLink></li>
{articleList.map(article => (
<li key={article.id}>
<NavLink to={`/article/${article.id}`}>
{article.title.substring(0, Math.min(article.title.length, 10))}
</NavLink>
</li>
))}
<li><NavLink to="/error/123">Error Page</NavLink></li>
</ul>
<p align="center">COUNTER: {count}</p>
</>
);
}
パラメータの受け取り †
import {Link, useParams} from 'react-router-dom';
import {artilceSelector} from "./logic/state";
import {useRecoilValue} from "recoil";
export default function ArticlePage () {
const {id} = useParams();
const article = useRecoilValue(artilceSelector(parseInt(id)));
return (
<>
Article #{id}. <br/>
<h1>{article.title}</h1>
<pre>{article.subject}</pre>
<br/>
<Link to={`/edit/${id}`}>Edit</Link>
</>
);
}
- const {id} = useParams(); で、URL に指定された id を取得できる
- この id をキーに artilceSelector から記事を取得する
- URLパラメータ id は String 型。このアプリでは articleFamily のキーには Number を使っているので型変換が必要
- <Link to={`/edit/${id}`}>Edit</Link> で、編集画面へのリンクを作っている
React Hook Form の入力フォーム †
import {useParams, useNavigate} from 'react-router-dom';
import {useForm} from 'react-hook-form';
import {useRecoilState, useRecoilValue} from "recoil";
import {idsAtom, artilceSelector} from "./logic/state";
export default function EditPage () {
const {id} = useParams();
const navigate = useNavigate();
const ids = useRecoilValue(idsAtom);
const newId = ('0' === id) ? Math.max(...(ids.length ? ids : [0])) + 1 : parseInt(id);
const [article, setArticle] = useRecoilState(artilceSelector(newId));
const defaultValues = {
title: article?.title ?? '',
subject: article?.subject ?? '',
};
const {register, handleSubmit, reset, formState: {errors}} = useForm({
defaultValues
});
const onSuc = (data, event) => {
const submitButton = event.nativeEvent.submitter?.name;
if ('button-cancel' === submitButton) {
onCancel();
return;
}
setArticle({
id : newId,
title: data.title,
subject: data.subject
});
navigate('/article/' + newId);
};
const onErr = (err, event) => {
const submitButton = event.nativeEvent.submitter?.name;
if ('button-cancel' === submitButton) {
onCancel();
return;
}
};
const onCancel = () => {
reset(defaultValues);
};
// MEMO: ...register() の ... は可変引数。register() は、配列を返す
return (
<form onSubmit={handleSubmit(onSuc, onErr)} noValidate>
<div>
<label htmlFor="title">TITLE:</label>
<span className="errorMsg">{errors.title?.message}</span>
<br/>
<input id="title" name="title" type="text" size="20"
{...register('title',{
required: 'titleは必須です',
maxLength: {
value: 20,
message: 'titleは20文字以内にしてください'
}
})}
/>
</div>
<div>
<label htmlFor="subject">SUBJECT:</label>
<span className="errorMsg">{errors.subject?.message}</span>
<br/>
<textarea id="subject" name="subject" rows="25" cols="80"
{...register('subject',{
required: 'subjectは必須です',
})}
/>
</div>
<div>
<button type="submit" name="button-register">Register</button>
<button type="submit" name="button-cancel">Cancel</button>
</div>
</form>
);
}
パラメータの受け取り †
- const {id} = useParams(); で、URL に指定された id を取得できる
Articleの取得/初期化 †
const ids = useRecoilValue(idsAtom);
const newId = ('0' === id) ? Math.max(...(ids.length ? ids : [0])) + 1 : parseInt(id);
const [article, setArticle] = useRecoilState(artilceSelector(newId));
- useRecoilValue?(idsAtom) で、Article の id のリストを受け取ります
- もしも パラメータとして受け取った id が 0 ならば、新規に記事を作るとみなして新たに id を採番します
- useRecoilState? で、newId 番目の Article を取得します
Reach Hook Form の初期化' †
const defaultValues = {
title: article?.title ?? '',
subject: article?.subject ?? '',
};
const {register, handleSubmit, reset, formState: {errors}} = useForm({
defaultValues
});
- 既存の Article がある場合には、その内容で defaultValues を作ります。ない場合は空文字を指定
- defaultValues を引数に useForm で Form を作ります (正確には Form を操作するインタフェースを作ります)。
- そのほかにも Form のふるまいを制御する Option 引数がある https://react-hook-form.com/docs/useform
- 返り値
register(...) | フォーム要素に紐づけるイベントハンドラを登録する |
formState | フォームの状態が格納されるオブジェクト |
watch(name) | 要素 name を監視 |
getValues(name) | 要素 name の値を取得 |
handleSubmit(onSuc, onErr) | フォームが Submit されたときのハンドラ |
reset(values) | フォームの値をリセット |
resetFiled(name) | 要素 name をリセット |
setError(name, erro) | 要素 name にエラーを強制的に設定 |
clearErrors(name) | 要素 name のエラーをクリア |
setValue(name, value) | 要素 name に値 value を設定 |
setFocus(name) | 要素 name にフォーカスを当てる |
getFiledState?(name) | 要素 name の状態を取得 isDirty/isTouched/invalid/error |
trigger(name) | 要素 name の検証を実行 |
- handleSubmit(onSuc, onErr) に指定するfunctionについて
const onSuc = (data, event) => {
const submitButton = event.nativeEvent.submitter?.name;
if ('button-cancel' === submitButton) {
onCancel();
return;
}
setArticle({
id : newId,
title: data.title,
subject: data.subject
});
navigate('/article/' + newId);
};
const onErr = (err, event) => {
const submitButton = event.nativeEvent.submitter?.name;
if ('button-cancel' === submitButton) {
onCancel();
return;
}
};
const onCancel = () => {
reset(defaultValues);
};
- onScr(data, event) は、Submit 成功時に呼ばれる
- data に、Form の内容が格納される
- event に、生のイベントが格納される
- 'Submit ボタンが複数ある場合には、event.nativeEvent.submitter?.name でイベントが発生したボタンの name 属性を得ることができる'
- このアプリでは、AtomFamily? にフォームの内容を設定して、navigate で、記事表示画面に遷移する (atom は recoil、navigate は react router の機能)
- onCancel() では、useForm の返り値の reset 関数をつかって、フォームをクリアしている
Form †
return (
<form onSubmit={handleSubmit(onSuc, onErr)} noValidate>
<div>
<label htmlFor="title">TITLE:</label>
<span className="errorMsg">{errors.title?.message}</span>
<br/>
<input id="title" name="title" type="text" size="20"
{...register('title',{
required: 'titleは必須です',
maxLength: {
value: 20,
message: 'titleは20文字以内にしてください'
}
})}
/>
</div>
<div>
<label htmlFor="subject">SUBJECT:</label>
<span className="errorMsg">{errors.subject?.message}</span>
<br/>
<textarea id="subject" name="subject" rows="25" cols="80"
{...register('subject',{
required: 'subjectは必須です',
})}
/>
</div>
<div>
<button type="submit" name="button-register">Register</button>
<button type="submit" name="button-cancel">Cancel</button>
</div>
</form>
);
- JSXで記述する。ほぼ普通の <form> と同じ
- エラー表示用に、入力フィールドの上に
<span className="errorMsg">{errors.title?.message}</span>
を設定している。入力エラー時には (errorオブジェクトができたら) 自動的に表示される。特にアプリ側から制御する必要ない。簡単!
- 入力フィールドに register 関数で、検証処理を追加する
<input id="title" name="title" type="text" size="20"
{...register('title',{
required: 'titleは必須です',
maxLength: {
value: 20,
message: 'titleは20文字以内にしてください'
}
})}
/>
- register(name [,opts])
optsに設定できる項目
value | 値 |
disabled | 入力無効(規定false) |
onChange | onChangeイベントハンドラ |
onBlur | onBlurイベントハンドラ |
required | 必須検証 {required:メッセージ} |
maxLength | 最大文字数 {maxLength:{value:値,message:メッセージ} |
minLength | 最小文字数 |
max | 最大値 |
min | 最小値 |
pattern | 正規表現 |
validate | 検証を行うためのカスタム関数 |
valueAsNumber? | 値をNumberにするか |
valueAsDate? | 値を日付にするか |
setValueAs? | 値を任意の型に変換するためのカスタム関数 |
- 動作イメージ
HTML#React