#1. Reactにもモデルが必要
Reactにはモデルはありません。しかしデータをuseStateなどに格納していると、一つのデータが更新されるたびに再描画がかかってしまいます。小さいプロジェクトであれば問題ありませんが、大きなプロジェクトでは再描画はサーバやクライアントにとって大きな負担となり得ます。ユーザデータは以下のようなモデルクラスを定義してそこに格納し、可能な限りuseStateの呼び出しを減らしましょう。
class AppData
{
public data : string = ""
}
export const appData = new AppData()
export const AppContext = React.createContext(appData)
Contextを使っています。Contextの使用方法については公式ドキュメントなどを参照ください。この記事ではContextを利用した簡単な再描画通知・ステート管理の仕組みを紹介します。
#2. API呼び出しはモデルの内部に書く
コンポーネント内部で直接APIを呼んでデータを更新するのはやめ、モデルに更新関数を定義しましょう。これにより一連の更新処理を一つの更新関数にまとめ、コードが見やすくなるとともに、コンポーネント内でのuseEffectを減らし、依存関係を簡単にすることができます。APIを複数呼ぶ場合、それぞれのデータに対してuseStateを使用するとその都度再描画がかかってしまいますが、モデル内のデータであればそのようなことは起こりません。
class AppData
{
public data : string = ""
public async update()
{
//API呼び出し&データ更新
}
}
#3. ビューの更新
さて、データをユーザ定義のモデルクラスに格納したため、ビューが自動で更新されなくなりました。Contextを利用すると、その内部のデータが更新されるたびに再描画が走ると誤解している人がいますがそんなことはありません。Reactによるデータ変更検知はシャローチェックです。ここから適切なタイミングで再描画をビューに通知してあげる仕組みを作ります。まずは単純な通知機能です。
function useSignal() : () => void
{
const [, set] = useState(new class{}())
return () =>{ set(new class{}()) }
}
これをコンポーネント内で呼び出せば、内部的にuseStateを呼び出し、再描画通知関数を返します。これが便利なのはフォームです。公式ドキュメントではフォームデータをuseStateに格納していますが、モデル内の他の関数や他のコンポーネントから簡単に参照できるのでフォームデータもモデル内に格納すべきです。データは一カ所に集めることを心がけましょう。それにより余計なAPI呼び出し・ローカルストレージ・クエリパラメタ・クッキーの操作も減らせるかもしれません。データをファイルに書き出したりするときも扱いやすいですね。
const Component () =>
{
const a = useContext(AppData)
const redraw = useSignal()
const handleChange = (e : any) =>
{
// aのデータを更新して再描画
a.data = e.target.value
redraw()
}
return(
<textarea value={a.data} onChange={handleChange}></textarea>
)
}
#4. 通知機能付きモデルクラス
モデルというからにはモデル内のデータが変更されたことをビューに通知する機能が必要です。以下のようなクラスを作成しました。
class Model
{
private notify : (n : any) => void = (n : any) => {}
public changed(): void
{
this.notify(new class{}())
}
public setNotifier(n : any) : void
{
this.notify = n
}
public resetNotifier() : void
{
this.notify = (n : any) => {}
}
}
またModelクラスを受け取ってModelにuseStateの更新関数をセットするuseModelを定義します。
function useModel(data: Model) : void
{
const [, set] = useState(null)
data.setNotifier(set)
}
先程のユーザモデルクラスをModelクラスを継承するように修正します。更新関数では更新が完了したタイミングでchangedという再描画を通知する関数を呼び出しています。
class AppData extends Model
{
public data : string = ""
constructor()
{
super()
}
public async update()
{
//API呼び出し&データ更新
super.changed()
}
}
コンポーネントではuseSignalの代わりにuseModelを呼び出します。changedが呼び出されると、このコンポーネントが再描画されます。useSignalと違うのは、更新通知関数をモデル側やモデルを利用する別のコンポーネントからも呼び出すことができることです。タイマを利用した定期更新や、このコンポーネントの外側で発生したイベントによる再描画指示などに便利です。
const Component () =>
{
const a = useContext(AppData)
useModel(a)
// 生成・消滅を繰り返すコンポーネントの場合は
// 消滅時に通知機能をリセットする
useEffect(()=>{
return () => a.resetNotifier()
},[])
const handleChange = (e : any) =>
{
// aのデータを更新して再描画
a.data = e.target.value
a.changed()
}
return(
<textarea value={a.data} onChange={handleChange}></textarea>
)
}
#5. ステート管理機能付きモデルクラス
さらにステート管理を追加しましょう。以下のようなクラスを作成しました。ここではステート遷移の整合性をチェックしていませんが、必要に応じて実装してもよいでしょう。
class StateModel<StateType> extends Model
{
private state : StateType
public constructor(initialState : StateType, bRedrawNow=false)
{
super()
this.state = initialState
if(bRedrawNow){
super.changed()
}
}
public setState(state : StateType, bRedrawNow=true) : void
{
if(this.state !== state){
this.state = state
if(bRedrawNow){
super.changed()
}
}
}
public getState() : StateType
{
return this.state
}
}
このクラスを継承するように、ユーザモデルクラスを再修正します。更新関数が開始したタイミングでsetState('loading')、完了したタイミングでsetState('complete')を呼び出します。
class AppData extends StateModel<'idle'|'loading'|'complete'>
{
public data : string = ""
constructor()
{
super('idle')
}
public async update()
{
super.setState('loading')
//API呼び出し&データ更新
fetch()
.then(result : any){
this.data = result
super.setState('complete')
}
}
}
コンポーネントではsetStateが呼び出されるたびに再描画が発生し、getStateの返り値に応じた描画が行われます。
const Component () =>
{
const a = useContext(AppData)
useModel(a)
switch(a.getState()){
case 'idle': return null
case 'loading': return <div>お待ちください...</div>
case 'complete': return <div>完了</div>
}
}
以上です。