NextJS11でfsなどのbuildを通す方法
ついこの間Next11がリリースされましたね。 Next11では、デフォルトでWebpack5になりました。(引き続きオプションによってWebpack4が使用できます。)
アップデートする際に、webpackに関するエラーが出たので備忘録として記録しておきます。
next.config.jsでは、webpackの設定を変更できる機能がありまして、webpack5だとその書き方が変わってきます。
NextJS 10 の時のwebpack設定
webpack: (config) => { config.node = { fs: 'empty', child_process: 'empty', net: 'empty', dns: 'empty', tls: 'empty', }; return config; },
fsなど、nodeの機能を呼び出す場合にこのようにして、emptyとなるようにしていました。
NextJS 11 のwebpack設定
webpack: (config) => { config.resolve.fallback = { ...config.resolve.fallback, fs: false, child_process: false, net: false, dns: false, tls: false, }; return config; },
書き方が変わり、resolve.fallbackに書かないと行けなくなりました。特にここ以外は問題なく動作しました。
最近はこれよりも、最新バージョンのreact-hook-formの破壊的変更のほうが修正が大変でした。フロントエンドはアップデートが早いので、yarn upgrade-interactive -- latest
で更新していくと良さそうですね。
擬似リアルタイムとリアルタイムの両立
最近開発で少し悩んだことがありまして、 JavaScriptで例えばですがチャット機能のようなものを作っていました。
チャットなのでリアルタイムに流れていきますよね。 投稿時はAjaxで、画面を更新せずにサーバーへPOSTし、チャットメッセージが送られるような仕組みです。
普通に実装すると、チャットメッセージを投稿しても、すぐには画面には表示されません。送信リクエストのレスポンスを受け取り、エラーがなければ画面表示します。
数秒遅れて画面に出る
するとどうでしょう、画面には数秒遅れて表示されます。あんまりリアルタイムに感じません。なので、投稿したらすぐに画面に表示したくなります。
それが、サーバーに送信したと同時にその内容をそのままクライアントにサーバーから受け取ったと見なして表示させてしまう方法です。
これを疑似リアルタイムと呼ぶことにしました。レスポンスを受け取ったときは、前者にはidが無いので、idを差し替えます。
これでリアルタイムにチャットが出来るような見え方になりました。
そこに本当のリアルタイム処理を入れると...
疑似リアルタイムだと、投稿時は本当にすぐに送られているようにみえるのですが、受信はリアルタイムではありません。受信もリアルタイムにするには、「WebSocket」を使用します。他にもWebRTCなどでも可能です。
FirebaseやAppSyncでも手軽に実装可能で、専用サービスを利用するとリアルタイムの実装の敷居は低いです。
このリアルタイム処理を入れると、挙動が怪しくなります。
一瞬二つにみえる
疑似リアルタイムでは、まだidのない疑似投稿を即座に表示させる方法です。リアルタイム投稿にはidがあるものの、疑似投稿のidが分からないために、消して差し替えることが出来ません。
リアルタイム投稿は、投稿時のレスポンスが返ってくる速さよりも早い場合があるため、その間、投稿が二つに見えてしまうのです。
困った。
対策
対策は、今のところ思いついてるものになりますが 「フロントエンドから一意性のあるIDを付与する」となります。このIDはフロントエンドから生成しているので、IDが特別な意味を持たずフロントエンドから改変可能な(改変してもセキュリティに影響のない)IDである必要があります。
なので、本来のIDとはわけた方が良いです。
これにより、投稿時レスポンスまたはリアルタイムによって受け取った投稿に同じIDが合った場合は、差し替えることにより、一瞬二つ表示ことを回避できます。
最後に
もっといい方法あると思うのですが、(メッセージ内容と日付で比較するなど) なかなかこれだという方法はないですね。
ts-resultsで安全にasync awaitを使う
TypescriptやJavaScriptで開発をしていると、async awaitをよく使います。コールバック関数のネストを防ぎ、コードのリーダビリティを保ちながらコードを書くことが出来るため有用なシンタックスです。
awaitの注意点
async functionでは、awaitを使う事ができるようになりますが、非同期処理内でエラーが発生した場合にキャッチする方法がないことに注意が必要です。
const func = async () => { const response = await axios.get('http://....'); console.log('エラーが発生するとここが実行されない'); }
axios内で通信エラーが発生した場合は、例外エラーが発生し、それ以降の処理が実行されません。例外エラーを処理したい場合は、axiosの返すPromiseで.catchを使うしかありません。
const func = async () => { const response = await axios.get('http://....') .catch( error => error); }
このようにすることで、例外エラーがスローされることなく、responseにエラーが返ります。
しかし、この時にresponseの型は AxiosResponse | AxiosError となり、responseにどちらの型が含まれているのかわからなくなってしまいます。
厳密には各型に含まれるフィールドから判定すれば良いのですが、型を推定するにはasを使う必要があり、コード量が増えてしまいます。かといって、.catch内で書いてしまうと、またコールバックのネストになってしまいます。
ts-resultsでラップする
ts-results https://github.com/vultix/ts-results
ts-resultsは、RustにあるOption型やRusult型をTypeScriptで実装したもので、コンパイルタイムで型チェックの機能を持ちます。
import { Ok, Err, Result } from 'ts-results'; const func = async () => { const result = await axios.get('http://....') .then( res => Ok(res)) .catch( error => Err(error)); }
この場合、resultには
Result<AxiosResponse, AxiosError>
という型の値が返ります。 左の型が、Ok型となり、右の型がErrの型となります。 左と右は他の様々な型にすることが出来ます。
OkまたはErrどちらかの状態を持つことが出来るのがこのResult型となります。
エラーかどうか判定する
Result型は、簡単にエラーかどうか判定する事が出来ます。
import { Ok, Err, Result } from 'ts-results'; const func = async () => { const result = await axios.get('http://....') .then( res => Ok(res)) .catch( error => Err(error)); } if (result.err) { return result; }
エラーがある場合は.err、エラーがない場合は.okにtrueが入ります。これによって、エラーがあった場合は処理を進めずに例外エラーを発生させることもなくResult型をそのまま返すことも出来ます。
値を取得する
特にエラーもなく、その後の処理を行いたい場合は
result.val
こちらを使用出来ますが、型の推定がうまくいかない場合があるので、.unwrapを使用します。
import { Ok, Err, Result } from 'ts-results'; const func = async () => { const result = await axios.get('http://....') .then( res => Ok(res)) .catch( error => Err(error)); } if (result.err) { return result; } const response = result.unwrap();
unwrapは、Result型からokだった場合に値を返します。errだった場合は、エラーになるので、あらかじめerrが入っていないか確認する必要があります。
このように、async awaitにts-resultsを使うことによって、安全にシステムを開発する事が出来ます。
React超入門 mapでリストアイテムを繰り返す
この記事ではTypeScriptを使います。
Reactでよく使う実装にArray.mapでのループがあります。 ループというのは繰り返しのことで一つのReactエレメントを繰り返してリスト表示させることができる機能です。
リストって何?
リストとは配列のことで、順番に要素が格納された長い箱のようなものです。
[1,2,3,4,5]
これは、1〜5までの数字が順番に格納された箱です。これがリスト(配列)です。
配列は、数字の他に文字列を入れることも出来ます。
['朝食','りんご','ヨーグルト']
文字列の他にオブジェクトも入ります。
[{key: 'value', 'キー': '値'}, {key: 'value', 'キー': '値'}]
オブジェクトは、キーと値がセットになったデータで、
data.key data['キー']
とすると、値が取得できます。
Reactでは、この配列とオブジェクトを組み合わせてReactエレメントを繰り返して表示させることが多いです。
繰り返す元となる型を作ろう
型とは、データがどういうキーを持っているのか決めたものです。例えば人の名前と年齢を管理するデータであれば、
type Human = { name: string; // 名前 age: number; // 年齢 }
となります。stringというのは文字列であるということを表します。numberは数値です。
Humanデータを作成する
リストアイテムを繰り返すために、このHumanを配列データにしてみます。
const humans: Human[] = [];
複数ある事がわかるように、humansのようにsをつけます。 その後ろにある Human[] が、Humanの配列型です。
今は空が代入されていますが、この配列の中にはHumanしか入れられないように型で縛っているわけです。
const humans: Human[] = [ { name: '斎藤', age: 20}, { name: '中島', age: 21}, { name: '田中', age: 50}, ];
先ほど定義した型定義に従って、データを作っていきます。 ちょっと、リストっぽくなってきたと思います。
Humanを繰り返す
ではいよいよ、このデータをReactエレメントで出力していきます。
const humans: Human[] = [ { name: '斎藤', age: 20}, { name: '中島', age: 21}, { name: '田中', age: 50}, ]; const HumanTag: React.FC = () => { return <> { humans.map( human => { <div key={human.name}> {human.name}さんは{human.age}歳です。 </div> })} </>; }
あとは.mapが配列の要素を一つずつ取り出してくれるので、そこにタグを入れるのみです。この時に必ずkeyを入れ忘れないようにしてください。
keyは必ず重複しないようにしてください。
NextJsでSSRに対応していないライブラリを使う
NextJsで読み込まれたライブラリは、サーバサイドでもレンダリングできるようになっていますが、稀にSSRに対応していないライブラリがあります。ベースがjQueryになっているようなライブラリだとよくあります。
原因は内部でwindowを呼んでいることで、windowが見つからないエラーとなるためです。
この場合は、dynamic import
を使います。
import dynamic from 'next/dynamic'; let Mod; if (typeof window !== 'undefined') { Mod = dynamic(() => import('@mod/module'), { ssr: false, }); }
見たとおりwindowが定義されていない時は、Modはnullとなります。
ライブラリがexport default
で宣言されている場合は下記のようにします。
let Mod; if (typeof window !== 'undefined') { Mod = import('mod').then((module) => module.default); }
Reactで気づいたらすべてがuseになっていた話
ReactはHooksが導入される前から書いていました。同時はconstでコンポーネントを定義するのではなく、class構文で書いていました。今ネットで検索してもclass構文で、stateへのセットもthis.setStateでした。 パフォーマンス改善と利便性のために、constructorには大量の.bindが並ぶそんなコードでした。
class構文からconst定義の関数型コンポーネントになった
コンポーネントが、React.FC型によって関数型のコンポーネントになり、classを定義しなくてもconstで変数を定義するようにコンポーネントを実装出来るようになりました。
setStateがuseStateになった
class構文の時はstateをclassに定義し、初期化を行いthis.setStateでstateの値を変更し、stateを定義する場所が決まっています。setStateするときに、どのstateだっけと定義を見に行ったりしていました。
const [mikan, setMikan] = useState(null);
新しい書き方では、配列の分割代入を使う形になっていて、配列の1番目にはstateそのものの変数、二番目にはstateに代入するための関数が返ります。
初期化処理などはuseEffectになった
当時は初期化処理などは componentDidMount と componentDidUpdate を使っていました。しかし自分は直接renderメソッドに実装を書いてしまっていた記憶があります。非常に良くない書き方をしていました。そもそもメソッドが分かれていたので、双方の関数でデータをやり取りするにはthisを使う必要があり 変更のあったpropsを判別するにはifやswitchで判別する必要がありました。
そのため、非常に煩雑な実装になるため、できるだけreact-reduxのほうに書いたり、renderに書いたりしてしまっていました。
しかし、そんなReactの書き方はもうおしまい! useEffect でも描けるようになりました。
特に嬉しいのは使い勝手がrenderメソッドに直接書いているような感覚で使えることです。ロジックがあれば、それをuseEffectで包めばよいです。第二引数に[] を指定すると、componentDidMount、propsの一部のフィールドを配列で渡すと、componentDidUpdate相当になります。propsのフィールドが更新されたときにuseEffectの第一引数のコールバック関数が再実行されます。
また、useEffect内でreturnで関数を返せば componentWillUnmount相当の処理をしてくれます。
useMemoが便利
useMemoは、何度も繰り返されるライフサイクルの中で、第二引数の配列に渡されたpropsのフィールドの値が変わっていなければ、前回実行した値を使いまわす、キャッシュ機能を持つHookです。挙動としてはuseEffectと一緒で、ただreturnで値を返すことが違います。
便利だからとrender内でついついロジックを書いて変数に代入をしてしまっている場所があれば全てuseMemoに置き換えると良いです。
ただ注意が必要で、useMemoはコンポーネントさえもキャッシュにすることが出来ますが、明示的に再レンダリングさせたいと言った目的がなければ、単純に別コンポーネント化してしまったほうが良いです。コンポーネントの中身が単純なjQueryなどによるDOM相当を行っていて何度も再レンダリング行われてしまったりする場合などは例外的にコンポーネントにたいして使っても良いと思います。
onClickなどの関数はuseCallbackへ
class構文では、onClickに渡す関数はclassのconstructorでthisが使えるようにbindしていました。claasのメソッドなので、thisで定義されたpropsなstateしか呼び出せず、クロージャの変数が使えず、かといってrenderで書くと何度も関数が再定義されたり、クロージャの変数の扱いなどで何かとトラブルの原因になっていました。
それも直接useCallbackを使う事で、関数内に定義できるようになりました。useEffect同様関数自体をキャッシュしてくれるので、第二引数の変数が更新されるまでは中の関数の変数は不変なので、安心してクロージャの変数が使えます。
まとめ: 気づいたらuseで包まなくていい物がなくなった
useを適切に使うと全てがuse系に包まれて、リアクティブな状態になりました。プログラムは何かしたのイベントにより発動し、連鎖的につながって一つの処理が行われていきます。
もし手元に既存の手続き型のレガシーなコードがあったら、一つ一つのロジックを分解してuse系で包み込んでいくだけで、リアクティブなプログラムにリファクタリングすることが出来ます。
どこに書かないと行けないといったコーディングルールを理解する必要は無く、ドミノ倒しやマインクラフトの全自動マシンのように、連鎖反応を起こす仕組みさえ考えるだけでいいのです。
Nextjsでデータベースに接続する時に「Critical dependency: the request of a dependency is an expression」が出る
NextJsではWebpackを使用しているため、モジュールがWebpackに対応していないとCritical dependency: the request of a dependency is an expression
というエラーが発生します。
NextJsでは、Sequelize や knex などでエラーが発生します。
これを回避するには、ウェブから読み込まれる部分とそうでない部分で明確にimportを分けると良いです。
importしたモジュールがWebpackに対応していない場合にそのページをWebから読み込んでしまうとエラーが発生するため、必ずWebpackがコンパイルせずにサーバサイドのみで実行するファイルで読み込みます。
DIを使っている場合は、明確にWebpack部分とバックエンド部分でDIコンテナを分けると良さそうです。