自分が経験したこと考えたことを記録したポエムです。特定の何かを貶めたり知識や努力の不足を指摘するような意図はありません。
1. 動機的なもの
React修行で自作のアプリ作ってるときに、useEffect+useStateでコンポーネント内のあちらこちらに処理がジャンプするコードができあがり、理解するのも拡張するのもしんどくなってしまい、これはどうしたものかと頭をかかえてしばらく離れていた。
その後ふとしたタイミングで Reactピタゴラスイッチ
というジャーゴンを知った。この言葉が自分の状況をうまく表現しているように思えた。GoogleやTwitterで React ピタゴラスイッチ
で検索するといろいろひっかかり、どうやら同じ悩みを持っている人はそれなりにいるみたいだ。
で、しばらくAndroidの開発に逃避していたところ単方向データフロー(Unidirectional Data Flow, UDF)というものを知り、AndroidのJetPack ComposeはReactと似た作りになっていたのでUDFをReactに応用すれば可読性を向上させることができるのではないかと思って試した成果がこれ(本記事)。目的は可読性を向上させ認知負荷を軽減することで、個人的にはそれなりに成功したのではないかと思っている。
2. デモコード
以下にデモコードを示しておく。別にどうということはないWeb+AP+DBの3レイヤーアプリケーションで、前回の社内名簿アプリをクラサバ化して実装している。
3. 実際にやったこと
3.1. stateにはコンポーネントの描画に関連するようなフラグやデータしか入れない
逆に言うと、「アクセストークンを取得した/してない」といった、処理の進行状況に関するような情報は state では管理しないようにした。
3.2. ビジネスロジックはコンポーネントとは別ファイルに記述する
今までは useEffect
の中に実際のビジネスロジックを書いていた。
これをやめ、Androidの単方向データフローに似せてコンポーネントからビジネスロジックを別ファイルに切り出した。
意図的に以下のように作った
-
切り出したファイルはuseEffectから呼び出して、propが変わったときに再実行されるようにした。具体的にはこんな感じ
import { betsufile_process } from './betsufile' export const Page = ({prop1, prop2} => { // 表示切り替え用のstate const [state1, setState1] = useState(null) // 実際の処理を行う部分 useEffect(() => { betsufile_process({ prop1: prop1, prop2: prop2 }).then((ret) => { if(ret.status === "status1") setState1("status1") else setState1("otherstatus") }).catch((error) => { goToLoginPage() }) },[prop1, prop2]) // 表示部分 if (state1 === null) return ( // 処理が終わってないときの表示内容 ) if (state1 === "status1") { return ( // いろいろ ) } else { return ( // いろいろ ) } })
-
各関数はPromiseを返すようにして、必要に応じてasync/awaitで処理完了を待つようにし、処理ができるだけ直線的に進むようにした
これによって切り出した別ファイルを読めばどのような処理をしているかがわかるし、上から順番に読んでいけば何をしているのかを理解しやすくなった。例えば以下のような感じ。
export const betsufile_process = ({prop1, prop2}) => { return new Promise(async(resolve, reject) => { // 処理を開始しない条件を設定 if (prop1 == null) { resolve({ status: "precondition has not fulfilled" }) return } // 処理1 const var1 = await subFunction(prop1) // 処理2 let var2 = 0 if(var1 === "aaa") { var2 = await subFunction2(var1) } else { var2 = await subFunction3(var1) } // 処理3 subFunction4(var2) .then((var3) => { if(var3 === "aaa") { resolve({ status: "status1", content: "content1" }) return } else { reject({ status: "error1", content: "error message1" }) return } }).catch((error) => { reject({ status: "error2", content: error }) }) }) }
4. 余談
AndroidとReactで単方向データフローが指すものが違う
Androidだと上の動画が示す通り、こういう風に作ればいい感じに整理できますよっていうデザインパターンの話なんだけど、Reactだと単方向データフロー(One-way data flow)とはデータがトップレベルのコンポーネントからツリーの下の方にあるコンポーネントに流れていくReactの構造のことを示している。日本語だと同じ単語になってしまってるのでアレ。別にいいとか悪いとかの話をしているわけではないけど、両方やってると勘違いしやすいかも。
Reactだと副作用(side effect)はレンダーの外で実行するという言葉がいちばん近いのかもしれない。最初私は横着してこれをほぼ読んでいなかった。というより何を意味しているかを理解できなかった。
5. さいごに
デモの作成に1ヶ月かかっちゃった。