React のレンダープロップについて

レンダープロップとは

レンダープロップの公式の説明は以下です。

“レンダープロップ (render prop)”という用語は、値が関数である props を使って、コンポーネント間でコードを共有するためのテクニックを指します。

https://ja.reactjs.org/docs/render-props.html

Reactではコンポーネントごとに状態(state)と、その状態を更新するための処理を持たせることがあります。

例えば、countという値と、それをincrementする処理をもっているコンポーネントがあるとします。

ボタンを押すと、押した回数だけcountという値がincrementされます。

このcountとcountをincrementする処理を他のコンポーネントでも使いたくなったとき、同じ処理を別のコンポーネント内に書くことになるので、コードの重複が生じます。

これを防ぐのがレンダープロップというテクニックです。

サンプルでみてみる

先ほどの例をもとにひとつコンポーネントを作成します。

import React, {useState} from 'react'

const ClickCounter = () => {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(prev => prev + 1)
  }

  return (
    <div>
      <button onClick={handleClick}>count: {count}</button>
    </div>
  )
}

export default ClickCounter

ボタンをクリックすると、countを+1しています。

このコンポーネントではボタンを押したときですが、onMouseのときに同じ処理をするコンポーネントが必要になったとします。

以下のようなコンポーネントを作成します。

import React, {useState} from 'react'

const MouseoverCounter = () => {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(prev => prev + 1)
  }

  return (
    <div>
      <div onMouseEnter={handleClick}>count: {count}</div>
    </div>
  )
}

export default MouseoverCounter

return の処理以外が重複しています。

例えば、今はincrementで+1ですが、すべて+5に変更する必要がでたときに、同じ処理を使っているコンポーネントをすべて修正する必要があり、修正漏れが発生するリスクがあります。

次にこれらをレンダープロップを使って書き換えてみます。

状態とそれを管理するロジックだけをもったコンポーネントを作成する

countとcountを更新する処理だけを持ったコンポーネントを作成します。

import React, {useState} from 'react'

const Counter = props => {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(prev => prev + 1)
  }

  return (
    <div>
      {props.render(count, handleClick)}
    </div>
  )
}

export default Counter

return の中が変更されています。

このCounterコンポーネントが受け取ったpropsのrenderを実行しており、引数にcountと更新処理を渡しています。

つまり、Counterコンポーネントは何を描画するかについては関与せず、props.renderによって渡された関数を実行することで描画するだけ、となります。

次にこのCounterコンポーネントを実際に使用してみます。

import React from 'react'
import Counter from './Counter'

const App = () => {
  return (
    <div>
      <Counter render={(count, handleIncrement) => <button onClick={handleIncrement}>count: {count}</button>} />
    </div>
  )
}

export default App

renderには関数が渡されています。

この関数の引数には、先ほどCounterコンポーネントで渡した二つの引数(count とそれを更新する関数)が渡されています。

そして、描画したいbuttonを返しています。

同じようにMouseoverもCounterコンポーネントを使ってみます。

import React from 'react'
import Counter from './Counter'

const App = () => {
  return (
    <div>
      <Counter render={(count, handleIncrement) => <button onClick={handleIncrement}>count: {count}</button>} />
      <Counter render={(count, handleIncrement) => <div onMouseEnter={handleIncrement}>count: {count}</div>} />
    </div>
  )
}

export default App

このようにすることで、状態とその状態を変更する処理を共通化することができます。

レンダープロップはformikなどのライブラリーでも使用されており、使えるようになるとコードの重複がなくなり保守性が上がると思います。

でもカスタムフックの登場によってレンダープロップも下火になるのかな。

https://ja.reactjs.org/docs/hooks-faq.html#do-hooks-replace-render-props-and-higher-order-components

参考