🧭

【React】関数型プログラミングを実践する上での、条件分岐の俺的ベストプラクティス

に公開
10

Discussion

kage1020kage1020

オプショナルチェーン(?.)と同じES2020で追加されたnull合体演算子を使うともう少しコンパクトに書けます.

const userName = user?.name ?? 'ユーザーはいません';
// user.name が null または undefined のときのみ代入される

const genderColor = (user?.gender ?? '') && (user.gender === '男性' ? 'blue' : 'pink')
// 複雑なので記事のようにuser.genderの存在判定を先にした方がよさそうです.

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator

基本この辺の演算子は可読性とのトレードオフですが,Reactを書いていると手続き型や行数が増えることを避けがちですね.

ItaIta

コメントありがとうございます!

そうですね。null合体演算子とか、場合によっては||等のショートサーキット評価もありなのかなと思います。
基本的に、可読性を損なわずに1-2行程度で書けるのであればこういった演算子を用いる方向で考えています。

クロパンダクロパンダ

説明変数を足すこと自体は賛成ですが、userNameの導入には反対です。userNameに"ユーザーはいません"というフォールバックが入ることを知らずに <p>ユーザー名: {userName}</p> だけ見たときに挙動を勘違いしうるからです。userNameという変数名をやめるか、単に user?.name ?? 'ユーザーはいません' と書き下した方が誤解が生じづらいです。

他の例も賛同しかねます。例えば「条件分岐が階層になっている場合や、3択以上あるとき」の例ですが、if(!user) return <p>ユーザー名: ユーザーはいません</p> という early return があれば narrowing も効いてかなり読みやすくなるはずです。<p className={genderColor()}>ユーザー名:{userName}</p> と書かれるといちいち genderColor の実装と userName の中身を調べないと動作を読み取れないので、あまり上手くない抽象化だと思います(僕ならレビューで弾きます)

ItaIta

ご指摘ありがとうございます!

そうですね、例として挙げたものが悪かった気がします。
説明変数を定義してあげて可読性を高める、という趣旨で書いていました。
userNameという変数名はご指摘の通りふさわしくないと思うので修正いたします。
(上記の例、「ユーザーがいないときは『ユーザーはいません』という文字列を出力するという例を変更するかもしれません)

2つ目の以下の指摘は僕も書いている途中に気づきました。

if(!user) return <p>ユーザー名: ユーザーはいません</p> という early return があれば narrowing も効いてかなり読みやすくなるはずです。

しかし、「UIの変更はしない」「関数内で条件分岐させてあげた方がいい」例として見せたかったので無理矢理このようにした次第です。(FC内での早期リターンは3つ目の例として出したかったというエゴです。)
多分これも例が悪く、一つ前の例から引き継ぎの形で書いたことが原因のように思います。
2つ目に関してはもっといい例を示したく思います。(2つ目の例が極端に少ないorないようなら内容自体見つめ直そうと思います。)

nuko_suke_devnuko_suke_dev

良い記事ですね!
React で条件分岐は私もよく考えされられます!
本筋とちょっと外れますが、コード例にある getSpecificUser は単純に Promise を返すだけなので asyncawait はいらないと思いました!

  // 例にある関数
  const getSpecificUser = async () => {
    const specificUser = await getUser(userId);

    return specificUser;
  };

  // こっちの方が簡潔
  const getSpecificUser = () => getUser(userId);
ItaIta

ご指摘ありがとうございます!
確かにそちらの方がベターですね!

A KidA Kid

三項演算子って、入れ子にせずに同じ方向に延ばしていく分には、3択以上になっても実は可読性が下がらないと思いますね。

const userName = user == null ? ''
                : user.gender === '男性' ? 'blue'
                : user.gender === '女性' ? 'pink'
                : 'white';
takezoux2takezoux2

ts-pattern を採用すると、かなりスッキリ分岐を書けます。
なんなら、条件分岐ではなくパターンマッチングにできます。

いわもとたかあきいわもとたかあき

こっちの方が良いというより、説得難しいっていう感想です。

三項演算子が許可されてるなら、単純な比較と値を返すだけならif ~ returnを1行で書くことも許されるかと。。。

例示されたコードはきれいだけど、行数の差が大きすぎて、PRなどで「ネストした三項演算子は読みづらい」って指摘したときに「なれれば1行の方が読みやすい」って言われそうだなぁと思いました。

const genderColor = () => {
  if (!user) return '';
  if (user.gender === '男性') return 'blue';

  return 'pink';
};

コーディングに対して合意を得るのって難しいですよねぇ。

nap5nap5

条件分岐により別のUIを出力したいとき

ts-patternでデモ作ってみました。

https://codesandbox.io/p/sandbox/blissful-tu-bs6pjm?file=%2Fsrc%2Ffeatures%2Ftodo%2Fcomponents%2FTodos.tsx

/todosページがデモになります。

const Todos = () => {
  const { neatLabelName } = useFormatter()
  const { data, error, refetch } = useListUpTodoHook()
  const { decidePageState } = useDecidePageState()

  const renderContent = () => {
    return match(decidePageState(data, error))
      .with('error', () => {
        return (
          <TodoLayout>
            <NiceButton
              type='button'
              labelName={neatLabelName(data, error)}
              onClick={() => {
                queryClient.removeQueries([TODO_KEY])
                refetch()
              }}
            />
            <Spacer />
            <ShowMe data={error?.response?.data} />
          </TodoLayout>
        )
      })
      .with('loading', () => {
        return (
          <TodoLayout>
            <Loading />
          </TodoLayout>
        )
      })
      .with('success', () => {
        const neatData = safeParseTodosData(data)
        return (
          <TodoLayout>
            <NiceButton
              type='button'
              labelName={neatLabelName(neatData, error, 'Latest Refresh')}
              onClick={() => {
                queryClient.removeQueries([TODO_KEY])
                refetch()
              }}
            />
            <Spacer />
            <div className='flex items-center flex-col justify-center gap-4'>
              {neatData.map((item, index) => {
                return (
                  <div key={index} className='shadow-bebop rounded-2xl p-4'>
                    <h1>{item.title}</h1>
                    <p>{item.body}</p>
                    <b>{`u${item.userId}`}</b>
                    <span>{`#${item.id}`}</span>
                  </div>
                )
              })}
            </div>
          </TodoLayout>
        )
      })
      .run()
  }

  return <>{renderContent()}</>
}

簡単ですが、以上です。