2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactをやるなら知っておきたい"純粋関数と副作用"の考え方

Last updated at Posted at 2024-07-25

イントロダクション

問題です。
以下2つの関数のうち、どちらが純粋関数でしょうか?
どちらも単純な名前の入力フォームになります。

form1.tsx
const Form = ({ name, setName }: FormProps) => {
  return (
    <div>
      <label>名前</label>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
};
form2.tsx
const Form = () => {
  const [name, setName] = useState('')
  return (
    <div>
      <label>名前</label>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
};

純粋関数

問題の答えの前に、そもそも純粋関数とは何でしょうか?
純粋関数とは「同じ値を入力したら、同じ答えが返ってくる関数」のことです。

例えば

const multiply = (a: number, b: number) => a*b

は純粋関数です。
計算すると以下のようになります。

multiply(5, 6) // 30
multiply(7, 9) // 63

1年後計算してもこうなります。

multiply(5, 6) // 30
multiply(7, 9) // 63

皆様のパソコンでも全く同じ答えが返ってくることでしょう。
同じ値を入力したら同じ答えが返ってくることが保証されています。
これが純粋関数です。

純粋関数ではない関数

代表例はこいつです。

const getNowTime = () => new Date()

私のパソコンで今計算したところ、こうなりました。

getNowTime() // Thu Jul 25 2024 12:03:14 GMT+0900 (Japan Standard Time)

しかし、皆様のパソコンで計算してみてください。上の値になりますか?
なりませんよね?
同じ値を入力している(この場合は0個の引数)のにです。

これは純粋関数ではありません。

これを副作用のある関数と呼びます。
ここでいう副作用はパソコンのシステムクロックやOSの時間を返すAPIなどになるでしょう。

最初の関数を考えてみよう

form1.tsx

以下の関数は純粋関数であることが分かります。

form1.tsx
const Form = ({ name, setName }: FormProps) => {
  return (
    <div>
      <label>名前</label>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
};

Form関数はすなわち
「引数がname,setNameであり

{
    type: "div",
    props: {
        children: [
            {
                type: "label",
                ...
            },
            {
                type: "input",
                props: {
                    value: name,
                    onChange: setName,
                    ...
                }
            }
        ]
    }
}

のようなJSX.Element型の値を返す純粋関数となります。(オブジェクトの詳細は異なるでしょう。イメージです。)
(※DOMが描画されることは保証してくれません。この関数はDOMではなく、あくまでJSX.Elementのデータを返す関数だと思ってください。このオブジェクトを描画する動作はReact本体に委ねられています。)

同じ値を入れたら、絶対に同じJSX.Element要素を返してくれます。

form2.tsx

それに比べて、以下は純粋関数ではありません。

form2.tsx
const Form = () => {
  const [name, setName] = useState('')
  return (
    <div>
      <label>名前</label>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
};

返す構造はさっきと同じ

{
    type: "div",
    props: {
        children: [
            {
                type: "label",
                ...
            },
            {
                type: "input",
                props: {
                    value: name,
                    onChange: setName,
                    ...
                }
            }
        ]
    }
}

なのですが、ここを見てください。

const [name, setName] = useState('')

DOM要素を返す前にある関数が実行されています。
ちなみにuseStateは純粋関数ではありません。同じ''という値を入れても、違う結果が返ってくる可能性がありますよね?
でないと状態管理ができませんから。

純粋関数ではない関数から受け取った値を、代入した値を入れたObjectを返却しているわけなので、form2.tsxのForm関数は純粋関数ではないのです。

で、この知識が何の役に立つのか?

Reactが思った挙動をしないときに、原因が分かります。

例えばこちら

const List = ({ defaultList }: ListProps) => {
  const [items, setItems] = useState(defaultList)
  return (
    <ul>
      {items.map((item, i) => (
        <li key={i}>{item.name}</li>
      ))}
    </ul>
  )
}

何らかのデフォルトリストを受け取って、それを描画しています。
またitemを内部で削除するようなUIもあるかもしれないので、stateで持っています。

仮にリストにも色々タイプがあったりして(食べ物リスト,スポーツリストなど)、defaultListも変わりうるとしましょう。
その場合、defaultListが親要素で変更されても、Listのitemsは変更されません。

で、それはuseStateが純粋関数でないことに起因します。
useStateは純粋関数ではないので、違うdefaultListを代入しても、同じ要素を返す可能性もありますし、同じdefaultListを代入しても、違う要素を返す可能性もあります。

defaultListが変更された際、itemsをリセットしたい時は、以下のような処理を書きます。

const List = ({ defaultList }: ListProps) => {
  const [items, setItems] = useState(defaultList)
+
+ useEffect(() => {
+   setItems(defaultList)
+ }, [defaultList])
+
  return (
    <ul>
      {items.map((item, i) => (
        <li key={i}>{item.name}</li>
      ))}
    </ul>
  )
}

以下のような手順になります。
①まず、defaultListが変更された際に、List関数が再実行されます。(再レンダリングされます)
②useStateでは前の状態がitemsに代入されます。
③しかし、useEffect関数は前のdefaultListとの比較を行い、変更されていることを検知し、setItems(defaultList)を実行します。
④List関数が再実行されます。(再レンダリングされます)
⑤useStateでは先ほどsetItemsで代入した、変更されたdefaultListが代入されます。
⑥めでたし。めでたし。

このケースだとuseState([])でも問題ないです。
初回もuseEffectが代入してくれるからです。
useState([])の場合、useEffectは描画が終わってから、実行されるので、ややラグが発生します。

あと、レンダリング前に実行してくれるuseLayoutEffectでも実装できます。

おまけ

Reactで以下のJSXを描画すると、永久に値が変わらない、無意味なinputが完成することはご存知でしょう。

const MeaningLessForm = () => {
  return <input value="meaningless" />
}

ですが下のJSXで描画されたinputであれば、自由に入力できます。

const FreeForm = () => {
  return <input />
}

あれ?FreeFormは純粋関数ではない?と思うかもしれませんが、ご安心ください。FreeFormは純粋関数です。
先ほど申した通り、FreeFormはDOMを返す関数ではなく、JSX.Elementを返す関数です。ちゃんと毎回同じJSX.Elementを返してくれます。

Reactは id="root" 内にあるイベントを常に監視しており、onChangeが実行されると、DOMの再描画を行うわけです。
そこで MeaningLessForm の Object には value 値が含まれているため、そこでvalueの値が代入されて、結果全く変更されないように見えます。
しかし、FreeFormにはvalue値が存在しないため、特に変更はされません。

なので、永遠に空文字のinputを作成したいなら(どういう動機?)

const FreeForm = () => {
  return <input value="" />
}

とするのが正解です。

読んでいただきありがとうございました。
何かご指摘や疑問点があれば、コメントいただけると幸いです。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?