この記事はGLOBIS Advent Calendar 2021の12月14日の記事です。
まえがき
アドベントカレンダーの季節ですね。
皆さん、いい記事を無料で沢山書いてくれていて本当に良い活動ですよね。
で、僕の担当の日が明日に迫っているんですが
記事化したかったものが間に合いそうになかった(調べきれない部分があった)ので
代わりに自分のコーディング時の病の一つを紹介します。
本当はReactのテストコードについて色々調べて記事を書きたかったんですが
今の手持ちの情報では既出の記事の劣化版になりそうなのでやめておきました。
予防線を貼ります。
技術的に大した事は書いていません。
コーディング中に出会ったつらみを共有することで
少しでもつらみが解消できればいいな〜とふんわり希望を乗せた記事です。
React Stateのsetを用途を限定せずに渡すつらみ
まずはこちらのコードを見てください。
import React, { useState } from 'react'
const useTodos = () => {
const [todos, setTodos] = useState()
// 省略: fetchとかfilterとか色々
return {
todos,
setTodos // これはカスタムフックの外に出さないで欲しい
}
}
setTodosを用途を限定せずにreturnしてますね。
別に悪くは無いんです。
悪くはないんですが、僕はこれを見ると
setTodosをreturnの中から除外したくなる病を発症しています。
setTodosの用途はなんですか?
上書きしたいんですか?
追加したいんですか?
取り除きたいんですか?
リセットしたいんですか?
stateの書き込みは用途を限定した方が
実装もテストもコードの読み手も楽になると思います。
用途を限定しなかったら
先程作ったカスタムフックを使ったコンポーネントです。
import React from 'react'
import { useTodos } from './useTodos'
import HogeComponent from './HogeComponent'
type Todo = {
title: string
isCompleted: boolean
}
export const Component = () => {
const { todos, setTodos } = useTodos()
const remove = (title: string) => {
setTodos((current) => current.filter((v) => v.title !== title))
}
const add = (todo: Todo) => {
setTodos((current) => [...current, todo])
}
const clear = () => {
setTodos([])
}
return (
<div>
<ul>
{todos.map(todo => (
{/* 省略: removeを使うコード */}
{/* 省略: addを使うコード */}
{/* 省略: clear使うコード */}
<li key={todo.title}>{todo.title}</li>
))}
</ul>
<HogeComponent setTodos={setTodos} />
</div>
)
}
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
(その関数、カスタムフックの中で定義したい)
↓↓↓↓
呼び出し側はこんな風に使えると読み易いですよね。
import React from 'react'
import { useTodos } from './useTodos'
import HogeComponent from './HogeComponent'
export const Component = () => {
const { todos, setTodos, remove, add, clear } = useTodos()
return (
<div>
<ul>
{todos.map(todo => (
{/* 省略: removeを使うコード */}
{/* 省略: addを使うコード */}
{/* 省略: clear使うコード */}
<li key={todo.title}>{todo.title}</li>
))}
</ul>
{/* ゴォゴゴゴゴォォォォ */}
<HogeComponent setTodos={setTodos} />
</div>
)
}
再利用できていいですよね。
remove等の関数の責任をcomponent側が持たなくていい。
テストコードを書くのも1回で済みます。
書き込みの用途を限定する
書き込みの用途を限定してみました。
import React, { useState } from 'react'
type Todo = {
title: string
isCompleted: boolean
}
const useTodos = () => {
const [todos, setTodos] = useState()
// 省略: fetchとかfilterとか色々
const remove = useCallback((title: string) => {
setTodos((current) => current.filter((v) => v.title !== title))
}, [setTodos])
const add = useCallback((todo: Todo) => {
setTodos((current) => [...current, todo])
}, [setTods])
const clear = useCallback(() => {
setTodos([])
}, [setTodos])
return {
todos,
remove,
add,
clear
}
}
これでもうsetTodosはretuenする必要が無くなったハズです。
これならテストも書き易いですし、todosがどんな風に更新されるのかを
このカスタムフックを見るだけでイメージできますね!
例えば、このカスタムフックを見る限り
カスタムフックの外で勝手にtodosを丸っきり新しいデータに上書きしてしまうような処理は無いと想定できます。
また、並び替えや特定のtodoの編集の処理なども無いことがイメージできます。
悪魔のpropsたらい回しのつらみ
僕が一番恐れているものなんですが、
useStateの書き込み関数をprops等でコンポーネントやカスタムフックスで
たらい回しにされる事が一番怖いです。
さて、先程ちょっとスッキリさせたコンポーネント側のコードをもう一度持ってきました。
恐らくTypeScriptを使っていれば、今頃コンパイルエラーを吐いて居ることでしょう。
「ゴォゴゴゴゴォォォォ」
と唸り声を上げるHogeComponentにsetTodosが渡って居るみたいです。
先程setTodosはuseTodosの返り値から除外したので、そんなもの存在していないと怒られているはずです。
import React from 'react'
import { useTodos } from './useTodos'
import HogeComponent from './HogeComponent'
export const Component = () => {
const { todos, setTodos, remove, add, clear } = useTodos()
return (
<div>
<ul>
{todos.map(todo => (
{/* 省略: removeを使うコード */}
{/* 省略: addを使うコード */}
{/* 省略: clear使うコード */}
<li key={todo.title}>{todo.title}</li>
))}
</ul>
{/* ゴォゴゴゴゴォォォォ */}
<HogeComponent setTodos={setTodos} />
</div>
)
}
propsで渡った先で何が起きているのか…。
1つ追いかけてもpropsに渡している。
import React from 'react'
import type { Todo } from './useTodos'
import ChildrenComponent from './ChildrenComponent'
type Props = {
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>
}
export const HogeComponent = (props: Props) => (
<div>
<ChildrenComponent {...props} />
</div>
)
2つ追いかけてもpropsに渡している。
import React from 'react'
import type { Todo } from './useTodos'
import ChildrenComponent2 from './ChildrenComponent2'
type Props = {
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>
}
export const ChildrenComponent = (props: Props) => (
<div>
<ChildrenComponent2 {...props} />
</div>
)
3つ追いかけてもpropsに渡している
import React from 'react'
import type { Todo } from './useTodos'
import ChildrenComponent3 from './ChildrenComponent3'
type Props = {
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>
}
export const ChildrenComponent2 = (props: Props) => (
<div>
<ChildrenComponent3 {...props} />
</div>
)
.
..
...
....
.....
......
.......
追いかけた先で唐突にとんでもない影響範囲の実装してたり
もし仮に同じ命名のsetTodosがあれば
「ぼくはどこのおうちのsetTodosくんかな?」と
保護者を特定するコストが発生したりします。
特にReact Nativeのrouterで渡されていた時は一番辛かった。。。
import React from 'react'
import type { Todo } from './useTodos'
import ChildrenComponent3 from './ChildrenComponent3'
type Props = {
setTodos: React.Dispatch<React.SetStateAction<Todo[]>>
}
export const Content = (props: Props) => (
const allComplite = () => {
setTodos((current) => {
return current.map(v) => ({
title: '完了済み: ' + current.title,
isCompleted: true
})
)
}
return (
<div>
<buton onClick={allComplite} />
</div>
)
)
おわりに
今回はReactのステートの書き込み関数を例に上げましたが
基本的に責任の範囲を小さく限定して、脳みそをフル回転させずに読めるコードを望みます。
むかし、同僚にコードレビューと口頭で一生懸命思いを伝えても
最後まで伝わらなかった思いを記事にしてみました。
「伝える」って中々に難しいものですね。
でも最近転職をして、沢山の初めてや知らないに触れて
「情報の乖離」が伝える事の難しさを作っているんだと気が付きました。
知らない技術、知らない人、知らない共通認識、知らないドメイン知識、知らない歴史。。。
「エンジニアの仕事は不確実性を埋めること」みたいなのを
何かの本で読んだ気がしますが、「ほんコレ」って感じです。
明日も不確実性を小さくしていくぞー!
そうそう、最近読んでよかったな〜と思う本を1冊紹介しておきますね。
(安心してください。アドセンスリンクじゃありません。)