前略
Reactを使い始めて3ヶ月。
毎日たくさんコードを読んで書いて、様々なことを学んだ。
3ヶ月前に君が書いたコードを読んでぶん殴りたくなるのも成長の証と言えるかもしれない。
ただ、僕はこの3ヶ月で人間的にも成長したので、ただ君をぶん殴るような非生産的なことはしない。
代わりに、Reactを始めたばかりの君に、より良いコードを書くためのポイントを伝えたいと思う。
3ヶ月後の君が、自分のコードを見て頭を抱えなくても済むように…
ただし、ここに書くことは個人的な好みも大きく影響している。
僕は3ヶ月後の君だから、君とは好みが合うとは思うけど、必ずしもすべて鵜呑みにせず、常により良い方法を模索して欲しい。
1ファイルの行数はもっと減らせる
1ファイルの行数は減らせるだけ減らした方が良い。
とりあえず見た目を整えて、とりあえず動くものを…と思うと、ひとつのファイルに何でもかんでも書きがちだ。
特に、vueで single file component の使い勝手に慣れた君は、その傾向が強いと思う。
しかしそうすると、どのファイルのどこに何が書いてあるのか探すだけで数分無駄にする。
積もり積もって何時間も捨てることになる。
積極的にファイルを分割し、ひとつのファイルの役割を絞った方が良い。
3ヶ月前
import React from 'react'
import styled from 'styled-component'
type Props = {
label: string
}
export const Button = ({ label }: Props) => {
return <StyledButton>{label}</StyledButton>
}
const StyledButton = styled.button`
border-radius: 3px;
`
現在
import React from 'react'
import { ButtonProps } from './types'
import { StyledButton } from './styles'
export const Button = ({ label }: ButtonProps) => {
return <StyledButton>{label}</StyledButton>
}
export type ButtonProps = {
label: string
}
import styled from 'styled-component'
export const StyledButton = styled.button`
border-radius: 3px;
`
「このくらい小さいファイルなら分割しない方が早いのに…」と感じるかもしれない。
だが、その「このくらい」の気持ちがあとで地獄を生む。
「このくらい」の基準が不明確だからだ。
最初から、役割で分割すると明確にルールを決めておくことで、アプリケーションの規模が大きくなったときにも管理に困らない。
プロパティは分割代入しよう
Reactを使う前、君は確か分割代入にあまり馴染みがなかったと思う。
初めて分割代入を知ったとき、なんだこれ気持ち悪! と思ったことは今でも覚えているし、何なら今でもちょっと気持ち悪い。
しかし、分割代入はコードを削減し、見通しを良くしてくれる。
積極的に使って行こう。
3ヶ月前
export const Button = (props) => {
return (
<button style={props.style}>
{props.icon}
{props.label}
</button>
}
現在
export const Button = (props) => {
const { style, icon, label } = props;
return <button style={style}>{icon}{label}</button>
}
なお、以下のように引数部分で分割している場合も多く見られるし、僕もこういう書き方をしたりもするが、個人的には上記のように関数の内側で分割する方が好きだ。
export const Button = ({ style, icon, label }) => {
return <button style={style}>{icon}{label}</button>
}
確かに、プロパティが少なかったり、それぞれのプロパティ名が短ければ、引数内で分割する方が行数は少なくなる。
以下のようにプロパティが増えたり、プロパティ名が長くなったりすると、どこからどこまでが引数なのかよくわからなくなる。
export const Button = ({
customButtonStyle,
customButtonIconBefore,
customButtonLabel,
customButtonSubLabel,
customButtonIconAfter
}) => {
// 略
}
これもルールとして、プロパティがn個以下の場合は引数内で、それ以上の場合は関数内で分割代入する、などと決めておくと、記述がバラつかなくて良いと思う。
カスタムフックはまだ早い
Reactを始めたのが遅かった君は、幸いなことに非常に洗練された形のReactと出会った。
関数型コンポーネントやフックという使いやすく見通しの良い記法で、スムーズにReactを使い始めることが出来た。
(クラスベースで書け、と言われると、今の僕でも泣きながらやる羽目になる)
Reactが手軽に始められるというだけで、Reactを始めたばかりの君に実力があるわけではなかった。
にも関わらず、君は調子に乗って、すぐにカスタムフックに手を出した。
そしてそれは地獄の始まりだった。
Reactを始めたばかりの君はカスタムフックを理解していない。
というかフックも理解していない。
なんとなくuseEffect
を使い、なんとなくuseCallback
を使っている。
そして今の僕も、まだなんとなくフックを使っている。
3ヶ月経ってもそんな状態なのに、カスタムフックを使いこなせるはずがなかった。
それは今の僕を苦しめ続けている。
だから、カスタムフックを使う前にまず立ち止まって考えてみて欲しい。
- コンポーネントを分割するだけで済むのではないか
- 関数を分割するだけで良いのではないか
- その機能を持ったオープンソースのモジュールなどはないか
そして、その上でどうしてもカスタムフックが使いたいと思ったら、次のことを考えて欲しい。
- そのカスタムフックの中ではフックを使うか
- そのカスタムフックは繰り返し使えるか
- そのカスタムフックは使いやすいか
1. 中でフックを使うかどうか
関数に useXxxx
という名前を付けるだけで簡単にカスタムフックとして扱える。
しかし、フックは、フックとしてしか使えない。
つまり、コンポーネントの中か、フックの中でしか呼び出すことが出来ない。
呼び出せる場所や順序も制限を受ける。
他の関数などからは呼び出すことが出来ない。
カスタムフックの中でフックを使わないのであれば、それはただの関数として定義してしまった方が使い勝手が良い。
useXxxx
という名前を付けなければ良いだけだ。
2. 繰り返し使えるか
カスタムフックの役割は機能の分割であり、コードの分割ではないと思う。
特定のコンポーネントでしか使わない機能であれば、カスタムフックではなくコンポーネントの分割を優先して考えてみて欲しい。
3. 使いやすいか
useState
がなぜ使いやすいか。
引数がstateの初期値、返り値が[state, dispatcher]
というシンプルな構成だからだ。
君が作ったカスタムフックは、引数のオブジェクトにプロパティが12個あり、返り値のオブジェクトには30個ある。
中身は800行くらいある。
やっぱり殴ってやろうかと思う。
どうやったら12個の引数を渡せるのか。
どうやって30個の返り値を使い分けるのか。
君はそんなに頭が良くない。
ちょっと感情的になってしまったが、許して欲しい。
僕は3ヶ月後の君なのだ。
使いやすさの観点から言うと、引数も返り値も1〜2個程度が理想的だと思う。
そして、なるべく公式のフックに合わせ、state
とdispatcher
のように、役割が明確なものを返すと良い。
ただし、useReducer
のdispatcher
は返さない方が良い。
useReducer
のdispatcher
にはaction
を文字列で渡してやる必要がある。
つまり、reducer
にどんなアクションがあるか把握していなければ使えないのだ。
わざわざreducer
を使うからには、そこにはやや複雑なstate
があり、それをどのaction
がどう変更するのかということを全部把握していないと使えないというのは使い勝手が良いとは言えない。
3ヶ月も経てば全部忘れているのだ。
カスタムフックの中でuseReducer
を使うのであれば、そのフック内でdispatchする関数を作り、それを返してやる方が役割が明確になって良いと思う。
そしてそもそもuseReducer
も君にはまだ早い。
スプレッド構文を使おう
これはReactに限ったことではないかもしれない。
先に例を挙げよう。
3ヶ月前
const updatedUser = {
name: user.name,
nickName: nickNameInput,
age: user.age,
email: user.email,
phoneNumber: user.phoneNumber,
address: user.address,
color: user.favoriteColor,
favoriteColor: user.favoriteColor
}
現在
const updatedUser = {
...user,
nickName: nickNameInput,
color: user.favoriteColor,
}
3ヶ月前の君は、単にスプレッドをよく知らなかったんだと思う。
それは仕方ないことだ。
だけどなるべく早く覚えた方が良い。
君のコードでは、変更箇所がどこなのか、ひとめではわかりにくい。
nickNameInput
が変更されているのはわかる。
じゃあuser
のプロパティを使っているところは変更されていないのか、と言われると、全部注意深く読まなければ見落としがあるかもしれない。
現に、ユーザーのお気に入りの色を、ユーザーの色(おそらくUIに利用するのだろう)に割り当てている。
とても見通しが悪く、書き間違いにも見える。
スプレッドを使うことで、変更箇所とそうでない箇所、つまり元のオブジェクトのものをそのまま使う箇所が明確にわかる。
コードは削減出来るし、可読性が大きく上がる。
分割代入でのスプレッド構文
分割代入でもスプレッドは活躍する。
例えば、ボタンをいくつか、縦か横に並べるコンポーネントがあるとしよう。
まず親コンポーネントWrapper
から子コンポーネントButtonGroup
にプロパティを渡す。
そこからさらに一部のプロパティだけを孫コンポーネントButtons
へ渡したい。
(なお、このコンポーネントには設計上の問題があるが、一例として許してほしい。)
親コンポーネント
export const Wrapper = () => {
return (
<div>
<ButtonGroup
direction="horizon"
colors={["red", "green", "blue"]}
labels={["danger", "safe", "info"]}
icons={["times", "thumbs-up", "information"]}
/>
</div>
)
}
3ヶ月前
export const ButtonGroup = (props) => {
return (
<ButtonGroup direction={props.direction}>
<Buttons
colors={props.colors}
labels={props.labels}
icons={props.icons}
/>
</ButtonGroup>
}
現在
export const ButtonGroup = (props) => {
const { direction, ...buttonsProps } = props;
return (
<ButtonGroup direction={direction}>
<Buttons {...buttonsProps} />
</ButtonGroup>
}
分割代入でスプレッドを使うと、残りのプロパティ全部を代入してくれる。
この場合、direction
だけを取り出し、colors
、labels
、icons
はbuttonsProps
に代入している。
こうすることで、ButtonGroups
コンポーネントで使うプロパティと、Buttons
コンポーネントに送るプロパティが明確になる。
そしてコードは削減出来て、見通しが良くなる。
初めは戸惑うかもしれないが、一度覚えると二度と手放せなくなる。
ぜひ早い段階で身につけて欲しい。
型は分離しよう
これもReactに限らず、Typescript全般に言えることかもしれない。
型注釈ではなく型宣言を行おう。
3ヶ月前
const getCommonTeam = (userId: string, teamId: string): {
teamId: string
members: {
userId: string
nickName: string
email: string
}[]
teamColor: string
} => {
// 略
}
現在
type MemberType = {
userId: string
nickName: string
email: string
}
type GetCommonTeamReturnType = {
teamId: string
members: MemberType[]
teamColor: string
}
export type GetCommonTeamType =
(userId: string, teamId: string) => GetCommonTeamReturnType
import { GetCommonTeamType } from './types'
const getCommonTeam: GetCommonTeamType = (userId, teamId) => {
// 略
}
結果的に行数は増えてしまったが、関数自体の見通しは大幅に良くなる。
関数の役割は機能を提供することだ。
機能以外の部分はなるべく分離して、目に入らないようにする方が、機能の開発や修正に集中出来ると思う。
最後に
3ヶ月後、君は今君自身が書いているコードにがっかりすることになる。
それはプログラマーとしては避けられないことだし、プログラマーである限り付き合っていかければならないことだ。
僕もまた3ヶ月後、今の僕のコードにがっかりして、この記事を必死に修正したりしているかもしれない。
それはそれで成長している証だし、良いとしよう。
さらに伝えたいことが増えたら追記するかもしれない。
何にせよこれが君の成長の助けになると幸いだ。
3ヶ月後の君より。
草々