cloneElement
cloneElement
允许你使用另一个元素作为起点创建一个新的 React 元素。
¥cloneElement
lets you create a new React element using another element as a starting point.
const clonedElement = cloneElement(element, props, ...children)
参考
¥Reference
cloneElement(element, props, ...children)
调用 cloneElement
创建一个基于 element
的 React 元素,但 props
和 children
不同:
¥Call cloneElement
to create a React element based on the element
, but with different props
and children
:
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>
参数
¥Parameters
-
element
:element
参数必须是有效的 React 元素。例如,它可以是一个像<Something />
这样的 JSX 节点,也可以是调用createElement
的结果,或者另一个cloneElement
调用的结果。¥
element
: Theelement
argument must be a valid React element. For example, it could be a JSX node like<Something />
, the result of callingcreateElement
, or the result of anothercloneElement
call. -
props
:props
参数必须是对象或null
。如果你传递null
,克隆的元素将保留原始element.props
的所有内容。否则,对于props
对象中的每个 prop,返回的元素将 “prefer”props
的值,而不是element.props
的值。其余属性将由原始element.props
填充。如果你传递props.key
或props.ref
,它们将替换原始元素。¥
props
: Theprops
argument must either be an object ornull
. If you passnull
, the cloned element will retain all of the originalelement.props
. Otherwise, for every prop in theprops
object, the returned element will “prefer” the value fromprops
over the value fromelement.props
. The rest of the props will be filled from the originalelement.props
. If you passprops.key
orprops.ref
, they will replace the original ones. -
可选
...children
:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、portals、空节点(null
、undefined
、true
和false
)以及 React 节点数组。如果你不传递任何...children
参数,则原始element.props.children
将被保留。¥optional
...children
: Zero or more child nodes. They can be any React nodes, including React elements, strings, numbers, portals, empty nodes (null
,undefined
,true
, andfalse
), and arrays of React nodes. If you don’t pass any...children
arguments, the originalelement.props.children
will be preserved.
返回
¥Returns
cloneElement
返回一个具有少量属性的 React 元素对象:
¥cloneElement
returns a React element object with a few properties:
-
type
:与element.type
相同。¥
type
: Same aselement.type
. -
props
:将element.props
与你传递的覆盖props
浅合并的结果。¥
props
: The result of shallowly mergingelement.props
with the overridingprops
you have passed. -
ref
:原始element.ref
,除非它被props.ref
覆盖。¥
ref
: The originalelement.ref
, unless it was overridden byprops.ref
. -
key
:原始element.key
,除非它被props.key
覆盖。¥
key
: The originalelement.key
, unless it was overridden byprops.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
-
克隆元素不会修改原始元素。
¥Cloning an element does not modify the original element.
-
只有当子组件都是静态已知的(例如
cloneElement(element, null, child1, child2, child3)
)时,才应将它们作为多个参数传递给cloneElement
。如果你的子组件是动态的,请将整个数组作为第三个参数传递:cloneElement(element, null, listItems)
。这确保 React 将对任何动态列表进行 警告你缺少key
。对于静态列表,这不是必需的,因为它们永远不会重新排序。¥You should only pass children as multiple arguments to
cloneElement
if they are all statically known, likecloneElement(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 missingkey
s for any dynamic lists. For static lists this is not necessary because they never reorder. -
cloneElement
使得追踪数据流变得更加困难,因此请尝试使用 alternatives。¥
cloneElement
makes it harder to trace the data flow, so try the alternatives instead.
用法
¥Usage
覆盖元素
¥Overriding props of an element
要覆盖某些 React 元素 的属性,请将其与要覆盖的 props 一起传递给 cloneElement
:
¥To override the props of some React element, pass it to cloneElement
with the props you want to override:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);
此处,生成的 克隆元素 将是 <Row title="Cabbage" isHighlighted={true} />
。
¥Here, the resulting cloned element will be <Row title="Cabbage" isHighlighted={true} />
.
让我们通过一个例子来了解它何时有用。
¥Let’s walk through an example to see when it’s useful.
想象一下,一个 List
组件将其 children
渲染为可选行列表,并带有一个 “下一个” 按钮,该按钮可以更改所选行。List
组件需要以不同的方式渲染选定的 Row
,因此它会克隆接收到的每个 <Row>
子组件,并添加额外的 isHighlighted: true
或 isHighlighted: false
属性:
¥Imagine a List
component that renders its 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 <Row>
child that it has received, and adds an extra isHighlighted: true
or isHighlighted: false
prop:
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 如下所示:
¥Let’s say the original JSX received by List
looks like this:
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>
通过克隆其子项,List
可以将额外的信息传递给其内部的每个 Row
。结果如下所示:
¥By cloning its children, the List
can pass extra information to every Row
inside. The result looks like this:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>
请注意,按下 “下一个” 会如何更新 List
的状态,并高亮不同的行:
¥Notice how pressing “Next” updates the state of the List
, and highlights a different row:
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 />
元素,并为其添加了一个额外的 prop。
¥To summarize, the List
cloned the <Row />
elements it received and added an extra prop to them.
备选方案
¥Alternatives
使用渲染属性传递数据
¥Passing data with a render prop
不要使用 cloneElement
,可以考虑接受像 renderItem
这样的渲染 prop。这里,List
接收 renderItem
作为 prop。List
为每个项目调用 renderItem
,并将 isHighlighted
作为参数传递:
¥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:
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
属性之所以被称为 “render 属性”,是因为它是一个指定如何渲染内容的属性。例如,你可以传递一个 renderItem
实现,该实现使用给定的 isHighlighted
值渲染 <Row>
:
¥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 <Row>
with the given isHighlighted
value:
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>
最终结果与 cloneElement
相同:
¥The end result is the same as with cloneElement
:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>
然而,你可以清楚地追踪 isHighlighted
值的来源。
¥However, you can clearly trace where the isHighlighted
value is coming from.
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
,因为它更明确。
¥This pattern is preferred to cloneElement
because it is more explicit.
通过上下文传递数据
¥Passing data through context
cloneElement
的另一种替代方案是 通过上下文传递数据。
¥Another alternative to cloneElement
is to pass data through context.
例如,你可以调用 createContext
来定义 HighlightContext
:
¥For example, you can call createContext
to define a HighlightContext
:
export const HighlightContext = createContext(false);
你的 List
组件可以将其渲染的每个项目封装到 HighlightContext
提供程序中:
¥Your List
component can wrap every item it renders into a HighlightContext
provider:
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
根本不需要接收 isHighlighted
prop。它会读取 context:
¥With this approach, Row
does not need to receive an isHighlighted
prop at all. Instead, it reads the context:
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...
这使得调用组件无需了解或担心将 isHighlighted
传递给 <Row>
:
¥This allows the calling component to not know or worry about passing isHighlighted
to <Row>
:
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>
List
和 Row
通过 context 协调高亮逻辑。
¥Instead, List
and Row
coordinate the highlighting logic through context.
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> ); }
¥Learn more about passing data through context.
将逻辑提取到自定义钩子中
¥Extracting logic into a custom Hook
你可以尝试的另一种方法是将 “non-visual” 逻辑提取到你自己的钩子中,并使用钩子返回的信息来决定要渲染的内容。例如,你可以像这样编写一个 useList
自定义 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:
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:
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>
);
}
数据流是显式的,但状态位于 useList
自定义钩子中,你可以从任何组件中使用它:
¥The data flow is explicit, but the state is inside the useList
custom Hook that you can use from any component:
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> ); }
如果你想在不同组件之间复用此逻辑,这种方法尤其有用。
¥This approach is particularly useful if you want to reuse this logic between different components.