diff --git a/beta/src/content/apis/react/cloneElement.md b/beta/src/content/apis/react/cloneElement.md index fa9d341f..114553d8 100644 --- a/beta/src/content/apis/react/cloneElement.md +++ b/beta/src/content/apis/react/cloneElement.md @@ -2,19 +2,693 @@ title: cloneElement --- - + -This section is incomplete, please see the old docs for [cloneElement.](https://reactjs.org/docs/react-api.html#cloneelement) - - +Using `cloneElement` is uncommon and can lead to fragile code. [See common alternatives.](#alternatives) + +`cloneElement` lets you create a new React element using another element as a starting point. + ```js -React.cloneElement(element, [config], [...children]) +const clonedElement = cloneElement(element, props, ...children) ``` + +--- + +## Usage {/*usage*/} + +### Overriding props of an element {/*overriding-props-of-an-element*/} + +To override the props of some React element, pass it to `cloneElement` with the props you want to override: + +```js [[1, 5, ""], [2, 6, "{ isHighlighted: true }"], [3, 4, "clonedElement"]] +import { clonedElement } from 'react'; + +// ... +const clonedElement = cloneElement( + , + { isHighlighted: true } +); +``` + +Here, the resulting cloned element will be ``. + +**Let's walk through an example to see when it's useful.** + +Imagine a `List` component that renders its [`children`](/learn/passing-props-to-a-component#passing-jsx-as-children) as a list of selectable rows with a "Next" button that changes which row is selected. The `List` component needs to render the selected `Row` differently, so it clones every `` child that it has received, and adds an extra `isHighlighted: true` or `isHighlighted: false` prop: + +```js {6-8} +export default function List({ children }) { + const [selectedIndex, setSelectedIndex] = useState(0); + return ( +
+ {Children.map(children, (child, index) => + cloneElement(child, { + isHighlighted: index === selectedIndex + }) + )} +``` + +Let's say the original JSX received by `List` looks like this: + +```js {2-4} + + + + + +``` + +By cloning its children, the `List` can pass extra information to every `Row` inside. The result looks like this: + +```js {4,8,12} + + + + + +``` + +Notice how pressing "Next" updates the state of the `List`, and highlights a different row: + + + +```js +import List from './List.js'; +import Row from './Row.js'; +import { products } from './data.js'; + +export default function App() { + return ( + + {products.map(product => + + )} + + ); +} +``` + +```js List.js active +import { Children, cloneElement, useState } from 'react'; + +export default function List({ children }) { + const [selectedIndex, setSelectedIndex] = useState(0); + return ( +
+ {Children.map(children, (child, index) => + cloneElement(child, { + isHighlighted: index === selectedIndex + }) + )} +
+ +
+ ); +} +``` + +```js Row.js +export default function Row({ title, isHighlighted }) { + return ( +
+ {title} +
+ ); +} +``` + +```js data.js +export const products = [ + { title: 'Cabbage', id: 1 }, + { title: 'Garlic', id: 2 }, + { title: 'Apple', id: 3 }, +]; +``` + +```css +.List { + display: flex; + flex-direction: column; + border: 2px solid grey; + padding: 5px; +} + +.Row { + border: 2px dashed black; + padding: 5px; + margin: 5px; +} + +.RowHighlighted { + background: #ffa; +} + +button { + height: 40px; + font-size: 20px; +} +``` + +
+ +To summarize, the `List` cloned the `` elements it received and added an extra prop to them. + + + +Cloning children makes it hard to tell how the data flows through your app. Try one of the [alternatives.](#alternatives) + + + +--- + +## Alternatives {/*alternatives*/} + +### Passing data with a render prop {/*passing-data-with-a-render-prop*/} + +Instead of using `cloneElement`, consider accepting a *render prop* like `renderItem`. Here, `List` receives `renderItem` as a prop. `List` calls `renderItem` for every item and passes `isHighlighted` as an argument: + +```js {1,7} +export default function List({ items, renderItem }) { + const [selectedIndex, setSelectedIndex] = useState(0); + return ( +
+ {items.map((item, index) => { + const isHighlighted = index === selectedIndex; + return renderItem(item, isHighlighted); + })} +``` + +The `renderItem` prop is called a "render prop" because it's a prop that specifies how to render something. For example, you can pass a `renderItem` implementation that renders a `` with the given `isHighlighted` value: + +```js {3,7} + + + } +/> +``` + +The end result is the same as with `cloneElement`: + +```js {4,8,12} + + + + + +``` + +However, you can clearly trace where the `isHighlighted` value is coming from. + + + +```js +import List from './List.js'; +import Row from './Row.js'; +import { products } from './data.js'; + +export default function App() { + return ( + + + } + /> + ); +} +``` + +```js List.js active +import { useState } from 'react'; + +export default function List({ items, renderItem }) { + const [selectedIndex, setSelectedIndex] = useState(0); + return ( +
+ {items.map((item, index) => { + const isHighlighted = index === selectedIndex; + return renderItem(item, isHighlighted); + })} +
+ +
+ ); +} +``` + +```js Row.js +export default function Row({ title, isHighlighted }) { + return ( +
+ {title} +
+ ); +} +``` + +```js data.js +export const products = [ + { title: 'Cabbage', id: 1 }, + { title: 'Garlic', id: 2 }, + { title: 'Apple', id: 3 }, +]; +``` + +```css +.List { + display: flex; + flex-direction: column; + border: 2px solid grey; + padding: 5px; +} + +.Row { + border: 2px dashed black; + padding: 5px; + margin: 5px; +} + +.RowHighlighted { + background: #ffa; +} + +button { + height: 40px; + font-size: 20px; +} +``` + +
+ +This pattern is preferred to `cloneElement` because it is more explicit. + +--- + +### Passing data through context {/*passing-data-through-context*/} + +Another alternative to `cloneElement` is to [pass data through context.](/learn/passing-data-deeply-with-context) + + +For example, you can call [`createContext`](/apis/react/createContext) to define a `HighlightContext`: + +```js +export const HighlightContext = createContext(false); +``` + +Your `List` component can wrap every item it renders into a `HighlightContext` provider: + +```js {8,10} +export default function List({ items, renderItem }) { + const [selectedIndex, setSelectedIndex] = useState(0); + return ( +
+ {items.map((item, index) => { + const isHighlighted = index === selectedIndex; + return ( + + {renderItem(item)} + + ); + })} +``` + +With this approach, `Row` does not need to receive an `isHighlighted` prop at all. Instead, it reads the context: + +```js Row.js {2} +export default function Row({ title }) { + const isHighlighted = useContext(HighlightContext); + // ... +```` + +This allows the calling component to not know or worry about passing `isHighlighted` to ``: + +```js {4} + + + } +/> +``` + +Instead, `List` and `Row` coordinate the highlighting logic through context. + + + +```js +import List from './List.js'; +import Row from './Row.js'; +import { products } from './data.js'; + +export default function App() { + return ( + + + } + /> + ); +} +``` + +```js List.js active +import { useState } from 'react'; +import { HighlightContext } from './HighlightContext.js'; + +export default function List({ items, renderItem }) { + const [selectedIndex, setSelectedIndex] = useState(0); + return ( +
+ {items.map((item, index) => { + const isHighlighted = index === selectedIndex; + return ( + + {renderItem(item)} + + ); + })} +
+ +
+ ); +} +``` + +```js Row.js +import { useContext } from 'react'; +import { HighlightContext } from './HighlightContext.js'; + +export default function Row({ title }) { + const isHighlighted = useContext(HighlightContext); + return ( +
+ {title} +
+ ); +} +``` + +```js HighlightContext.js +import { createContext } from 'react'; + +export const HighlightContext = createContext(false); +``` + +```js data.js +export const products = [ + { title: 'Cabbage', id: 1 }, + { title: 'Garlic', id: 2 }, + { title: 'Apple', id: 3 }, +]; +``` + +```css +.List { + display: flex; + flex-direction: column; + border: 2px solid grey; + padding: 5px; +} + +.Row { + border: 2px dashed black; + padding: 5px; + margin: 5px; +} + +.RowHighlighted { + background: #ffa; +} + +button { + height: 40px; + font-size: 20px; +} +``` + +
+ +[Learn more about passing data through context.](/apis/react/useContext#passing-data-deeply-into-the-tree) + +--- + +### Extracting logic into a custom Hook {/*extracting-logic-into-a-custom-hook*/} + +Another approach you can try is to extract the "non-visual" logic into your own Hook, and use the information returned by your Hook to decide what to render. For example, you could write a `useList` custom Hook like this: + +```js +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]; +} +``` + +Then you could use it like this: + +```js {2,9,13} +export default function App() { + const [selected, onNext] = useList(products); + return ( +
+ {products.map(product => + + )} +
+ +
+ ); +} +``` + +The data flow is explicit, but the state is inside the `useList` custom Hook that you can use from any component: + + + +```js +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 ( +
+ {products.map(product => + + )} +
+ +
+ ); +} +``` + +```js useList.js +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]; +} +``` + +```js Row.js +export default function Row({ title, isHighlighted }) { + return ( +
+ {title} +
+ ); +} +``` + +```js data.js +export const products = [ + { title: 'Cabbage', id: 1 }, + { title: 'Garlic', id: 2 }, + { title: 'Apple', id: 3 }, +]; +``` + +```css +.List { + display: flex; + flex-direction: column; + border: 2px solid grey; + padding: 5px; +} + +.Row { + border: 2px dashed black; + padding: 5px; + margin: 5px; +} + +.RowHighlighted { + background: #ffa; +} + +button { + height: 40px; + font-size: 20px; +} +``` + +
+ +This approach is particularly useful if you want to reuse this logic between different components. + +--- + +## Reference {/*reference*/} + +### `cloneElement(element, props, ...children)` {/*cloneelement*/} + +Call `cloneElement` to create a React element based on the `element`, but with different `props` and `children`: + +```js +import { clonedElement } from 'react'; + +// ... +const clonedElement = cloneElement( + + Hello + , + { isHighlighted: true }, + 'Goodbye' +); + +console.log(clonedElement); // Goodbye +``` + +[See more examples above.](#usage) + +#### Parameters {/*parameters*/} + +* `element`: The `element` argument must be a valid React element. For example, it could be a JSX node like ``, the result of calling [`createElement`](/apis/react/createElement), or the result of another `cloneElement` call. + +* `props`: The `props` argument must either be an object or `null`. If you pass `null`, the cloned element will retain all of the original `element.props`. Otherwise, for every prop in the `props` object, the returned element will "prefer" the value from `props` over the value from `element.props`. The rest of the props will be filled from the original `element.props`. If you pass `props.key` or `props.ref`, they will replace the original ones. + +* **optional** `...children`: Zero or more child nodes. They can be any React nodes, including React elements, strings, numbers, [portals](/apis/react-dom/createPortal), empty nodes (`null`, `undefined`, `true`, and `false`), and arrays of React nodes. If you don't pass any `...children` arguments, the original `element.props.children` will be preserved. + +#### Returns {/*returns*/} + +`cloneElement` returns a React element object with a few properties: + +* `type`: Same as `element.type`. +* `props`: The result of shallowly merging `element.props` with the overriding `props` you have passed. +* `ref`: The original `element.ref`, unless it was overridden by `props.ref`. +* `key`: The original `element.key`, unless it was overridden by `props.key`. + +Usually, you'll return the element from your component or make it a child of another element. Although you may read the element's properties, it's best to treat every element as opaque after it's created, and only render it. + +#### Caveats {/*caveats*/} + +* Cloning an element **does not modify the original element.** + +* You should only **pass children as multiple arguments to `createElement` if they are all statically known,** like `cloneElement(element, null, child1, child2, child3)`. If your children are dynamic, pass the entire array as the third argument: `cloneElement(element, null, listItems)`. This ensures that React will [warn you about missing `key`s](/learn/rendering-lists#keeping-list-items-in-order-with-key) for any dynamic lists. For static lists this is not necessary because they never reorder. + +* `cloneElement` makes it harder to trace the data flow, so **try the [alternatives](/#alternatives) instead.** diff --git a/beta/src/sidebarReference.json b/beta/src/sidebarReference.json index c7396afd..a2c94401 100644 --- a/beta/src/sidebarReference.json +++ b/beta/src/sidebarReference.json @@ -17,8 +17,7 @@ }, { "title": "cloneElement", - "path": "/apis/react/cloneElement", - "wip": true + "path": "/apis/react/cloneElement" }, { "title": "Component",