LoginSignup
0
4

More than 1 year has passed since last update.

Reactの再描画を減らそう。ついでにステートも管理しよう。

Last updated at Posted at 2021-12-24

1. Reactにもモデルが必要

Reactにはモデルはありません。しかしデータをuseStateなどに格納していると、一つのデータが更新されるたびに再描画がかかってしまいます。小さいプロジェクトであれば問題ありませんが、大きなプロジェクトでは再描画はサーバやクライアントにとって大きな負担となり得ます。ユーザデータは以下のようなモデルクラスを定義してそこに格納し、可能な限りuseStateの呼び出しを減らしましょう。

model.ts
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を使用するとその都度再描画がかかってしまいますが、モデル内のデータであればそのようなことは起こりません。

model.ts
class AppData
{
    public data : string = ""
    public async update()
    {
        //API呼び出し&データ更新
    }
}

3. ビューの更新

さて、データをユーザ定義のモデルクラスに格納したため、ビューが自動で更新されなくなりました。Contextを利用すると、その内部のデータが更新されるたびに再描画が走ると誤解している人がいますがそんなことはありません。Reactによるデータ変更検知はシャローチェックです。ここから適切なタイミングで再描画をビューに通知してあげる仕組みを作ります。まずは単純な通知機能です。

useModel.tsx
function useSignal() : () => void
{
    const [, set] = useState(new class{}())
    return () =>{ set(new class{}()) }
}

これをコンポーネント内で呼び出せば、内部的にuseStateを呼び出し、再描画通知関数を返します。これが便利なのはフォームです。公式ドキュメントではフォームデータをuseStateに格納していますが、モデル内の他の関数や他のコンポーネントから簡単に参照できるのでフォームデータもモデル内に格納すべきです。データは一カ所に集めることを心がけましょう。それにより余計なAPI呼び出し・ローカルストレージ・クエリパラメタ・クッキーの操作も減らせるかもしれません。データをファイルに書き出したりするときも扱いやすいですね。

component.tsx
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. 通知機能付きモデルクラス

モデルというからにはモデル内のデータが変更されたことをビューに通知する機能が必要です。以下のようなクラスを作成しました。

useModel.tsx
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を定義します。

useModel.tsx
function useModel(data: Model) : void
{
    const [, set] = useState(null)
    data.setNotifier(set)
}

先程のユーザモデルクラスをModelクラスを継承するように修正します。更新関数では更新が完了したタイミングでchangedという再描画を通知する関数を呼び出しています。

model.ts
class AppData extends Model
{
    public data : string = ""
    constructor()
    {
        super()
    }
    public async update()
    {
        //API呼び出し&データ更新
        super.changed()
    }
}

コンポーネントではuseSignalの代わりにuseModelを呼び出します。changedが呼び出されると、このコンポーネントが再描画されます。useSignalと違うのは、更新通知関数をモデル側やモデルを利用する別のコンポーネントからも呼び出すことができることです。タイマを利用した定期更新や、このコンポーネントの外側で発生したイベントによる再描画指示などに便利です。

component.tsx
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. ステート管理機能付きモデルクラス

さらにステート管理を追加しましょう。以下のようなクラスを作成しました。ここではステート遷移の整合性をチェックしていませんが、必要に応じて実装してもよいでしょう。

useModel.tsx
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')を呼び出します。

model.ts
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の返り値に応じた描画が行われます。

component.tsx
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>
    }
}

以上です。

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4