# フロントエンド再構成
目的:各サービスごとに独立して開発が行えるようなフロントエンド開発の基盤を整える
[TOC]
## 現在のディレクトリ構成と問題点
```
- src
- components
- items
- organisms
- templates ... ページ追加のたびに要修正
- pages ... 各ページに実装が集中
- utils
- App.tsx
- App.css
```
## 再構成の方針① - Atomic Design
> 参考ページ
> - [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/)
> - [React で実装する atomic design のコンポーネントごとの責務の分け方とコード規約](https://qiita.com/takano-h/items/8731d8e7413d7b1f6d7b)
> - [10分で説明するAtomic Design と、弊チームで扱うためのカスタマイズについて](https://qiita.com/putan/items/ec312314698087fca5b2)
### Atomic Design とは


- 部品の組み合わせでUIを作成
- グループに分類し、それらの依存関係を一方向に(上図)
- 自分より左のレイヤーのみに依存
- 修正したレイヤーより左のレイヤーには影響が出ない
- **依存方向は、必ず守ること!!**
> We’re not designing pages, we’re designing systems of components.—Stephen Hay
### Atomic Design のイイコト
- コンポーネントごとにしっかり分かれていて、UIの機能の再利用性が高い
- 多くの画面を少ないコードで実装
- 画面別ではなく、機能別にUI設計ができる
- 複数人で並行実装ができる
- 開発速度がUP!
- 効率良くテストができる
### 各グループの役割
※IT:ITエンジニアチーム内での共通認識
### pages

- templatesにデータ(from API?)を流し込み、ページを動かす
- 1ページ1ファイル
- IT:各プロジェクトが管理する単位
#### templates

- atoms,molecules,organismsを配置する
- プレイスホルダー(コンテンツがない)だけのスケルトンの状態
- レイアウトを担保する
- IT:全プロジェクトで共有
#### organisms

- コンテンツが完結しているもの
- atoms,molecules,organismsを組み合わせて構成
- 独立して機能し、他のページでも同じ目的で使える
#### molecules

- atomsを組み合わせた部品
- 最低限の意味をつけるもの
- 文字を入れる+クリックする=検索する
- 独立して存在はできない
- 操作性を担保
### atoms

- HTMLのタグ単位(ex.label,input,button)
- Button Atomは、`<button>`に対応
- UIは含むが、動作/ロジックは含まない
- OK:Color,fonts,animations
- NG:Atomの組み合わせ、他componentと依存関係
- 基本デザインを担保
### 各グループの境界
- pages vs templates
- 実際のデータに、依存する(pages)/依存しない(templates)
- templates vs organisms
- 配置を自由に、変えられる(templates)/変えられない(organisms)
- organisms vs molecules
- 組み合わせによって意味/意図が、変わる(molecules)/変わらない(organisms)
- molecules vs atoms
- 単独コンポーネント(atoms)/複数組み合わせコンポーネント(molecules)
## 再構成の方針② - Recoil
> 参考ページ
> - [Recoilで快適フロントエンド開発 - Nulab](https://nulab.com/ja/blog/nulab/recoil-example/)
> - [Recoilで始めるお手軽フロントエンドDDD](https://zenn.dev/susiyaki/articles/95dea88e673e1f854130)
> - [ReactのState管理を比較してみた](https://qiita.com/cheez921/items/7c5f82da375a5988a179)
> - [2021年に活用していきたいReactの状態管理ライブラリRecoil](https://tech.aptpod.co.jp/entry/2020/12/19/100000)
### Recoil とは
- Meta(元Facebook)が開発中の状態管理ライブラリ
- ざっくり言うと、Reactの`useState`みたいな状態保持hookをコンポーネントをまたいで使える
- propsのバケツリレーをせずに、コンポーネントの階層に関係なくstateを子コンポーネントに伝えることができる
### Recoil 用語集
- Atom
- グローバルな状態変数`RecoilState`を生成
- `key`:アプリ全体でユニークな値、keyでatomを判別
- `default`:初期値
```typescript=
const counterState = atom<number>({
key: "counterState",
default: 0
});
```
- Selector
- Atomで生成されたRecoilStateを、使いやす形に加工して取得するための関数
```typescript=
// /src/hooks/RecoilAtom.tsx ※実際は、AtomFamilyを使う
const tempFahrenheit = atom({
key: 'tempFahrenheit',
default: 32,
});
// /src/hooks/RecoilSelector.tsx
const tempCelcius = selector({
key: 'tempCelcius',
get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9,
set: ({set}, newValue) =>
set(
tempFahrenheit,
newValue instanceof DefaultValue ? newValue : (newValue * 9) / 5 + 32
),
});
```
- AtomFamily
- 動的にatomを作成してくれる関数
- 同じatomFamilyを用いて、同じ型を持つ別の値の読み取り・書き込みができる
- いちいちatomからRecoilStateを生成する手間を解決
- RecoilStateを取得する際に、パラメータをatomFamilyに渡す
```typescript=
// /src/hooks/RecoilAtom.tsx
import React from "react";
import { useRecoilState, atomFamily } from "recoil";
const inputTextState = atomFamily<string, string>({
key: "inputTextState",
default: ""
});
// 各コンポーネントにて
export const InputFamily = () => {
// もしパラメータを同じものにすると変数名が違っても、同じ値になる
const [userName, setUserName] = useRecoilState(inputTextState("userName"));
const [password, setPassword] = useRecoilState(inputTextState("password"));
return (
<>
<input value={userName} onChange={(e) => setUserName(e.target.value)} />
<input value={password} onChange={(e) => setPassword(e.target.value)} />
<p>{userName}</p>
<p>{password}</p>
</>
);
};
```
- SelectorFamily
- selectorに引数を渡して条件分岐をしたい時に使用
- idで指定したものを呼び出す
- RecoilをsubscribeするHooks
1. `useRecoilState` 「値」と「更新するための関数」両方必要な場合
2. `useRecoilValue` 「値」だけ必要な場合
3. `useSetRecoilState` 「更新する関数」だけ必要な場合
```typescript=
// 1. useStateと同じ
const [count, setCount] = useRecoilState(counterState);
// 2. useStateからstateだけを取り出したもの
const count = useRecoilValue(counterState);
// 3. useStateからsetStateだけを取り出したもの
const setCount = useSetRecoilState(counterState)
```
- useRecoilCallback
- `snapshot`:Recoilの現在のStateを読み取れるオブジェクト
- snapshotを用いて必要な時にだけStateを読み込み発火する
- 不要な再レンダリングを防げる(`useCallback`と同様)
### Recoilによる状態管理のルール
#### 1. atom key, selector keyを一元管理する
- Recoilでは、keyをアプリ全体でユニークにする必要
- 将来的に状態の数が増えても、この制約が守られるように、RecoilKeys.tsxにenumを作成してkeyを管理
```typescript=
// RecoilKeys.ts
export enum RecoilAtomKeys {
TODO_STATE = 'todoState',
NOTICE_STATE = 'noticeState'
}
export enum RecoilSelectorKeys {
TODO_TODOS = 'Todo_todos',
TODO_TODO_ITEM = 'Todo_todoItem',
NOTICE_HAS_UNREAD_NOTICE = 'Notice_hasUnreadNotice'
}
```
```typescript=
// todoState.ts
import { atom } from 'recoil'
import { RecoilAtomKeys, RecoilSelectorKeys } from './RecoilKeys'
type TodoItem = {
id: string
label: string
}
type TodoState = {
todos: TodoItem[]
}
const todoState = atom<TodoState>({
key: RecoilAtomKeys.TODO_STATE,
default: {
todos: []
}
})
```
#### 2. atomやselectorをそのままexportしない
- 状態設計者の意図しない状態変更を防止
- 用途に応じたカスタムフックのみをexport
- `useAddRows`:日報列を追加する
- `useRows`:日報列を全て呼び出す
- `useDeleteRow`:idを指定して日報列を削除する
```typescript=
// todoState.tsx
import { useRecoilCallback } from 'recoil'
// Actions
type TodoActions = {
useAddTodoItem: () => (label: string) => void
}
export const todoActions: TodoActions = {
// Todoを追加する
useAddTodoItem: () =>
useRecoilCallback(({ set }) => (label: string) => {
set(todoState, (prev) => {
const newItem: TodoItem = {
id: createNewId(),
label
}
return {
...prev,
todos: [...prev.todos, newItem]
}
})
}, [createNewId]),
}
// Selectors
type TodoSelectors = {
useTodos: () => TodoItem[]
useTodoItem: (id: string) => TodoItem | undefined
}
const todosSelector = selector<TodoItem[]>({
key: RecoilSelectorKeys.TODO_TODOS,
get: ({ get }) => get(todoState).todos
})
const todoItemSelector = selectorFamily<TodoItem | undefined, string>({
key: RecoilSelectorKeys.TODO_TODO_ITEM,
get: (id) => ({ get }) => {
const todos = get(todoState).todos
return todos.find((v) => v.id === id)
}
})
export const todoSelectors: TodoSelectors = {
useTodos: () => useRecoilValue(todosSelector),
useTodoItem: (id: string) => useRecoilValue(todoItemSelector(id))
}
```
## 参考になりそうなGithubリポジトリ
- https://github.com/tockri/recoil-sample-for-nulab-blog
- styles/
- https://github.com/susiyaki/recoil-frontend-ddd-sample
- src/hooks/
## 再構成後のディレクトリ構成
- public
- src
- App.tsx
- components
- pages ... 現状のページ構成
- LoginPage.tsx
- HomePage.tsx
- ...
- templates
- AppBar.tsx
- AdposSearchResult.tsx ... 答案検索結果の表形式表示
-
- organisms
- BacklogApiCertification.tsx
- UplaodByCsv.tsx
- AdposSheetDownload.tsx
- molecules
- Dialog.tsx
- DragDrop.tsx
- Pagination.tsx
- atoms
- Icons.tsx
- Image.tsx
- constants
- Constants.tsx
- ConstantsNippo.tsx
- api ... APIを叩く部分をwrap,axios/fetchはここでのみ使うイメージ?
- AdposApi.tsx ... ADPOS APIを叩く部分wrap
- BacklogApi.tsx ... Backlog APIを叩く部分wrap
- SlackApi.tsx ... Slack APIを叩く部分wrap
- hooks ... utilsをまとめる
- recoil ... Recoil State関連
- RecoilKeys.tsx ... RecoilのKey管理
- AdposState.tsx ... AdposのRecoilState管理
- BacklogState.tsx ... BacklogのRecoilState管理
- UseHandler.tsx
- UseTracking.tsx
- LocalStorage.tsx
- FormatDate.tsx
- styles ... 見た目の定義
- GlobalStylessty.tsx ... テーマ色の定義など全体に反映(今はGenericTemplateにある)
-
- routes ... path/pageの対応を定義
- Routes.tsx ... App.tsxからRoute部分を切り出す
## memo
### 命名法則:BCD Design
- コンポーネント名に使用される単語の意味や性質を相対的に利用することで、 コンポーネントを Base Case Domain の3つの概念へ法則的に配置し、体系的に管理できるようにする分類手法
- https://qiita.com/misuken/items/19f9f603ab165e228fe1
- Atomic Design : 設計手法
- BCD Design : 分類手法
- Base - 基礎 (名詞)
- 基礎的な機能(名詞)そのもの、事実上の “型” を表す単語のことを指します。
- Case - 状況 (動詞/形容詞/名詞)
- 状況(動詞/形容詞)や状態(名詞)を表す単語のことを指します。
- Domain - 関心 (名詞)
- 人(ロール)や物(名詞)など "関心の対象" を表す単語のことを指します。

- コンポーネント名を突き詰めていくと、以下の4つのパターンに

### importを絶対パスで指定したい
- https://zenn.dev/nbr41to/articles/84f4a7a7c1c165af2ef7
- `src`を絶対パスのbaseUrlに指定
### recoilを使っている某会社のめも(口外禁止)
recoil使うコツ
- ReactのComponentから読むselector/atomは1つまでにする
- Component専用のselectorを用意するのは大いにあり。そのselectorでいろいろなselectorを読んだり、変形する。
- selectorの中にロジックの大部分をいれる
- for文やforEachはNG。ループの中を別selectorにしてwaitForAllする
- 並列化
- 再計算も最小化される
- apiもselectorでラップするとよい
- dataloaderを組み合わせるのも可
- apiを組み合わせたり変形するselectorもあってよい
- snapshotを使うことに臆病にならない。subscriptionをあえてしたくないときはある
- キャッシュの効きにくさ的に配列をパラメーターにするのはやめたほうがいい。user_ids[]みたいなのはNG。 [client_id, user_id]のようなタプルはOK
- キャッシュが効くので同じselectorを同じパラメーターでなんども読むのはなんの問題もない
- キャッシュがメモリ上にたまるのは注意が必要。キャッシュ戦略をいじるべき場合もある
- データ更新系はrecoilを使わないほうがおそらくいい。データ更新後にrefreshをかける。(selectorでできなくはないけど・・だいぶ書き心地も保守性も悪い感じになる)
### めも(平野)
- recoilででてくるinstance of DefalutValueとは
- https://scrapbox.io/study-react/DefaultValue
- 要はreset用のお約束的な記述
- [リンク先](https://github.com/tockri/recoil-sample-for-nulab-blog/blob/main/src/components/pokemon/pokemon-api.ts)の80行目でthenの中にasync使ってるのなぜ
- https://teratail.com/questions/357415
- response.json()がPromiseを返すから
- 復習用
- https://qiita.com/niusounds/items/37c1f9b021b62194e077
- https://qiita.com/jun1s/items/ecf6965398e00b246249
- https://medium.com/acompany/javascript%E3%81%AEasync-await%E3%82%92%E5%AE%8C%E5%85%A8%E3%81%AB%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B-6552337eb92
- 盲目な相手に極限までわかりやすく解説していたのであろうコメント
- https://qiita.com/rana_kualu/items/e6c5c0e4f60b0d18799d#comment-97fdd4d668bcac29972e
- awaitがだめな例
- https://okapies.hateblo.jp/entry/2020/12/13/154311
- 例えば非同期関数でresolveする予定の値を用いて後続の処理で使うとき、その処理が2つ以上だとawaitにしてしまうと並列処理ができない
- 1.imageUrlを取ってくる
- 2.取ってきたimageUrlをキャッシュする
- 3.取ってきたimageUrlからimageを取ってくる
- この場合はimageUrlが返す生のPromiseを用いて2.3.の処理をthenでつなぐようにすると2.3.が並列で実行される
- Promise.allとか使えばよいではないか?
- https://qiita.com/okazuki/items/2c17cd509f86c8b13a15
- ↑反論の記事。特にコメントで「小学生か」と罵られている
- ユーザーエクスペリエンスもちゃんと考えたい
- https://qiita.com/teradonburi/items/5b8f79d26e1b319ac44f
- 具体的には以下の様なことを考えて処理速度の基準を設けたい
```
0~16 ミリ秒: とても良い、ゲームとかでも60FPS(1フレームあたりの描画間隔が16ミリ秒)とかになっていますね
0~100 ミリ秒: ページ内動作としては許容範囲
100~300 ミリ秒: ページ内動作のレスポンスとしてはやや遅い
300~1000 ミリ秒: ページの読み込み単位であればスムーズな体験を提供している
1000 ミリ秒以上: 1秒を超えるとユーザは実行したタスクへの関心を失う
10,000 ミリ秒以上: ユーザーは不満を感じてタスクを中断し、そのまま戻ってこない恐れがあります
```
- カスタムフックではuseCallbackを使っていこうね、という話
- https://blog.uhy.ooo/entry/2021-02-23/usecallback-custom-hooks/
- useCallbackはコンポーネントが再レンダリングされたときも更新されない(useEffectみたいにとある変数が更新されたら更新するみたいな設定ができる)ため、例えばuseMemoを使うような重たいコンポーネント内で呼び出されてた際に毎回更新してしまうようなことが防げる
- 簡単な言葉で言い直せば、結局のところ「返り値の関数はuseCallbackで囲んだほうがカスタムフックの汎用性が高くなるからそうしろ」ということです。 場合によってはそのuseCallbackが無駄になるかもしれませんが、観測できるかどうかも分からないオーバーヘッドよりは設計上の要請のほうを優先したいというのが筆者の考えです。(引用)
- 要は、全く同じ関数なら「変更した」とプログラムに捉えられないようにしたほうが、余分な再レンダリングを防げるという話。
- 今後責任範囲を切り分けていく上で必要な話かも。
- 参考
- https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2
- Recoilもその辺最適化されているらしい
- https://unblog.unreact.jp/blog/2n7qgaa-k
- React18ではレンダリングが非同期化(現在LIMEはReact17)
- https://reactjs.org/blog/2022/03/29/react-v18.html
- https://www.infoq.com/jp/news/2022/04/react-18-concurrent-renderer/
- recoilもこのための布石?
- https://www.kwbtblog.com/entry/2020/11/07/032250
- 画面が固まって操作できないみたいな状況に陥らない+レンダリングの中断もできる
- ユーザーエクスペリエンス超向上
- ただ、まだ記事が少ない
### めも(大鐘)
- Reactのtest
- 本当はリファクタリング前にやるべきでしたね。。。
- Jestが、typescriptで一般的、ReactでもOKっぽい
- https://zenn.dev/296u/articles/7175641f1c4492
- そのままLIMEのCI/CDを一気に進めちゃいたい
- https://note.com/tabelog_frontend/n/n4b8bcb44294c
### TODO
- [ ] API切り出し
- [x] NippoAPI
- [x] BacklogAPI
- [ ] ADPOSAPI
- [ ] style
- [ ] component
- [ ] 新規機能
- [ ] 答案検索結果集計の改良
- [ ] 日報、1日のメモを編集可能に
- [ ] 問い合わせ・要望の状況表示