react入門6

reactルールの世界と外(JS)の世界の扉:useEffect

reactは、javascriptを関数型プログラミングに拡張したような言語です。関数型プログラミングというのは、簡単にいうと、入力propsと内部状態stateが同じなら常に同じ出力 (同じレンダリング、子コンポーネント呼び出し)が出る関数のみでプログラミングしましょうと言う考え方です。そうすると全体としても同じ入力なら同じ出力が出るので、コンポーネント毎にテストすれば、システム全体としてバグが出にくいと言う思想です。
しかし現実的には、グローバルな状態管理が欲しくなり、またreact ルール外の世界とのやり取りが必要になります。グローバルな状態管理については、入門5で触れましたので、今回は、react ルール外の世界、つまり外部APIの利用や、reactルールでないJSのライブラリの活用について記載します。

外部APIとのやり取り

まずは、外物APIとして、以下のAPIを利用させてもらいます。

以下のような形でラップしたクラスを作ります。このクラスのインスタンスを生成し、そのメンバ関数getDataを呼び出すと、引数(数字の1から200)に応じたJSON(todoリスト)を返します。これをReactで使うことを考えます。(fetch,promise,async/awaitについてご存じない方はググってみてください。)

class GetTodoJson{
    constructor(){
        this.base_url = "https://jsonplaceholder.typicode.com/todos/"
    }

    async getData(num){
        const response = await fetch(this.base_url + String(num))
        const data = await response.json()
        return data
    }

}
export default GetTodoJson

const getTodo = new GetTodoJson()
let json = await getTodo.getData(199)//1 to 200
console.log(json)

さて、呼び出し順の概念がない関数型プログラミングで構成されたreactに、どのように組み込めば良いでしょうか。
2つの観点が必要になります。一つは、外の世界との情報のやり取りです。もう一つは、実行のタイミングです。
まず、外の世界との情報のやり取りについて考えます。reactの世界には、関数コンポーネントの中に、情報を格納できる変数の種類が2つありました。親コンポーネントから読み取り専用として与えられるpropsと、コンポーネントの内部の状態を保持し、専用の関数で値を書き換えるstateです。外の世界の関数にpropsやstateを渡したり、外の世界の関数の出力をstateを書き換える関数に入れることで、外の世界とコンポーネントの情報をやりとりできそうです。
次に、実行のタイミングについて考えます。そもそも関数コンポーネント(とその中のレンダリング)が実行されるタイミングがありました。

  • 初回レンダリング

  • 親コンポーネントが再レンダリングされ呼び出された時

  • propsが変化した時

  • stateが変化した時

ここで、考えなければならないことがあります。レンダリングの際に、外の世界の関数を使う処理でstateを書き換えた場合、再レンダリングされ、無限ループに落ち入ります。AWSでサーバーを立て、外部APIアクセスするなどしていた場合、外部通信のあるAPIを無限ループで呼び出すなど、あっという間に破産しそうです。何らかの方法で実行タイミングを制御する必要があります。
reactには、上記を備えた機能useEffectがあります。useEffectは、関数コンポーネントの中で次のように使われます。

useEffect(()=>{外の世界の関数やクラス,props,stateを使った処理},[トリガとなる変数(配列)])

useEffectの第一引数は、処理する関数を入れます。ただしこの関数はreturn を返さない、第二引数はタイミング制限のため、トリガになる変数を入れます。トリガになる変数が変化した時のみ、第一引数の処理が走ります。第二引数を空の配列[]にしたときは、初回のレンダリング前に1度だけ実行されます。第一引数の処理の中でstateを書き換えている場合など、絶対に第二引数を忘れないようにしてください。また、書き換えたstateなどの変数は第二引数にいれないようにしてください。

長くなりましたが、先ほど作成したGetTodoJsonクラスをReactの世界に取り込みます。inputで数字を入力させ、その数値の変化をトリガとしてuseEffect内でgetData関数を呼び出し、その結果を表示するというものです。


//index.js

import React, { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
    
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
    <StrictMode>
        <App/>
    </StrictMode>
)
//GetTodoJson.js

class GetTodoJson{
    constructor(){
        this.base_url = "https://jsonplaceholder.typicode.com/todos/"
    }

    async getData(num){
        const response = await fetch(this.base_url + String(num))
        const data = await response.json()
        return data
    }

}
export default GetTodoJson
import { useState } from "react"
import { useEffect } from "react"
import GetTodoJson from "./GetTodoJson"

const App = (props)=>{

    const [num,setNum] = useState(1)
    const [out,setOut] = useState("")

    const handle_change = (e) =>{
        setNum(e.target.value)
    }

    useEffect(()=>{
        const getjson = async()=>{
            const getTodo = new GetTodoJson()
            let json = await getTodo.getData(num)//1 to 200
            //console.log(JSON.stringify(json))
            setOut(JSON.stringify(json))
        } 
        getjson()
    }
    ,[num])

    return(

        <>
            <input type="number" onChange={handle_change}></input>
            <p>{out}</p>
        </>
    )
}

export default App

useEffectの第一引数の関数の中で、処理する内容を関数getjsonにまとめ、直後にその関数getjsonを呼び出しています。これには2つの理由があります。
一つはメモリリークを避けるためです。getjson内でnewによりインスタンスを生成しています。2つ目の理由はgetjson内でawaitをしているため、getjsionをasync関数として定義しなければならなかったためです。

その機能実現に本当にuseEffectは必要か

useEffectを用いる大きな理由として、タイミングを制御するというのがありました。でも、今回はユーザのイベントをトリガとしているので、イベントハンドラ内で外部APIを呼び出せばいいんじゃないと思った人、正解です。
下記でも動きます。

import { useState } from "react"
import GetTodoJson from "./GetTodoJson"

const App = (props)=>{

    const [out,setOut] = useState("")

    const handle_change = (e) =>{

        const getjson = async()=>{
            const getTodo = new GetTodoJson()
            let json = await getTodo.getData(e.target.value)//1 to 200
            setOut(JSON.stringify(json))
        } 
        getjson()
    }

    return(

        <>
            <input type="number" onChange={handle_change}></input>
            <p>{out}</p>
        </>
    )
}

export default App

でも、以下は意図した動作ではありません。

import { useState } from "react"
import GetTodoJson from "./GetTodoJson"

const App = (props)=>{

    const [num,setNum] = useState(1)
    const [out,setOut] = useState("")

    const handle_change = (e) =>{
        setNum(e.target.value)
        const getjson = async()=>{
            const getTodo = new GetTodoJson()
            let json = await getTodo.getData(num)//1 to 200
            setOut(JSON.stringify(json))
        } 
        getjson()
    }

    return(

        <>
            <input type="number" onChange={handle_change}></input>
            <p>{out}</p>
        </>
    )
}

export default App


useEffectについては下記を一読することをお勧めします。


今後

reactは、javascriptを関数型プログラミングに拡張したような言語の位置付けです。プラスアルファとしてuseEffect,useContextなどの機能が提供されていますが、どちらかというとライブラリ作成者に向けたプリミティブな機能です。いろいろな用途に使えるけれど、例えば外部API呼び出しの際のエラー確認や、関数コンポーネントをまたぐグローバルな状態管理にキチンと使おうと思うと、多くの検討と実装が必要になります。また、見た目についても、HTMLとCSSを0から書くのではなく、専用のライブラリを使用することが一般的になってきます。つまり素のreactだけでサービスを作成するには、かなりのリソースが必要となります。
入門4の家の建築で例えるなら、サービスを家とすると、reactやそれが提供するuseEffect,useContextといった機能は、材料、木材や金属といった材料です。大企業は、材料から家具や建材を作り、家具や建材から家を建てるところまでできるでしょう。
しかし、個人や小さい組織でサービスを作ろうとすると、家具や建材を用意するリソースはありません。代わりに、家具や建材に相当するフレームワークやライブラリ、ミドルウェアが沢山あります。
今後は、家具職人向けではなく、建築家向け、つまりreactの上に構築されたフレームワークやライブラリを活用して、サービスを作るための情報を乗せていこうと思っています。

この記事が気に入ったらサポートをしてみませんか?