Hero img
React Recoilを使ってみる。

Recoilを使った時のobjectを更新した際のレンダリング制御

reactのグローバルステートのライブラリーであるRecoilを使って見ました,とてもいい感じですが、再レンダリングが発生することが多く困った事になり、selectorやselectorFamilyを使うことでレンダリングを抑制出来ました。


目次

  • React recoil
  • React recoilを使う
  • recoilを使うためには
  • 初期設定
  • インポート
  • Atomを作成する。
  • App.jsで状態を取得
  • 値の変更
  • Atomとは?
  • reactの再レンダリング制御
  • useSetRecoilStateとuseRecoilValue
  • objectのレンダー制御
  • objectの値の更新と、取得
  • 再レンダリングが発生するダメな例
  • selectorを使う
  • プロジェクトファイル
  • まとめ

React recoil

React recoilは比較的に新しく登場した、状態管理ライブラリーです。グローバルステートの状態管理です。
状態管理ライブラリーはRedux かContext APIが有名どころ単純なアプリであれば、Contextで代用できるが、ソースコードが膨大になって来ると、ちょっと困ったりするので、状態管理のライブラリーはとりあえずReduxかRecoilを使えば良いのではないか? Reduxは個人的には使いづらかったが、個人差なので...

React recoilを使う

公式

recoilを使うためには

recoilを使うためには

  1. 1.ライブラリーインストール
  2. 2.recoilをインポートして、親となるコンポーネントをRecoilRootで囲む
  3. 3.atomを作成する。
  4. 4.atomとrecoilstateをインポートして、値の取得、保存をする。

私は、ちょっと??!Atomってなんだよ。ってなってしまいましたが、Recoilを使う上で一番理解しないといけない部分だった。(後程補足します。)

初期設定

インストールはコマンド一行。

npm install recoil

インポート

次にrecoilをインポートする。
ここは公式と異なった書き方をしました。
公式はapp.jsに書いていますが、index.jsに書きます。
<App/>を<RecoilRoot>囲むだけなのでどちらでも動きますが、index.jsに書いた方が良いんじゃないかな?

index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot } from 'recoil'; // RecoilRootをインポートしていることを確認
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
  <RecoilRoot>
    <App />
  </RecoilRoot>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
/*
<App/>となっているところを<RecoilRoot>で囲むだけです。
*/

Atomを作成する。

適当なjsファイルを作成します。  

公式のコードをちょこっと修正しています。公式ではimportが書かれていませんね....

src/atom.js
import { atom } from "recoil";
export const TextState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: 'Defaulttext', // default value (aka initial value)
});

App.jsで状態を取得

recoilはusestateとほとんど同じ使い方なのでそこまで難しくないと思いますが、先ほど作ったatom.jsをインポートする必要がありますのでその点だけ注意ください。

App.js
import {useRecoilState} from "recoil";
import {TextState} from "./atom";

function App() {
  const [text, setText] = useRecoilState(TextState);

  return(
<>
<p>
  {text}
</p>
</>
  )
}
export default  App
起動コマンド
npm run start

ブラウザーで確認

recoil-defaulttext

ブラウザーで確認するとatomのdefaultに書いた'Defaulttext'がちゃんと取得できます。

値の変更

usestateと同じように使うため比較的に分かりやすいですね。

app.js
import {useRecoilState} from "recoil";
import {TextState} from "./atom";
function App() {
  const [text, setText] = useRecoilState(TextState);
  const ButtonClick = (()=>{
    setText(new Date().toString())//ここ
  })
  return(
<>
<button onClick={ButtonClick}>ChangeRecoilStates</button>
<p>
  {text}
</p>
</>
  )
}
export default  App
recoil-change-to-time

とりあえ動かすことができた。

Atomとは?

atomとは何か?
データストアとか単位とか状態とか色々書かれているがしっくりこない。
要するにグローバルステート。つまりvar TextState ="Defaulttext" というグローバル変数ではないのか? 結局何? == グローバルステートの箱?

reactの再レンダリング制御

usesateと同様に値が更新されると、再レンダリングが発生するので、recoilを使う場合は再レンダリングを気にしないといけない。
先ほどのapp.jsを少し変えてみた。

更新される値を取得してないけど、再レンダリングが発生してしまう。

app.js
import {useRecoilState} from "recoil";
import {TextState} from "./atom";
function App() {
  const [text, setText] = useRecoilState(TextState);
  const ButtonClick = (()=>{
    setText(new Date().toString())
  })
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
<>
<button onClick={ButtonClick}>ChangeRecoilStates</button>
<p style={style}>
  文字色が変わればレンダリングされている
</p>
</>
  )
}
export default  App
recoil-rerender

useSetRecoilStateとuseRecoilValue

先のレンダリングされる現象は、useRecoilStateを使うと、値を取得して使わない場合であっても再レンダリングが発生してしまいます。
そこで登場するのがuseSetRecoilStateとuseRecoilValueです。
useRecoilStateは値の変更と取得の両方の機能をもっていますが、

  • useSetRecoilStateは値の変更。
  • useRecoilValueは値の取得。

更新のコードと、取得のコードが別々になっています。 そのため、useSetRecoilStateを使えば、値の変更ではレンダリングが発生しません。

app.js
import {useRecoilValue ,useSetRecoilState} from "recoil";
import {TextState} from "./atom";
function App() {
    const asetText = useSetRecoilState(TextState);
  const ButtonClick = (()=>{
    asetText(new Date().toString())
  })
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
<>
<button onClick={ButtonClick}>ChangeRecoilStates</button>
<p style={style}>
  文字色が変わればレンダリングされている
</p>
<Appdemo/>
</>
  )
}

const Appdemo=(()=>{
const gettext = useRecoilValue(TextState);
const getColor = () => Math.floor(Math.random() * 255)
const style = {
  color: `rgb(${getColor()},${getColor()},${getColor()})`,
}
return(
  <p style={style}>
  子のコンポーネントです:{gettext}
</p>
)
})


export default  App
recoil-rerender-setandgetvalue

画像のように親のは変わらず、子だけが再レンダリングされていることが分かります。

objectのレンダー制御

グローバルステートを使う時は単一の文字データだけではなく、objectやarrayを使いたいことがほとんどだと思います。

例の為にatom.jsとapp.jsを修正してみます。

atom.js
import { atom } from "recoil";
export const TextState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: 'Defaulttext', // default value (aka initial value)
});

//以下を追加
export const Userinfo = atom({
    key: 'yumenoinfo', // unique ID (with respect to other atoms/selectors)
    default: {
        name:"yumeno",
        id:"001",
        password:"abcd",
        country:"jp",
    }, 
  });
app.js
//Appdemoのコンポーネントを修正
const Appdemo = (() => {
  const gettext = useRecoilValue(TextState);
  const yumenoinfo = useRecoilValue(Userinfo);
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return (
    <div>
      <p style={style}>
        子のコンポーネントです:{gettext}
      </p>
      <ul>
        <li>名前:{yumenoinfo.name}</li>
        <li>ID:{yumenoinfo.id}</li>
        <li>パスワード:{yumenoinfo.password}</li>
        <li>:{yumenoinfo.country}</li>
      </ul>
    </div>
  )
})

変更し、次のような画面になります。

  recoil-object-sample


objectの値の更新と、取得

ではobjectの値の更新と取得をするには?

  • objectの一部を変えるときは?
  • objectの一部を取得する時は?

どうすればいいのかというと

  • 値の更新は分割代入を使う
  • 値の取得はselectorを使う

再レンダリングが発生するダメな例

レンダリング結果が分かるようにコンポーネントを小分けします。
無駄にコードを分けているので長くなって申し訳ないです。

app.js
import { useRecoilValue, useSetRecoilState , useRecoilState } from "recoil";
import { TextState, Userinfo } from "./atom";
function App() {
  const asetText = useSetRecoilState(TextState);

  const ButtonClick = (() => {
    asetText(new Date().toString())
  })
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return (
    <>
      <button onClick={ButtonClick}>ChangeRecoilStates</button>
      <p style={style}>
        文字色が変わればレンダリングされている
      </p>
      <Appdemo />
      <NameComponent/>
      <IDComponent/>
      <PWComponent/>
      <CountryComponent/>
    </>
  )
}

const Appdemo = (() => {
  const gettext = useRecoilValue(TextState);

  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  const [yumenoinfo,setyumenoinfo] = useRecoilState(Userinfo);
  const ChangeYumenoid = (()=>{
    setyumenoinfo({...yumenoinfo , id : "002"})
  })
  return (
    <div>
      <button onClick={(()=>ChangeYumenoid())}>YumenoidChange</button>
      <p style={style}>
        子のコンポーネントです:{gettext}
      </p>

    </div>
  )
})

const NameComponent = () =>{
  const yumenoinfo = useRecoilValue(Userinfo);
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
    <p style={style}>
      名前:{yumenoinfo.name}
    </p>
  )
}

const IDComponent = () =>{
  const yumenoinfo = useRecoilValue(Userinfo);
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
    <p style={style}>
      ID:{yumenoinfo.id}
    </p>
  )
}

const PWComponent = () =>{
  const yumenoinfo = useRecoilValue(Userinfo);
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
    <p style={style}>
      PW:{yumenoinfo.password}
    </p>
  )
}

const CountryComponent = () =>{
  const yumenoinfo = useRecoilValue(Userinfo);
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
    <p style={style}>
      :{yumenoinfo.country}
    </p>
  )
}


export default App
recoil-changeuserinfo

IDだけを変更しているのに、名前、ID,PW,国の全てが再レンダリングされてしまっている.... これを解消できるのがselectorです。

selectorを使う

objectでIDだけを変更したが、その他の全ても再レンダリングしてしまう。
そこでselectorまたはselectorFamilyを使います。
この2つの違いはselectorFamilyは引数を受け取ることができ、 selectorは引数を渡すことができません。

国と名前の部分のみselectorを使ってレンダリングの抑制をします。

atom.js
import { atom, selector , selectorFamily } from "recoil";//<--importする
export const TextState = atom({
  key: 'textState', 
  default: 'Defaulttext', 
});


export const Userinfo = atom({
    key: 'yumenoinfo', 
    default: {
        name:"yumeno",
        id:"001",
        password:"abcd",
        country:"jp",
    }, 
  });

//selectorFamilyを追加
  export const AtomselectorFamilydemo = selectorFamily({
    key: 'getyumenoinfo' , 
    get: (key) =>({get}) =>{
        let yumenoinfo = get(Userinfo);
        return yumenoinfo[key];
    }
})

//selectorを追加
export const Getusercountry = selector({
    key: 'getyumenoinfo' , 
    get: ({get}) =>{
        let yumenoinfo = get(Userinfo);
        return yumenoinfo["name"];
    }
})

app.jsも修正します。

app.js
//AtomselectorFailydemoをatom.jsで作成したので、インポートします。
import { TextState, Userinfo , AtomselectorFamilydemo } from "./atom";

const NameComponent = () =>{
  const yumenoinfo = useRecoilValue(AtomselectorFamilydemo("name"));//<--引数を渡せる。ここではnameを渡している。
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
    <p style={style}>
      名前:{yumenoinfo}
    </p>
  )
}


const CountryComponent = () =>{
  const yumenoinfo = useRecoilValue(Getusercountry);//引数は渡せない。
  const getColor = () => Math.floor(Math.random() * 255)
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  }
  return(
    <p style={style}>
      :{yumenoinfo}
    </p>
  )
}

名前と国の部分だけ再レンダリングが発生しなくなりました。

recoil-family

このようにselectorを使うことで、objectの一部が更新され、関係がない場合は再レンダリングが発生しないようになります。

プロジェクトファイル

gitに今回のプロジェクトファイルをアップしました。

まとめ

reocilを使えばコンポーネント分かれていても値を取得でき、更新もすることができる。atomのjsを作り、importすれば何処のコンポーネントでもグローバルステートとして使えるので、個人的このライブラリーが使いやすい。

覚えて置く事は、recoilを使う為にはatomファイルをまず作成し、余計な再レンダリングが発生しないように、selectorを使う。 objectの一部を更新する場合はusesateと同様分割代入を使う。

  • atomを作ってデータの箱を先に作る。その時defaultは必ず何か入れて置くこと。
  • データの更新だけをする場合はuseSetRecoilStateを使う。
  • データの取得だけをする場合はuseRecoilValueを使う。
  • データ取得コードを入れると再レンダリングが発生する。
  • useRecoilStateはusestateと使い方が一緒。
  • objectの一部だけを取得する際はselector,selectorFamilyを使う。

関連記事

コメント

コメントを書く

質問や、間違いがありましたら、お気軽にどうぞ

※お名前とコメントの内容を入力してください。
※全てこのブログ上に公表されます。
※許可なく管理者によって修正・削除する場合がございます。 詳しくはプライバシーポリシーを参照ください