こんにちは。フューチャー 3 年目の柏木です。
はじめに React、Next.js を触り始めて2年半ほど経ちました。 これまでによくつまずいたポイントから、自分なりのノウハウを言語化してみます。
想定する読者
React、Redux(、Next.js)を初めて触る人
システム開発の中で初めてフロントエンドを担当することになった人
開発で使用している技術要素
Node.js(10.5.0)
Express(4.16.3)
Next.js(5.1.0)
React.js(16.4.1)
Redux(3.7.2)
ノウハウたち 次の1~4について順番に説明していきます
フロントで持つべきデータの形とデータベースに登録するデータの形は必ずしもイコールではない
React の各ライフサイクルで適切な処理を行う
更新を React が正しく検知してくれるよう、値はコピーしてまるっと置き換える
(おまけ)ライブラリやフレームワークを導入するときは CSS をどれだけカスタマイズできるか注意する
1. フロントで持つべきデータの形とデータベースに登録するデータの形は必ずしもイコールではない
データベースに登録する値は、業務要件にもよりますが、出来るだけ冗長な構造を避け、シンプルであるべきだとよく聞きます。
一方、フロントエンドでは、データベースにある形のままデータを持つことが必ずしもベストではないこともあります。
例えば、データベースでは下記のような配列のデータを持っていたとします。
data.json { "deta_1" : [ { "id" : "xxx" , "param_1" : "xxx" , "param_2" : "xxx" , "param_3" : "xxx" } , { "id" : "yyy" , "param_1" : "yyy" , "param_2" : "yyy" , "param_3" : "yyy" } , { "id" : "zzz" , "param_1" : "zzz" , "param_2" : "zzz" , "param_3" : "zzz" } ] }
フロントでもこのままの形を維持した場合、id
が”yyy” であるオブジェクトにアクセスしたい場合、deta_1
の配列で For 文を回して検索することになります。
これでは、deta_1
のオブジェクトが 10000 ある場合や、検索したいid
が”yyy”の他にいくつもある場合、描画処理のたびに大きな負担がかかってしまいます。
フロント側で面倒な処理を重ねることは、描画のタイムラグに直結、使うユーザーのストレスを増やしかねません。
そこでこのデータを、配列ではなくid をキーとしたオブジェクト で持つようにします。
変換するタイミングは、データを取得してフロントに返ってきた直後です。
例えば私のプロジェクトでは、API コールは画面初期表示時の場合getInitialProps
(Next.js の機能)の中、イベント発火の場合actions
(Redux の機能)内で行っています。下記例はactions
内でデータ取得した時の想定です。
例)xxx/actions/testpage.js const arrangeDataForFront = data_1 => { return Object .assign ({}, ...data_1.map (data => ({ [data.id ]: data }))); }; export const searchTest = parameter => { return async dispatch => { const result = res.json (); const data_1ForFront = arrangeDataForFront (result.data_1 ); console .log (data_1ForFront); dispatch ({ type : ***, data_1 : data_1ForFront, }); }; };
これで、目的のデータにはdeta_1[yyy]
で参照できるようになりました。
このようにアクセスしやすいデータの形を作ることは、描画の際の負担を減らし、無駄な処理によるバグを生みだしにくくすることに繋がります!
2. React の各ライフサイクルで適切な処理を行う
React には様々なライフサイクルのメソッドがあります。また、Next.js も親コンポーネントで使えるデータ取得のメソッドが存在します(それぞれのメソッドの特徴については上記公式ドキュメントに詳細に記載されているので割愛します)。
これらのライフサイクルをそれぞれのコンポーネントで使い分け、必要な時に必要な処理が適切に行われる ことが、React での開発の鍵なのではないかと個人的に思っています。
ハマった失敗談の例
画面をリロードした時は検索して描画するまで想定通りに動くが、画面上でボタンをクリックして検索するとエラーになってしまう。実はgetInitialProps
に実装した必要な処理は、初期描画の時しか呼ばれていなかった !
子供コンポーネントでのイベント発火時に親コンポーネントのonChange
メソッドをコールパックしたら、子供コンポーネントの値が変わる度に親コンポーネントも再描画され、レンダリングに大変な時間がかかってしまった!
このように自分の予期せぬところで値が更新されてしまうと、不具合がおきた時の切り分けが難しくなってしまいます。
それぞれのメソッドで適切な処理をコードにまとめると以下のような感じです。
testpage.js class TestPage extends Component { constructor (props ){ } static getInitialProps ({ query } ) { } componentDidMount ( ) { } onChangeXXX ( ){ } render ( ) { return ( ); } } export default TestPage ;
データの流れがわからなくなってしまったら、書こうとしている処理が、どういったタイミングで行われて欲しいかを一度図にして整理してみると、すっきりすると思います。
※BFF(Backend For Frontend) とは
3. 更新を React が正しく検知してくれるよう、値はコピーしてまるっと置き換える
配列で持っているデータの値を更新したはずなのに、描画してみたらうまく行かない、、、、、となったことはありませんか。
実は、React では差分検知は「浅い比較 」で行われます。
そのため、下記のデータの項目paramC
の値を更新したい場合、
arrayA.json [ "object1" : { "paramC" : "xxx" } , "object2" : { "paramC" : "yyy" } , "object3" : { "paramC" : "zzz" } ]
reducer を下記のように実装するとデータは更新されるが再レンダリングは行われない状態になります。
reducers/test.js export const testReducer = ({ arrayA = [] } = {}, action ) => { switch (action.type ) { case "NG_ASSIGNMENT" : if (action.arrayA ) { arrayA = action.arrayA ; } break ; } return { arrayA }; };
そこで下記のようにスプレッド演算子でコピーするようにします。
reducers/test.js export const testReducer = ({ arrayA = [] } = {}, action ) => { switch (action.type ) { case "OK_ASSIGNMENT" : if (action.objectB ) { arrayA = [...arrayA, ...action.objectB ]; } break ; } return { arrayA }; };
こうすることで、arrayA
の更新が画面でも検知され、再レンダリングが行われます。
注意すべきなのは、スプレッド演算子は第一階層までしかコピーできないことです。
そのため下記のようなデータ構造でparamC
を更新するためには、JSON.stringify()
を用いて強制的に値の変更を検知させるか、arrayA
でなくobject1
で更新するなど、更新検知のオブジェクトの粒度を見直した方が良いでしょう。
arrayA.json [ "object1" : { "arrayB" : [ { "paramC" : "xxx" } ] } , ]
4. (おまけ)ライブラリやフレームワークを導入するときは CSS をどれだけカスタマイズできるか注意する
Javasript 関連のライブラリは多種多様であり、加えて「npm」というパッケージ管理ツールのおかげで、コマンドを打つだけでやりたいことをやってくれる外部ライブラリがサクッと入られます。
また、React であれば Material UI などのフレームワークも充実しており、見た目が統一しやすく簡単に実装できるので取り入れるメリットは大きいと思います。
ただ一点注意が必要なのは、ライブラリやフレームワークの内部で設定されている CSS のカスタマイズには工夫が必要であるということです。
過去失敗談として、要件に適したライブラリを導入し、要望の画面イメージに合わせていざ見た目を整えようとしたところ、調整に1日ほど溶かし、結果的に!important
で内部の CSS をオーバーライドする羽目になったことがあります。。
例えば前述の Material UI は公式ドキュメント にも記載があるようにユーザーがカスタマイズしやすいフレームワークですが、このように、見た目のためのプロパティ(=ライブラリ・フレームワークを使う人が調整可能な部分)がどれだけ準備されているかを最初に把握して、自分たちの作るデザインとどれだけすり合わせが必要かを知っておくといいと思います。
最後に
フロントで持つべきデータの形とデータベースに登録するデータの形は必ずしもイコールではない Reactの各ライフサイクルで適切な処理を行う 更新をReactが正しく検知してくれるよう、値はコピーしてまるっと置き換える (おまけ)ライブラリやフレームワークを導入するときはCSSをどれだけカスタマイズできるか注意する
React に限らず、フロントエンドはデータのやり取り・描画・見た目が密接に関連しており、同時に考えることが多いですが、思った通りに動いた時は本当に楽しいです! 使う人に一番近く、いろんなフィードバックをいただけるのもフロント開発の醍醐味だと思います。
少しでも開発の手が止まった時のヒントになれば幸いです。