generated at
フロントエンドワークショップ React入門ハンズオン

Reactで仕訳入力画面を作っていきます
コードは基本的には簡単な方法を選択したものになってるはず…
一部pastakのクセみたいなのがあるかもですが、まぁその辺は良い感じにお願いします
https://github.com/pastak/mf-frontend-workshop に順を追ったcommitが一応あります
カンニングに使ってください
コード内が Subject ってなってますが、資料では Item に変更してます

コード書く前にReactについておさらい
React Componentは関数の形で記述する
JSXという記法でHTMLやReact Componentを記述する
TypeScriptで記述する際はファイルは .tsx 拡張子にする
基本的にはPropsやStateが変わると再レンダリングが走る
状態は useState を用いて扱う。その返り値が [getter, setter] になっている。
use*** はフックと呼ばれる特別な関数
名前は use から始める
関数のトップレベルで呼ぶ
if for の中などで呼ばない
early returnする前に必ず呼ぶ
これらはeslint-plugin-react-hooksで検出してくれるように出来る

仕訳入力画面について
左側に「借方勘定科目」と「金額」の入力欄がある
右側に「貸方勘定科目」と「金額」の入力欄がある
それぞれの「科目」の項目はAPIからJSONで取得する
金額はユーザーが数字を入力する
左右それぞれの合計を最後の行に表示する
そのとき、左右が一致して無ければその旨を表示する

こういう感じになる予定という図
<Journal /> のstateとして data[] を持っておいて、それを更新したり、各Componentに配る形で素朴にアプリケーションの状態を管理する

Create React Appでの環境セットアップ
Create React App(以下、craと省略することがあります)はReactの開発環境を一発でバシッと作ってくれるツール
継続的に改善がされていて、その時々のベストプラクティス集的な感じにもなっている
npm init react-app -- . --template typescript
npm init foo npm exec create-foo になる https://docs.npmjs.com/cli/v8/commands/npm-init
npm start で localhost:3000で起動
自動でリロードとかもしてくれて便利
npm run test でテスト実行
何が中で動いてるかとかの詳細は次のパートでやるので、ここでは一旦便利に全部動くぞという世界観で進めていきます

自動リロードの様子を眺める
npm start でサーバーとブラウザが立ち上がる
src/App.tsx を編集するとブラウザが自動で再読み込みされて変更が適応されていることを確認
便利

まず全体で利用することになる型を書いておく
src/types/journal.ts に置いていく
各行のデータとそれの配列があれば良さそうということでざっくりこんな感じ
src/types/journal.ts
export type JournalRow = Readonly<{ debitItem: string, debitValue: number creditItem: string, creditValue: number, }> export type JournalData = Readonly<JournalRow[]>

<Row> のインターフェイスを作っていく
<Row> は各行のデータを表示/更新する機能を備えている
Propsを考えてみる
JournalRow を受け取る
親の持っている状態を自身の操作で更新する update 関数
ひとまず素朴に (newData) => void くらいでどうか
src/Row.tsx
type RowProps = Readonly<{ data: JournalRow update: (newData: JournalRow) => void }> export const Row: React.VFC<RowProps> = () => null; //一旦中身無いので、nullを返しておく
React Componentで何もレンダリングしない時は null を明示的に返す
React.FC には children が最初から定義されているが、これが原因でchildrenが本当に必要かどうかが型から分からない問題を解決するために React.VFC という children の無い型があるので、基本はこれを使っていく。
将来的には React.FC からも children が削除される予定なので、その際には VFC FC に置換できる予定
コラム: Function Componentの書き方・宣言方法について
export function Row {} という書き方も出来たり、 React.VFC は無くても良いという説もあります。実際、craでも#8177 React.FC が削除されています。( src/App.tsx を見ると function App() で定義され、 export default App とdefault exportされていることが分かります)
ですが、今回は明示的に型を書いていく方法を取っていきます。このハンズオンでは型を書くことに慣れたり、それによりコードを書いていく手助けになるという考えからです。

<Row> のJSXを書く
まずは素朴にJSX部分から書いてみましょう
必要そうな要素を並べる
src/Row.tsx
<div> <select>{/* ここに課目が並ぶ */}</select> <input type='number' /> <select>{/* ここに課目が並ぶ */}</select> <input type='number' /> </div>

<Journal> を作って試しに表示させてみる
見た目を作ったので一旦表示させてみたくないですか?ということで表示させるために <Journal> を雑に作って <App> に置いて実際にレンダリングされるようにする
src/Journal.tsx
import { useState } from "react" import { Row } from "./Row"; import { JournalData } from "./types/journal" export const Journal = () => { const [data, setData] = useState<JournalData>([]); return <>{ data.map((d) => <Row data={d} update={() => {/* 一旦空の関数を渡してごまかす */}} />) }</> }
<> <React.Fragment> の省略形
React Componentは JSX.Element[] のような配列ではなく、1つのElementに囲われている形である必要があるが、 <div> などで囲いたくない場合等に使用する
このままだと配列が空なので、適当に初期データを生成しておけるようにしておく
ts
const createInitialData = () => ({ debitItem: '', debitValue: 0, creditItem: '', creditValue: 0, });
diff
- const [data, setData] = useState<JournalData>([]); + const [data, setData] = useState<JournalData>([createInitialData()]);
そして <App> <Journal> をマウントさせたら、一度 npm start してみましょう
src/App.tsx.diff
<div className="App"> - <header className="App-header"> - <img src={logo} className="App-logo" alt="logo" /> - <p> - Edit <code>src/App.tsx</code> and save to reload. - </p> - <a - className="App-link" - href="https://reactjs.org" - target="_blank" - rel="noopener noreferrer" - > - Learn React - </a> - </header> + <Journal /> </div>
表示されましたか 🎉🎉🎉

<Journal>で行を追加出来るようにする
<button> を設置して行を追加できるようにする
src/Journal.tsx.diff
export const Journal = () => { const [data, setData] = useState<JournalData>([createInitialData()]); + const addRow = () => { + setData((prev) => [...prev, createInitialData()]) + } + return <>{ data.map((d) => <Row data={d} update={() => {/* 一旦空の関数を渡してごまかす */}} />) - }</> + } + <button onClick={addRow}>行を追加</button> + </> }
setState setter (ここでは setData )は、更新用の値を渡すことも出来るし、関数を渡すことで現在の値から次の状態を生成することも出来る
ここで一旦Devtoolのconsoleを見てみてください
> Warning: Each child in a list should have a unique "key" prop.
map を使って要素をリストするときには key を使ってユニークであることを表現する必要がある
keyを使うことでリスト内の要素の順番などが変更された場合にも要素がどのように対応しているかが検証され、更新される範囲を最小限に出来る
このとき配列のindexをkeyに使うと酷い目にあうケースがあるので注意
というわけで今回はuuidを与えておきます

JournalDataにidを追加する
JournalRow id: string を追加する
uuidの生成には Crypto.randomUUID() を使う
src/Journal.tsx.diff
const createInitialData = () => ({ + id: crypto.randomUUID(), debitItem: '', debitValue: 0, creditItem: '',
このときTSの型エラーが出る
何故なら、tscの持っている型情報には crypto.randomUUID() がまだ追加されていないので手元で足してあげる必要がある
src/types/global.d.ts
declare interface Crypto { randomUUID: () => string; }
*.d.ts は型定義宣言が書いてあるファイル
VSCodeをリロードすると読み込まれるはず
declare で型推論器にだけ情報を渡す
コラム: ファイルの置き場について src/types 以下に置いてますが、アプリケーション向けの型などの実装と混ざって微妙な気持ちになるので、 types/ などに置いておいて、 tsconfig.json を編集する方が素直で良いかも(今回は変更を最小限にするためにここに置いてます)

idを使ってkeyを設定する
<Row> に値を渡すところで key={d.id} も指定する
JournalRowの型にidを足しておくこと
Warningが消えていることを確認する

行の追加をテストする
craにはReact ComponentのテストのためにJestReact Testing Libraryが入っている
src/App.test.tsx を見ると雰囲気掴める?
一方で、Appの中身は変更していて今は用をなさないので削除しておく
というわけで、ボタンを押したら行が追加される様子をテストしてみましょう
src/Journal.test.tsx を作成してテストを書いていく
src/Journal.test.tsx
import { screen, render, fireEvent } from "@testing-library/react"; import {Journal} from "./Journal"; test('「行を追加」ボタンをクリックすると1行増える', () => { render(<Journal />); const button = screen.getByText('行を追加'); fireEvent.click(button); expect(screen.getAllByRole('row').length).toBe(2); })
render でComponentをレンダリングして、ボタンを見つけてクリックする
その後に role=row が2つになっていることを確認してテスト成功
craに入っているeslintに従うと getByRole などを使うように誘導されるので、行を発見できるように <Row> <div> role='row' を与えておく
src/Row.tsx.diff
- <div> + <div role="row"> <select>{/* ここに課目が並ぶ */}</select>
ここで npm run test するとJestの実行時には crypto がブラウザと違ってglobalに無いことを怒られるので、setupファイルを置いて回避する
src/setupTests.ts を起動時に読んでくれる
src/setupTests.ts
global.crypto = require('crypto');
テスト成功しましたか?

コラム: React Componentのテストとモック
今回の <Journal /> のテストは <Row /> などをそのままレンダリングしている
一方で子要素が増えるとテスト時に影響を受ける範囲が広がり、純粋に「行を追加する」という機能だけをテストし辛くなっていく
そのような場合はJestの機能を用いてmockするのも1つのアイデア
src/Journal.test.tsx
jest.mock("./Row", () => ({ Row: () => <div role="row"></div> }));
Render Propsを採用して外から注入できるようにするのも良い

課目を取得する①
今回はAPIを叩いた気になれるように、JSONファイルを置いておいて、 fetch() で取得してみましょう
public/api/items.json
["現金", "当座預金", "普通預金", "定期預金", "その他の預金", "受取手形", "売掛金", "有価証券", "商品"]
http://localhost:3000/api/items.json で取得できるようになる

課目を取得する②
<Row> の中で fetch をする
fetch は非同期に通信をするAPI
Promise<Response> が返ってくる
このような処理をする場合は副作用を扱う useEffect を用いて、通信が完了したらその結果を用いて状態を更新して反映できるようにする
第2引数に配列で依存する値を渡すことでその値が更新されたら処理を実行できる
[] を渡すとマウント時にのみ実行されるように出来る
第1引数の関数が関数を返すとき、その返ってきた関数が副作用を呼び出す前に呼ばれる
example.tsx
useEffect(() => { subscribe(onUpdate); return () => unsubscribe() }, [onUpdate])
↑はアンマウント時に購読を取り消してくれる
src/Row.tsx.diff
+type ItemsAPI = Readonly<string[]> + type RowProps = Readonly<{ data: JournalRow update: (newData: JournalRow) => void }> export const Row: React.VFC<RowProps> = () => { + const [items, setItems] = useState<ItemsAPI | undefined>(undefined); + + useEffect(() => { + fetch('/api/items.json') + .then((res) => res.json()) + .then((json: ItemsAPI) => { + setItems(json); + }); + }, []); return <div role="row"> - <select>{/* ここに課目が並ぶ */}</select> + <select>{ + items?.map(item => <option value={item}>{item}</option>) + }</select> <input type='number' /> - <select>{/* ここに課目が並ぶ */}</select> + <select>{ + items?.map(item => <option value={item}>{item}</option>) + }</select> <input type='number' /> </div> }

更新を反映できるようにする
現在の値と JournalData[] の特定の要素を更新できる関数を返してくれるフックを作る
こういう感じになってくれる useJournalData があると嬉しそう
src/Journal.tsx.diff
export const Journal = () => { - const [data, setData] = useState<JournalData>([createInitialData()]); - - const addRow = () => { - setData((prev) => [...prev, createInitialData()]) - } + const {data, updateDataById, addRow} = useJournalData(); return <>{ - data.map((d) => <Row key={d.id} data={d} update={() => {/* 一旦空の関数を渡してごまかす */}} />) + data.map((d) => <Row key={d.id} data={d} update={updateDataById} />) } <button onClick={addRow}>行を追加</button> </>

useJournalData を実装する①
欲しい形も決まってるので、先にテストを書く作戦で向き合ってみる
フックのテストを支援してくれるreact-hooks-testing-libraryを導入します
npm i -D @testing-library/react-hooks
src/journal-data.ts
import { JournalData, JournalRow } from "./types/journal" export const useJournalData = (): Readonly<{ data: JournalData, updateDataById: (id: string, newData: Partial<JournalRow>) => void; addRow: () => void; }> => { return { data: [], updateDataById: (id: string, newData: Partial<JournalRow>) => {}, addRow: () => {} } }
src/journal-data.test.ts
import { renderHook } from '@testing-library/react-hooks' import { act } from 'react-dom/test-utils'; import { useJournalData } from './journal-data'; test('updateDateByIdで特定のデータを更新できる', () => { const {result} = renderHook(useJournalData); act(() => { result.current.addRow(); result.current.addRow(); result.current.addRow(); result.current.addRow(); }) const id = result.current.data[2].id; act(() => { result.current.updateDataById(id, {debitValue: 100}); }); expect(result.current.data[2]).toEqual( expect.objectContaining({debitValue: 100}) ) act(() => { result.current.updateDataById(id, {debitItem: 'test',debitValue: 200}); }) expect(result.current.data[2]).toEqual( expect.objectContaining({debitItem: 'test',debitValue: 200}) ) })
act はstateの更新があるときに囲っておく

useJournalData を実装する②
テストで一通り欲しい振る舞いは書かれているので、それを満たすように実装を書く
Try 書けますよね?
素朴に書くとこういう感じになると思う
src/journal-data.ts.diff
+ const [data, setData] = useState<JournalData>([createInitialData()]); + return { - data: [], - updateDataById: (id: string, newData: Partial<JournalRow>) => {}, - addRow: () => {} + data, + updateDataById: (id: string, newData: Partial<JournalRow>) => { + setData(prev => prev.map((d) => d.id === id ? { ...d, ...newData } : d)) + }, + addRow: () => { setData((prev) => [...prev, createInitialData()]) } } }

<Row> で使えるように更新用の関数を渡す
src/Journal.tsx.diff
- const [data, setData] = useState<JournalData>([createInitialData()]); - - const addRow = () => { - setData((prev) => [...prev, createInitialData()]) - } + const {data, updateDataById, addRow} = useJournalData(); return <>{ - data.map((d) => <Row key={d.id} data={d} update={() => {/* 一旦空の関数を渡してごまかす */}} />) + data.map((d) => { + const update = (_data: Partial<JournalRow>) => updateDataById(d.id, _data) + return <Row key={d.id} data={d} update={update} /> + }) } <button onClick={addRow}>行を追加</button>

<Row> のそれぞれの onChange で渡ってきた更新用の関数を叩くようにする
update を受け取るようにする
src/Row.tsx.diff
type RowProps = Readonly<{ data: JournalRow - update: (newData: JournalRow) => void + update: (newData: Partial<JournalRow>) => void }> - export const Row: React.VFC<RowProps> = ({ }) => { + export const Row: React.VFC<RowProps> = ({ update }) => {
ts
onChange: (event: React.SyntheticEvent<HTMLSelectElement>)
という感じのインターフェイスで型を与えておくと良い感じに出来る
イベントに応じて、 React.MouseEvent などが React.SyntheticEvent を継承したものとして在る
src/Row.tsx.diff
return <div role="row"> - <select>{ + <select onChange={(e: React.SyntheticEvent<HTMLSelectElement>) => update({ + debitItem: e.currentTarget.value + })}>{ items?.map(item => <option value={item}>{item}</option>) }</select> - <input type='number' /> - <select>{ + <input type='number' onChange={(e: React.SyntheticEvent<HTMLInputElement>) => update({ + debitValue: e.currentTarget.valueAsNumber + })}/> + <select onChange={(e: React.SyntheticEvent<HTMLSelectElement>) => update({ + creditItem: e.currentTarget.value + })}>{ items?.map(item => <option value={item}>{item}</option>) }</select> - <input type='number' /> + <input type='number' onChange={(e: React.SyntheticEvent<HTMLInputElement>) => update({ + creditValue: e.currentTarget.valueAsNumber + })}/> </div>
HTMLInputElement.valueAsNumber は値を数値として取得できるので型変換をしなくて済むので便利
ここで data を受け取っていたが使ってなかったことに気付くので、それぞれ初期値として defaultValue で渡しておく
inputの初期値に 0 が入るはず

合計を計算する
<Sum> を実装する
借方と貸方それぞれの合計
不足額がある場合はどっちがいくら足りないか表示する
src/Sum.tsx
type SumProps = Readonly<{ data: JournalData }> export const Sum: React.VFC<SumProps> = ({data}) => { const debit = data.reduce((sum, d) => sum + d.debitValue, 0); const credit = data.reduce((sum, d) => sum + d.creditValue, 0); return <div role="row"> 借方の合計 {debit}円<br/> 貸方の合計 {credit}円<br/> { debit !== credit ? <span style={{color: 'red'}}> {debit > credit ? "貸方" : "借方"}の不足額: {Math.abs(debit - credit)}円 </span> : null } </div> }
JSX内でJSを実行する場合は {} で囲む
{} の中は式しか書けないので、条件分岐させたい場合は三項演算子を入れ子させる
ここで role=row が増えるのでテストは壊れます

お疲れ様でした
ひとまず一通り動くようなものが出来たはず
いかがでしたか?

pastak ここまで出来たら大丈夫です!ここに感想をどうぞ!!

pastak続きは時間があったら読んでください。 useMemo などの話は全員の前でやれたらやります

useMemo useCallback について
値や関数をメモ化するための組み込みフック
useEffect などで依存を書いた際の値の同一性を担保するためなどで活躍する
プリミティブな値は === 、それ以外は Object.is で比較される

useJournalData が返す関数たちについて考える
updateDateById addRow はレンダリングのたびに関数オブジェクトが生成されている
src/Journal.tsx.diff
const {data, updateDataById, addRow} = useJournalData(); + useEffect(() => { + console.log('addRow is updated') + }, [addRow]); + return <>{
これで行を追加してレンダリングを走らせると毎回 addRow が違うオブジェクトになっているので、 useEffect の中身が実行されていることが分かる
addRow をメモ化すると解決する
src/journal-data.ts.diff
+ + const addRow = useCallback(() => { + setData((prev) => [...prev, createInitialData()]) + }, []); return { data, updateDataById: (id: string, newData: Partial<JournalRow>) => { setData(prev => prev.map((d) => d.id === id ? { ...d, ...newData } : d)) }, - addRow: () => { setData((prev) => [...prev, createInitialData()]) } + addRow }

メモ化とパフォーマンス
useMemo はヘビーな計算結果をメモ化しておくことにも勿論使われる
特に独自フックなどで関数やオブジェクトや配列を返却する場合、その後に別のフックやComponentに渡されることを考えて、基本的には useMemo useCallback しておくと安心

コラム: React Component自体の設計について
今回のハンズオンではかなり素朴にあらゆるComponentやフックを設計しました
React Componentは素朴な関数なので、関数合成を用いてクリーンアーキテクチャ的なアイデアを持ち込んで債務を分離するということも出来ます
JournalView useJournalPresenter を作って合成したものを <Journal> として提供する
PresenterはPropsを受け取って加工したりしてViewに渡す
use から始めてフックとして振る舞うようにしておく
connect.tsx
// 実際にはもう少し色々やることになると思うので、イメージ図みたいな感じです function connect(usePresenter, View) { const Component: React.FC = (props) => { const propsForView = usePresenter(props); return <View {...props} /> }; return Component; }
Container ComponentとPresentational Componentに分けて実装する

Try時間が余ったらやってみて欲しいことの例
こういうことが出来るとより良くなりそうという変更の例です
DevToolのconsoleを見ると、inputが空になったときにNaNになってしまうという問題があるので修正する
<Row> もテストを書く
行を削除できるようにする
素朴に要素が並んでいるが、 <table> などを使って整形してみる
role="table" role="cell" とかをちゃんと付けるとかでも良いですね
巨大な状態を1つ親が持っていて、それを配る形になっているが、もう少し良い設計を採用してみる
useJournalData があらゆる関数も返してくるが、なんかもう少し気が利いたり出来そう
inputの値などをバリデーションする
useRef を使ってDOMへの参照を持って値を取得する
科目名を貸方と借方で分離させる
useItems でのAPIからのJSON取得、今は <Row /> がマウントされるたびに取得しているが、フロントエンド側でキャッシュをすることが出来そう
フックに切り出して、共通化。その上でなんらかのキャッシュを入れる
適当な変数に入れておく、 useSWR を使うなど
useState に入れるだけでは、その呼び出し元のComponentごとの状態にしかならないことに注意
pastak どこもかしこも data って書いてるけど、もう少し命名なんとかならないか