落とし穴

cloneElement οΏ½?δ½Ώη”¨γ―δΈ€θˆ¬ηš„γ§γ―γͺγγ€γ‚³γƒΌγƒ‰γŒε£Šγ‚Œγ‚„γ™γγͺγ‚‹ε―θƒ½ζ€§γŒγ‚γ‚ŠγΎγ™γ€‚δΈ€θˆ¬ηš„γͺ代替手�?�をご覧ください。

cloneElement を使用すると、εˆ₯οΏ½?要素に基γ₯いて新しい React θ¦η΄ γ‚’δ½œζˆγ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚

const clonedElement = cloneElement(element, props, ...children)

γƒͺフゑレンス

cloneElement(element, props, ...children)

cloneElement を呼び出して、element γ‚’εŸΊγ«γ€η•°γͺγ‚‹ props と children γ‚’ζŒγ£γŸ React θ¦η΄ γ‚’δ½œζˆγ—γΎγ™γ€‚

import { cloneElement } from 'react';

// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);

console.log(clonedElement); // <Row title="Cabbage" isHighlighted={true}>Goodbye</Row>

さらに例を見る

εΌ•ζ•°

  • element: element εΌ•ζ•°γ―ζœ‰εŠΉγͺ React 要素でγͺγ‘γ‚Œγ°γͺγ‚ŠγΎγ›γ‚“γ€‚δΎ‹γˆγ°γ€<Something /> οΏ½?γ‚ˆγ†γͺ JSX γƒŽγƒΌγƒ‰γ€createElement οΏ½?ε‘Όγ³ε‡Ίγ—η΅ζžœγ€γΎγŸγ―εˆ₯οΏ½? cloneElement οΏ½?ε‘Όγ³ε‡Ίγ—η΅ζžœγͺどです。

  • props: props 引数はγ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ‹ null でγͺγ‘γ‚Œγ°γͺγ‚ŠγΎγ›γ‚“γ€‚null γ‚’ζΈ‘γ™γ¨γ€γ‚―γƒ­γƒΌγƒ³γ•γ‚ŒγŸθ¦η΄ γ―ε…ƒοΏ½? element.props γ‚’γ™γΉγ¦δΏζŒγ—γΎγ™γ€‚γγ‚Œδ»₯ε€–οΏ½?ε ΄εˆγ€props γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆε†…οΏ½?すべて�?ι …οΏ½?γ«γ€γ„γ¦γ€θΏ”γ•γ‚Œγ‚‹θ¦η΄ γ§γ― element.props οΏ½?ε€€γ‚ˆγ‚Šγ‚‚ props から�?ε€€γŒγ€Œε„ͺε…ˆγ€γ•γ‚ŒγΎγ™γ€‚οΏ½?οΏ½γ‚ŠοΏ½? props は元�? element.props γ‹γ‚‰εŸ‹γ‚γ‚‰γ‚ŒγΎγ™γ€‚props.key γ‚„ props.ref γ‚’ζΈ‘γ—γŸε ΄εˆγ€γγ‚Œγ‚‰γ―ε…ƒοΏ½?γ‚‚οΏ½?γ‚’οΏ½?γζ›γˆγΎγ™γ€‚

  • 省η•₯可能 ...children: ゼロ個δ»₯上�?ε­γƒŽγƒΌγƒ‰γ€‚γ‚γ‚‰γ‚†γ‚‹ React γƒŽγƒΌγƒ‰γ€γ€γΎγ‚Š React θ¦η΄ γ€ζ–‡ε­—εˆ—γ€ζ•°ε€€γ€γƒγƒΌγ‚Ώγƒ«γ€η©ΊγƒŽγƒΌγƒ‰οΌˆnull、undefined、true、false)、React γƒŽγƒΌγƒ‰οΏ½?ι…εˆ—γ«γͺγ‚ŠγΎγ™γ€‚...children 引数を渑さγͺγ„ε ΄εˆγ€ε…ƒοΏ½? element.props.children γŒδΏζŒγ•γ‚ŒγΎγ™γ€‚

θΏ”γ‚Šε€€

cloneElement はδ»₯δΈ‹οΏ½?γƒ—γƒ­γƒ‘γƒ†γ‚£γ‚’ζŒγ€ React 要素γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ‚’θΏ”γ—γΎγ™γ€‚

  • type: element.type γ¨εŒγ˜γ€‚
  • props: element.props γ«γ€ζΈ‘γ•γ‚ŒγŸδΈŠζ›Έγη”¨οΏ½? props γ‚’ζ΅…γγƒžγƒΌγ‚Έγ—γŸη΅ζžœγ€‚
  • ref: ε…ƒοΏ½? element.refγ€‚γŸγ γ—γ€props.ref γ«γ‚ˆγ£γ¦δΈŠζ›Έγγ•γ‚ŒγŸε ΄εˆγ―ι™€γγ€‚
  • key: ε…ƒοΏ½? element.keyγ€‚γŸγ γ—γ€props.key γ«γ‚ˆγ£γ¦δΈŠζ›Έγγ•γ‚ŒγŸε ΄εˆγ―ι™€γγ€‚

ι€šεΈΈγ€γ“οΏ½?θ¦η΄ γ‚’γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‹γ‚‰θΏ”γ™γ‹γ€δ»–οΏ½?要素�?子として用います。要素�?プロパティをθͺ­γΏε–γ‚‹γ“γ¨γ―ε―θƒ½γ§γ™γŒγ€δ½œζˆεΎŒγ―θ¦η΄ οΏ½?ζ§‹ι€ γ‚’ιžε…¬ι–‹ (opaque) として扱い、レンダー�?γΏθ‘Œγ†γ‚ˆγ†γ«γ™γ‚‹γΉγγ§γ™γ€‚

注意点

  • 要素をクローンしても元�?θ¦η΄ γ―ε€‰ζ›΄γ•γ‚ŒγΎγ›γ‚“γ€‚

  • 耇数�?子�?ε†…οΏ½?οΏ½γŒγ™γΉγ¦ι™ηš„γ«εˆ†γ‹γ£γ¦γ„γ‚‹ε ΄εˆγ€cloneElement には子を cloneElement(element, null, child1, child2, child3) οΏ½?γ‚ˆγ†γ«θ€‡ζ•°οΏ½?εΌ•ζ•°γ¨γ—γ¦ζΈ‘γ—γ¦γγ γ•γ„γ€‚ε­γŒε‹•ηš„γͺε ΄εˆγ―γ€ι…εˆ—ε…¨δ½“γ‚’η¬¬ 3 引数として cloneElement(element, null, listItems) οΏ½?γ‚ˆγ†γ«ζΈ‘γ—γ¦γγ γ•γ„γ€‚γ“γ‚Œγ«γ‚ˆγ‚Šγ€React γ―ε‹•ηš„γͺγƒͺγ‚Ήγƒˆγ« key γŒζ¬ γ‘γ¦γ„γ‚‹ε ΄εˆγ«θ­¦ε‘Šγ‚’ε‡Ίγ™γ‚ˆγ†γ«γͺγ‚ŠγΎγ™γ€‚ι™ηš„γͺγƒͺγ‚Ήγƒˆγ§γ―δΈ¦γ³ζ›Ώγˆγ―ζ±Ίγ—γ¦η™Ίη”Ÿγ—γͺγ„γŸγ‚γ€key γ―εΏ…θ¦γ‚γ‚ŠγΎγ›γ‚“γ€‚

  • cloneElement を使うとデータフロー�?θΏ½θ·‘γŒι›£γ—γγͺγ‚‹γŸγ‚γ€δ»£γ‚γ‚Šγ«δ»£ζ›Ώζ‰‹οΏ½?�を試してみてください。


使用法

要素�? props γ‚’δΈŠζ›Έγγ™γ‚‹

React 要素 οΏ½? props γ‚’δΈŠζ›Έγγ™γ‚‹γ«γ―γ€γγ‚Œγ‚’ cloneElement γ«ζΈ‘γ—γ€δΈŠζ›Έγγ—γŸγ„ props γ‚’ζŒ‡οΏ½?�します。

import { cloneElement } from 'react';

// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);

こ�?ε ΄εˆγ€η΅ζžœγ¨γͺγ‚‹γ‚―γƒ­γƒΌγƒ³γ•γ‚ŒγŸθ¦η΄ γ― <Row title="Cabbage" isHighlighted={true} /> にγͺγ‚ŠγΎγ™γ€‚

δΎ‹γ‚’δ½Ώγ£γ¦γ€γ“γ‚ŒγŒε½Ήη«‹γ€ε ΄ι’γ‚’θ¦‹γ¦γΏγΎγ—γ‚‡γ†γ€‚

ιΈζŠžε―θƒ½γͺ葌�?γƒͺγ‚Ήγƒˆγ¨γ€ιΈζŠžγ•γ‚Œγ¦γ„γ‚‹θ‘Œγ‚’ε€‰ζ›΄γ™γ‚‹ β€œNext” γƒœγ‚Ώγƒ³γ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹ List γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚’ζƒ³εƒγ—γ¦γΏγ¦γγ γ•γ„γ€‚List γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―γ€ιΈζŠžγ•γ‚ŒγŸ Row γ‚’η•°γͺγ‚‹ζ–Ήζ³•γ§γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹εΏ…θ¦γŒγ‚γ‚‹γŸγ‚γ€ε—γ‘ε–γ£γŸγ™γΉγ¦οΏ½? <Row> をクローンし、isHighlighted: true または isHighlighted: false γ‚’θΏ½εŠ οΏ½? props γ¨γ—γ¦ζŒ‡οΏ½?�します。

export default function List({ children }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{Children.map(children, (child, index) =>
cloneElement(child, {
isHighlighted: index === selectedIndex
})
)}

δΎ‹γˆγ° List γŒε—γ‘ε–γ‚‹ε…ƒοΏ½? JSX がδ»₯δΈ‹οΏ½?γ‚ˆγ†γͺγ‚‚οΏ½?γ§γ‚γ‚‹ε ΄εˆγ‚’θ€ƒγˆγΎγ™γ€‚

<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>

子要素をクローンすることで、List は内部�?すべて�? Row γ«θΏ½εŠ ζƒ…ε ±γ‚’ζΈ‘γ™γ“γ¨γŒγ§γγΎγ™γ€‚η΅ζžœγ―δ»₯δΈ‹οΏ½?γ‚ˆγ†γ«γͺγ‚ŠγΎγ™γ€‚

<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>

β€œNext” γ‚’ζŠΌγ™γ¨ List οΏ½? state γŒζ›΄ζ–°γ•γ‚Œγ€η•°γͺγ‚‹θ‘ŒγŒγƒγ‚€γƒ©γ‚€γƒˆγ•γ‚Œγ‚‹γ“γ¨γ«η€οΏ½?してください。

import { Children, cloneElement, useState } from 'react';

export default function List({ children }) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  return (
    <div className="List">
      {Children.map(children, (child, index) =>
        cloneElement(child, {
          isHighlighted: index === selectedIndex 
        })
      )}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % Children.count(children)
        );
      }}>
        Next
      </button>
    </div>
  );
}

γŠγ•γ‚‰γ„γ™γ‚‹γ¨γ€List γ―ε—γ‘ε–γ£γŸ <Row /> θ¦η΄ γ‚’γ‚―γƒ­γƒΌγƒ³γ—γ€γγ‚Œγ‚‰γ«θΏ½εŠ οΏ½? props γ‚’δ»˜εŠ γ—γŸγ¨γ„γ†γ“γ¨γ§γ™γ€‚

落とし穴

ε­θ¦η΄ γ‚’γ‚―γƒ­γƒΌγƒ³γ™γ‚‹γ¨γ€γƒ‡γƒΌγ‚ΏγŒγ‚’γƒ—γƒͺγ‚±γƒΌγ‚·γƒ§γƒ³γ‚’ι€šγ˜γ¦γ©οΏ½?γ‚ˆγ†γ«ζ΅γ‚Œγ‚‹γ‹γ‚’ζŠŠζ‘γ™γ‚‹οΏ½?γŒι›£γ—γγͺγ‚ŠγΎγ™γ€‚δ»£ζ›Ώζ‰‹οΏ½?οΏ½οΏ½?γ„γšγ‚Œγ‹γ‚’θ©¦γ—γ¦γΏγ¦γγ γ•γ„γ€‚


代替手�?οΏ½

レンダープロップを用いてデータを渑す

cloneElement γ‚’δ½Ώη”¨γ™γ‚‹δ»£γ‚γ‚Šγ«γ€renderItem οΏ½?γ‚ˆγ†γͺレンダープロップ (render prop) γ‚’ε—γ‘ε–γ‚‹γ‚ˆγ†γ«γ™γ‚‹γ“γ¨γ‚’ζ€œθ¨Žγ—γ¦γΏγ¦γγ γ•γ„γ€‚δ»₯δΈ‹οΏ½?例では、List は renderItem γ‚’ props γ¨γ—γ¦ε—γ‘ε–γ‚ŠγΎγ™γ€‚List は各をむテムに対して renderItem を呼び出し、isHighlighted を引数として渑します。

export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return renderItem(item, isHighlighted);
})}

renderItem οΏ½?γ‚ˆγ†γͺγ‚‚οΏ½?γ―γ€Œγƒ¬γƒ³γƒ€γƒΌγƒ—γƒ­γƒƒγƒ—γ€γ¨ε‘Όγ°γ‚ŒγΎγ™γ€‚δ½•γ‹γ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹ζ–Ήζ³•γ‚’ζŒ‡οΏ½?οΏ½γ™γ‚‹γŸγ‚οΏ½? props γ γ‹γ‚‰γ§γ™γ€‚δΎ‹γˆγ°γ€δΈŽγˆγ‚‰γ‚ŒγŸ isHighlighted οΏ½?倀で <Row> をレンダーする renderItem οΏ½?οΏ½?οΏ½θ£…γ‚’ζΈ‘γ™γ“γ¨γŒγ§γγΎγ™γ€‚

<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>

ζœ€η΅‚ηš„γͺ硐果は cloneElement γ¨εŒγ˜γ§γ™γ€‚

<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>

しかし、isHighlighted ε€€γŒγ©γ“γ‹γ‚‰ζ₯γ¦γ„γ‚‹γ‹γ‚’ζ˜Žη’Ίγ«θΏ½θ·‘γ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚

import { useState } from 'react';

export default function List({ items, renderItem }) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  return (
    <div className="List">
      {items.map((item, index) => {
        const isHighlighted = index === selectedIndex;
        return renderItem(item, isHighlighted);
      })}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % items.length
        );
      }}>
        Next
      </button>
    </div>
  );
}

こ�?γƒ‘γ‚ΏγƒΌγƒ³γ―γ‚ˆγ‚Šζ˜Žη€Ίηš„γ§γ‚γ‚‹γŸγ‚γ€cloneElement γ‚ˆγ‚Šγ‚‚ζŽ¨ε₯¨γ•γ‚ŒγΎγ™γ€‚


γ‚³γƒ³γƒ†γ‚―γ‚Ήγƒˆγ§γƒ‡γƒΌγ‚Ώγ‚’ζΈ‘γ™

cloneElement οΏ½?εˆ₯οΏ½?代替手�?οΏ½γ¨γ—γ¦γ‚³γƒ³γƒ†γ‚―γ‚Ήγƒˆγ‚’ι€šγ˜γ¦γƒ‡γƒΌγ‚Ώγ‚’ζΈ‘γ™γ“γ¨γŒε―θƒ½γ§γ™γ€‚

例として、createContext を呼び出して HighlightContext γ‚’οΏ½?�義しましょう。

export const HighlightContext = createContext(false);

List γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―γ€γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γ™γΉγ¦οΏ½?をむテムを HighlightContext プロバむダでラップします。

export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return (
<HighlightContext.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}

こ�?をプローチでは、Row は props で isHighlighted γ‚’ε—γ‘ε–γ‚‹εΏ…θ¦γŒδΈ€εˆ‡γ‚γ‚ŠγΎγ›γ‚“γ€‚δ»£γ‚γ‚Šγ«γ‚³γƒ³γƒ†γ‚―γ‚Ήγƒˆγ‹γ‚‰θͺ­γΏε–γ‚ŠγΎγ™γ€‚

export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...

γ“γ‚Œγ«γ‚ˆγ‚Šγ€ε‘Όγ³ε‡Ίγ—ε…ƒοΏ½?γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ― <Row> に isHighlighted を渑すことに぀いてηŸ₯る必要も、気にする必要もγͺくγͺγ‚ŠγΎγ™γ€‚

<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>

δ»£γ‚γ‚Šγ«γ€List と Row γ―γ‚³γƒ³γƒ†γ‚―γ‚Ήγƒˆγ‚’ι€šγ˜γ€γƒγ‚€γƒ©γ‚€γƒˆοΏ½?ロジックに閒して協θͺΏγ—γ¦ε‹•δ½œγ—γΎγ™γ€‚

import { useState } from 'react';
import { HighlightContext } from './HighlightContext.js';

export default function List({ items, renderItem }) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  return (
    <div className="List">
      {items.map((item, index) => {
        const isHighlighted = index === selectedIndex;
        return (
          <HighlightContext.Provider
            key={item.id}
            value={isHighlighted}
          >
            {renderItem(item)}
          </HighlightContext.Provider>
        );
      })}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % items.length
        );
      }}>
        Next
      </button>
    </div>
  );
}

γ‚³γƒ³γƒ†γ‚―γ‚Ήγƒˆγ‚’ι€šγ˜γ¦γƒ‡γƒΌγ‚Ώγ‚’ζ·±γζΈ‘γ™ζ–Ήζ³•γ«γ€γ„γ¦θ©³γ—γε­¦γΆ


γƒ­γ‚Έγƒƒγ‚―γ‚’γ‚«γ‚Ήγ‚Ώγƒ γƒ•γƒƒγ‚―γ«ζŠ½ε‡Ίγ™γ‚‹

試すべきεˆ₯οΏ½?γ‚’γƒ—γƒ­γƒΌγƒγ―γ€γ€Œιžθ¦–θ¦šηš„γ€γͺロジックをθ‡ͺ前�?γƒ•γƒƒγ‚―γ«ζŠ½ε‡Ίγ—γ€γƒ•γƒƒγ‚―γ‹γ‚‰θΏ”γ•γ‚Œγ‚‹ζƒ…ε ±γ‚’δ½Ώη”¨γ—γ¦δ½•γ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γ‹γ‚’ζ±ΊοΏ½?οΏ½γ™γ‚‹γ“γ¨γ§γ™γ€‚δΎ‹γˆγ°ζ¬‘οΏ½?γ‚ˆγ†γͺ useList γ‚«γ‚Ήγ‚Ώγƒ γƒ•γƒƒγ‚―γ‚’ζ›Έγγ“γ¨γŒγ§γγΎγ™γ€‚

import { useState } from 'react';

export default function useList(items) {
const [selectedIndex, setSelectedIndex] = useState(0);

function onNext() {
setSelectedIndex(i =>
(i + 1) % items.length
);
}

const selected = items[selectedIndex];
return [selected, onNext];
}

γ“γ‚Œγ‚’δ»₯δΈ‹οΏ½?γ‚ˆγ†γ«δ½Ώη”¨γ§γγΎγ™γ€‚

export default function App() {
const [selected, onNext] = useList(products);
return (
<div className="List">
{products.map(product =>
<Row
key={product.id}
title={product.title}
isHighlighted={selected === product}
/>
)}
<hr />
<button onClick={onNext}>
Next
</button>
</div>
);
}

γƒ‡γƒΌγ‚Ώγƒ•γƒ­γƒΌγ―ζ˜Žη€Ίηš„γ§γ™γŒγ€state は任意�?γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‹γ‚‰δ½Ώη”¨γ§γγ‚‹ useList γ‚«γ‚Ήγ‚Ώγƒ γƒ•γƒƒγ‚―ε†…γ«γ‚γ‚ŠγΎγ™γ€‚

import Row from './Row.js';
import useList from './useList.js';
import { products } from './data.js';

export default function App() {
  const [selected, onNext] = useList(products);
  return (
    <div className="List">
      {products.map(product =>
        <Row
          key={product.id}
          title={product.title}
          isHighlighted={selected === product}
        />
      )}
      <hr />
      <button onClick={onNext}>
        Next
      </button>
    </div>
  );
}

こ�?をプローチは、特にこ�?ロジックを異γͺγ‚‹γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆι–“γ§ε†εˆ©η”¨γ—γŸγ„ε ΄εˆγ«ζœ‰η”¨γ§γ™γ€‚