最近まで一つのサービスの開発をしていました。
その過程で最初の段階でしっかりと設計できていなかったこともあり
Propsのバケツリレーの方が多発して最初は大したことなかったものの後々大きくなるにつれてとても管理自体が大変なものになってしまいました。
(このプロダクトの反省点として現在は次に活かそうと思っています。)
その中で本格的に今まで毛嫌いしていた状態管理部分をしっかりと理解しようと思って技術選定をした結果今回のRecoilというライブラリを知ることができたので試してアウトプットとしてハンズオンしたいと思います。
今回はかなりしっかりと書いているのでわからない部分などがもしあればご連絡していただけると幸いです。適時追記します。
※ 自分自身、このライブラリはおろかフロントエンド部分の知識なども乏しい状況です。もし何かアドバイスやおかしいと思った部分などがありましたら教えていただけると幸いです🙇♂️
Starとフォローをしていただけるとより一層喜びます💭
https://github.com/soutaschool/recoil-app
このプロジェクトは Next.js + Typescript + Docker + Recoilです!
しっかりDockerさんを使って簡単に環境をセットアップしていきます。
Next.js or CRA(create react app)のどちらかで迷いましたが最近もっぱらNext.jsしか使っていないのでこっちにします!すいません。
Dockerの説明についてはここでは省きます。
ターミナル
$ mkdir recoil-app && cd recoil-app
$ touch Dockerfile
フォルダ作ってDockerfile
突っ込みます
Dockerfile
# ここはバージョンは基本的になんでもいいです。自分はなるべく軽量にしたいのでAlpineを使います
FROM node:17-alpine
WORKDIR /usr/src/app
こんな感じで書きました。
次にdocker-composeの方を作ります。
ターミナル
$ touch docker-compose.yml
docker-compose.yml
version: '3'
services:
recoil-app:
container_name: recoil-app
build:
context: .
dockerfile: Dockerfile
volumes:
- ./app:/usr/src/app
command: sh -c "yarn dev"
ports:
- '3000:3000'
自分はこんな感じで書きました!!
Next.jsのセットアップコマンドを打ちましょう!
ターミナル
$ docker-compose run --rm recoil-app yarn create next-app --typescript
するとこんなのが出てきます
✔ What is your project named? … app
と自分は設定しました。
として環境構築をすることができたら
ターミナル
$ docker-compose up -d
として
みたいに表示されたらセットアップ完了です。
早速ライブラリを入れていきましょう!
$ docker-compose run --rm recoil-app yarn add recoil
このRecoilRootというものでラッピングした部分が状態管理を扱うことができるようになると思っています。
もしかしたらアプリケーション全体のステート管理するReduxとはここで差別化を図っているかもしれません。。。(間違っていたら教えてください)
今回は簡単なアプリケーションなのでindex.tsxに直接記述していきます。
※ _app.tsxではないことに注意してください!できるとは思いますが自分の場合はここに追加するとerror - Error: This component must be used inside a <RecoilRoot> component.
が出現し、GithubのIssueやドキュメントなども読み返してもあっているはずなのですが今回は自分の方ではIndexの方に追加とさせていただきます。
(もし原因わかる方いましたら教えてください〜〜!)
index.tsx
import { NextPage } from 'next';
import { RecoilRoot } from 'recoil';
interface RootProps {}
const Root: NextPage<RootProps> = () => {
return (
<RecoilRoot>
{/* ここの部分にコンポーネントを追加していく予定です! */}
</RecoilRoot>
);
};
export default Root;
これでRecoilRootの追加完了です。
次に、pagesフォルダとstylesフォルダを$ cd app && mkdir src
を実行してsrcファイル内に移動してください!!
srcファイル内はこんな感じにあるはずです。
※ わからなそうなら上記に貼ってあるGithubを見てみてください!
.
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── hello.ts
│ └── index.tsx
└── styles
├── Home.module.css
└── globals.css
次にsrcディレクトリで$ touch types.ts
を実行してみてください!
このファイルは定義する型の集合ファイルです。
types.ts
export type Task = {
title: string;
content: string;
};
export type Tasks = Task[];
こんな感じで定義できると思います。
次にsrcディレクトリで$ touch atoms.ts
を実行してみてください!
このファイルは定義するatomの集合ファイルです。
公式ドキュメント: https://recoiljs.org/docs/basic-tutorial/atoms
自分自身reduxを深く触ったことがないのでどれに似ているとかはわかりません。
例えがなくてごめんなさい!
ただ初期値をファイルにまとめるあたりReduxのStoreでの定義に似ているような気はします。
実際の使い方を踏まえて作成したファイルに記述しましょう!
atoms.ts
import { atom } from 'recoil';
import { Tasks } from './types';
// ここの部分でTypescriptならユニオンにできます。
export const taskListAtom = atom<Tasks>({
// ここの部分はユニークなキーになることが求められます。
key: 'TaskList',
// ここの部分が初期値にあたります。
// 例: const [hoge, setHoge] = useState() <- ここです!
default: [
{
title: '夜ご飯を買いに行く',
content: '二郎ラーメンでニンニクマシマシアブラカラメ',
},
],
});
export const taskTitleFormAtom = atom<string>({
key: 'TaskTitleForm',
default: '',
});
export const taskContentFormAtom = atom<string>({
key: 'TaskContentForm',
default: '',
});
こんな感じで自分は書きました。
次にsrcディレクトリで$ touch selectors.ts
を実行してみてください!
このファイルは定義するselectorの集合ファイルです。
公式ドキュメント: https://recoiljs.org/docs/basic-tutorial/selectors
実際にコンポーネント側に渡すのがSelectorを渡すので
ここの部分で値をどのように整形したいかなどの処理を記述しておく必要があると思っています。
(自分が勉強してて思ったことです!間違っていたら教えていただけると幸いです😭)
実際にここで分岐分だったり値の追加や変更だったりができるのでしっかりとキャッチアップする必要があると思いました。
selectors.ts
import { selector } from 'recoil';
import { taskListAtom } from './atoms';
import { Tasks } from './types';
// ここの部分でTypescriptならユニオンにできます。
export const taskListSelector = selector<Tasks>({
// ここの部分はユニークなキーになることが求められます。
key: 'TaskListSelector',
// getを受け取る関数になっています
get: ({ get }) => {
// 先ほどAtomsの方で定義したものを取得しています。
const taskList: Tasks = get(taskListAtom);
// 値を返さないと許してくれません
return taskList;
},
});
ちょっとだけおしゃれにします!
最近自分自身めちゃめちゃ難しいと思っているライブラリです。
今回はせっかくなので使っていきます。
公式サイト: https://mui.com
ターミナル
$ docker-compose run --rm recoil-app yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material
こちらの方をインストールしてみてください!
次にsrcディレクトリで$ mkdir components && cd components && touch TaskList.tsx
を実行してみてください!
TaskList.tsx
import CheckIcon from '@mui/icons-material/Check';
import {
Avatar,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from '@mui/material';
import { NextPage } from 'next';
import { useRecoilValue } from 'recoil';
import { taskListSelector } from '../selectors';
import { Task, Tasks } from '../types';
interface TasKListProps {}
const TasKList: NextPage<TasKListProps> = () => {
const taskList: Tasks = useRecoilValue<Tasks>(taskListSelector);
return (
<>
<p>残っているタスク一覧</p>
<List sx={{ width: '100%', maxWidth: 720, bgcolor: 'background.paper' }}>
{taskList.map((task: Task, index: number) => (
<ListItem key={`${task.title}_${index}`}>
<ListItemAvatar>
<Avatar>
<CheckIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={task.title} secondary={task.content} />
</ListItem>
))}
</List>
</>
);
};
export default TasKList;
公式ドキュメント: https://recoiljs.org/docs/api-reference/core/useRecoilValue
このフックはデータの読み取りのみの場合に使用するフックとなっています。
もし変更があった際にコンポーネントを再レンダリングする作用を持っていて
読み取り専用と書き込み専用の両方に対応しているのが特徴です!!
次にsrcディレクトリで$ mkdir components && cd components && touch TaskForm.tsx
を実行してみてください!
TaskFormtsx
import { FormControl, FormHelperText, Input, InputLabel } from '@mui/material';
import { NextPage } from 'next';
import { ChangeEvent, useCallback } from 'react';
import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil';
import { taskContentFormAtom, taskTitleFormAtom } from '../atoms';
interface TaskFormProps {}
const TaskForm: NextPage<TaskFormProps> = () => {
// stateの値を取得
const taskTitleValue: string = useRecoilValue<string>(taskTitleFormAtom);
const taskContentValue: string = useRecoilValue<string>(taskContentFormAtom);
// setStateの部分の処理
const setTaskTitleValue: SetterOrUpdater<string> =
useSetRecoilState<string>(taskTitleFormAtom);
const setTaskContentValue: SetterOrUpdater<string> =
useSetRecoilState<string>(taskContentFormAtom);
// 値の変更処理
const onChangeFormTitleValue = useCallback(
(event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setTaskTitleValue(event.target.value);
},
[setTaskTitleValue]
);
const onChangeFormContentValue = useCallback(
(event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setTaskContentValue(event.target.value);
},
[setTaskContentValue]
);
return (
<>
<FormControl>
<InputLabel htmlFor="my-input">タスク名</InputLabel>
<Input
id="my-input"
aria-describedby="my-helper-text"
type="text"
value={taskTitleValue}
onChange={(event) => onChangeFormTitleValue(event)}
placeholder="自動販売機に行く"
/>
<FormHelperText id="my-helper-text">
項目を記述してください!
</FormHelperText>
</FormControl>{' '}
<br />
<FormControl>
<InputLabel htmlFor="my-input">内容</InputLabel>
<Input
id="my-input"
aria-describedby="my-helper-text"
type="text"
value={taskContentValue}
onChange={(event) => onChangeFormContentValue(event)}
placeholder="今日の気分はコカコーラ"
/>
<FormHelperText id="my-helper-text">
内容を記述してください!
</FormHelperText>
</FormControl>
</>
);
};
export default TaskForm;
公式ドキュメント: https://recoiljs.org/docs/api-reference/core/useSetRecoilState
このフックはデータの読み取りを行わずに状態に書き込む場合に使用するフックとなっています。
Reactのステート管理に触れたことがある人なら共感いただけると思いますが
簡単にいうとSetState関数の部分を作成するためのフックと自分の方は思っています。(間違っていたらアドバイスください!)
こちらもアトムまたはセレクターが更新されたときに役立つものが多いのでわかりやすい仕様になっていると思います。
次にsrcディレクトリで$ mkdir components && cd components && touch Task Button.tsx
を実行してみてください!
TaskButton.tsx
import { Button } from '@mui/material';
import { NextPage } from 'next';
import { useCallback } from 'react';
import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil';
import { taskContentFormAtom, taskListAtom, taskTitleFormAtom } from '../atoms';
import { Tasks } from '../types';
interface TaskButtonProps {}
const TaskButton: NextPage<TaskButtonProps> = () => {
const taskList: Tasks = useRecoilValue<Tasks>(taskListAtom);
const taskTitleFormValue: string = useRecoilValue<string>(taskTitleFormAtom);
const taskContentFormValue: string =
useRecoilValue<string>(taskContentFormAtom);
const setTaskList: SetterOrUpdater<Tasks> =
useSetRecoilState<Tasks>(taskListAtom);
const setTaskTitleFormValue: SetterOrUpdater<string> =
useSetRecoilState<string>(taskTitleFormAtom);
const setTaskContentFormValue: SetterOrUpdater<string> =
useSetRecoilState<string>(taskContentFormAtom);
const onClickFormValue = useCallback(() => {
// スプレッド構文を用いて追加する
setTaskList([
...taskList,
{ title: taskTitleFormValue, content: taskContentFormValue },
]);
// 値を初期化
setTaskTitleFormValue('');
setTaskContentFormValue('');
}, [
setTaskList,
setTaskContentFormValue,
taskList,
setTaskTitleFormValue,
taskContentFormValue,
taskTitleFormValue,
]);
return <Button onClick={() => onClickFormValue()}>タスクを追加する</Button>;
};
export default TaskButton;
ここの部分は特に解説することはありません!!!
今までの復習だと思います。
ここまで本当にお疲れ様でした!
かなりのコード量になったと思いますが最後に今までのコンポーネントを結合しましょう。(これが楽しい)
pages/index.tsx
import { NextPage } from 'next';
import { RecoilRoot } from 'recoil';
import TaskButton from '../components/TaskButton';
import TaskForm from '../components/TaskForm';
import TasKList from '../components/TaskList';
interface RootProps {}
const Root: NextPage<RootProps> = () => {
return (
<RecoilRoot>
<TaskForm />
<TasKList />
<TaskButton />
</RecoilRoot>
);
};
export default Root;
こんな感じになると思います!!!!!
完成系の画面としては
こんな感じのUIのものになっていると思います。
実はこれ以外に使いやすいものになっていてデザイン的にも悪くないと思うのでもしよかったら使ってみてください!
(Stateなのでリロードでそこは注意✋)
もし何かありましたら上記の方にじぶんのGithubまたはDMなのでもいいので何なりと言ってください!!
まずかなりAPIリファレンスがドキュメントとして完成されているように感じました。(日本語訳でもしっかり読めた!?)
やはりPropsリレーにならないという恩恵が今回の学習を通して自分自身は感じることができました。。。
値の変更やコンポーネントが増えていけばいくほど便利になるとあたらめて実感することができたと思います!!
TypeScriptなら型もしっかりと定義することができるので安心して開発をすることができるだけではなく開発体験が非常に向上するので相性がいいと思いました。
しかし、自分がまだまだ簡略化できていない部分が多いですが、無駄な部分も多いと思うのでさらにコードの質も高めていければと思います。
ついでにですが、React Hook Formというライブラリがあるのですがこれめっちゃフォーム作るときにはバリデーションとかもかけてくれるんで便利です。(使えばよかった笑)
今までプロダクトの大きさ問わず状態管理ライブラリというものを置き去りにしてきた部分がとてもあるのでしっかりとこれからはキャッチアップしてできる部分はアウトプットしていこうと思います。
自分自身現在プロダクトを作っていてまだまだ至らない部分も多いですがRedux含めRecoilなのかどうかという部分をしっかりと技術選定して導入していきたいです。
(もちろん今はバケツリレー状態になっています💦)
また、もしよければSNSも最近始めたのでフォローしていただけると幸いです。
是非是非質問やアドバイスなどもありましたらお願い申し上げます。。。