如何将 props 传递给 {this.props.children}?

1425

我正在努力寻找定义一些可以以通用方式使用的组件的正确方法:

<Parent>
  <Child value="1">
  <Child value="2">
</Parent>

在父子组件之间进行呈现的逻辑是存在的,你可以想象 <select><option> 是这种逻辑的一个例子。

以下是此问题的虚拟实现:

var Parent = React.createClass({
  doSomething: function(value) {
  },
  render: function() {
    return (<div>{this.props.children}</div>);
  }
});

var Child = React.createClass({
  onClick: function() {
    this.props.doSomething(this.props.value); // doSomething is undefined
  },
  render: function() {
    return (<div onClick={this.onClick}></div>);
  }
});

问题是,当你使用{this.props.children}来定义一个包装组件时,如何将某些属性传递给它的所有子组件?


4
通过这个问题的答案,我学到了很多。我认为在当今的React领域中,Context API是最好的解决方案。但是,如果你想使用React.cloneElement,我遇到的一个陷阱是没有正确地通过 React.Children.map() 迭代子元素。详情请参见 如何将属性传递给{react.children} - Victor Ofoegbu
32个回答

1481

使用新属性克隆子元素

您可以使用React.Children来迭代子元素,然后使用React.cloneElement克隆每个元素并附加新的属性(浅合并)。

请查看代码注释,了解我为什么不建议使用此方法。

const Child = ({ childName, sayHello }) => (
  <button onClick={() => sayHello(childName)}>{childName}</button>
);

function Parent({ children }) {
  // We pass this `sayHello` function into the child elements.
  function sayHello(childName) {
    console.log(`Hello from ${childName} the child`);
  }

  const childrenWithProps = React.Children.map(children, child => {
    // Checking isValidElement is the safe way and avoids a
    // typescript error too.
    if (React.isValidElement(child)) {
      return React.cloneElement(child, { sayHello });
    }
    return child;
  });

  return <div>{childrenWithProps}</div>
}

function App() {
  // This approach is less type-safe and Typescript friendly since it
  // looks like you're trying to render `Child` without `sayHello`.
  // It's also confusing to readers of this code.
  return (
    <Parent>
      <Child childName="Billy" />
      <Child childName="Bob" />
    </Parent>
  );
}

ReactDOM.render(<App />, document.getElementById("container"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<div id="container"></div>

调用子级作为函数

或者,您可以通过渲染属性将 props 传递给子级。在这种方法中,子级(可以是children或任何其他 prop 名称)是一个函数,它可以接受您想要传递的任何参数,并返回实际的子级:

const Child = ({ childName, sayHello }) => (
  <button onClick={() => sayHello(childName)}>{childName}</button>
);

function Parent({ children }) {
  function sayHello(childName) {
    console.log(`Hello from ${childName} the child`);
  }

  // `children` of this component must be a function
  // which returns the actual children. We can pass
  // it args to then pass into them as props (in this
  // case we pass `sayHello`).
  return <div>{children(sayHello)}</div>
}

function App() {
  // sayHello is the arg we passed in Parent, which
  // we now pass through to Child.
  return (
    <Parent>
      {(sayHello) => (
        <>
          <Child childName="Billy" sayHello={sayHello} />
          <Child childName="Bob" sayHello={sayHello} />
        </>
      )}
    </Parent>
  );
}

ReactDOM.render(<App />, document.getElementById("container"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<div id="container"></div>


7
这对我没用。这在React.cloneElement()中没有定义。 - Patrick
21
这个答案行不通,传递给“doSomething”的“value”丢失了。 - Dave
4
抱歉,我把console.log改成alert时忘记将两个参数连接成一个字符串了。 - Dave
1
这个答案非常有帮助,但我遇到了一个没有提到的问题,我想知道它是不是一些新的改变,还是我的问题。当我克隆我的子元素时,它的子元素被设置为旧元素,直到我将this.props.children.props.children添加到cloneElement的第三个参数中。 - aphenine
8
如果孩子组件是通过一个从不同路由页面加载的路由(v4)加载的,会发生什么? - blamb
显示剩余24条评论

554

如果您想要一种稍微简洁的方法,请尝试:

<div>
    {React.cloneElement(this.props.children, { loggedIn: this.state.loggedIn })}
</div>

编辑: 要在多个单独的子组件上使用(子组件本身必须是一个组件),可以这样做。在16.8.6版本中进行了测试。

<div>
    {React.cloneElement(this.props.children[0], { loggedIn: true, testPropB: true })}
    {React.cloneElement(this.props.children[1], { loggedIn: true, testPropA: false })}
</div>

12
有人能解释一下这是如何工作的(或者它实际上是做什么的)吗?阅读文档,我没有看到它会如何进入子元素并将该属性添加到每个子元素中 - 这是它的预期行为吗?如果是,我们怎么知道它会这样做?甚至将不透明的数据结构(this.props.children)传递给期望接收元素的cloneElement,这显然并不明显。 - GreenAsJade
56
没错,这似乎不能适用于多个孩子。 - Danita
19
你可以编写代码,在仅传递一个子组件时就可以工作,但当添加另一个子组件时,它会崩溃...这听起来并不好? 这似乎是对提问者的陷阱,他特别询问了如何将props传递给“所有”子组件。 - GreenAsJade
13
只要你的组件只接收一个子元素,那么这没问题。你可以通过定义组件的 propTypes 来表明它只能接收一个子元素。React.Children.only 函数会返回唯一的子元素,如果有多个子元素则会抛出异常(如果不存在这种情况,该函数也就没有存在的必要了)。 - cchamberlain
4
被踩了。它没有回答问题“如何将某些属性传递给所有子元素?”,它甚至没有提到如果你试图将其用于多个子元素,它会崩溃和失败的事实。173人如何认为这是一个好答案呢? - Søren Boisen
显示剩余12条评论

129

5
直接返回React.cloneElement()而不用包裹在<div>标签中,是否可行?因为如果子元素是<span>(或其他元素),我们可能想保留其标签类型。 - adrianmcli
1
如果只有一个子元素,您可以省略包装器。这个解决方案仅适用于一个子元素,所以是的。 - ThaJay
2
对我来说没问题。不用包含<div>也可以。 - Crash Override
8
如果您需要明确强制仅接收一个子元素,您可以使用 React.cloneElement(React.Children.only(this.props.children), {...this.props})。如果传递了多个子元素,它将抛出错误。这样您就不需要用 div 进行包装了。 - itsananderson
2
这个答案可能会产生一个 TypeError: 循环对象值。除非你想让子元素的属性是它本身,否则请使用 let {children, ...acyclicalProps} = this.props 然后 React.cloneElement(React.Children.only(children), acyclicalProps) - Parabolord
显示剩余3条评论

107

将props传递给直接子组件。

查看其他全部答案

通过context在组件树中传递共享的全局数据

Context旨在共享在React组件树中可以视为“全局”的数据,例如当前已验证的用户、主题或首选语言。1

免责声明:这是更新后的答案,前一个答案使用了旧的context API

它基于Consumer / Provide原理。首先,创建你的context。

const { Provider, Consumer } = React.createContext(defaultValue);

然后通过使用

<Provider value={/* some value */}>
  {children} /* potential consumers */
</Provider>

以及

<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

只要 Provider 的 value 属性发生变化,所有作为其子节点的 Consumer 都将重新渲染。从 Provider 到其后代 Consumer 的传播不受 shouldComponentUpdate 方法的限制,因此即使祖先组件放弃更新,Consumer 也会更新。1

完整示例,半伪代码。

import React from 'react';

const { Provider, Consumer } = React.createContext({ color: 'white' });

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: { color: 'black' },
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

class Toolbar extends React.Component {
  render() {
    return ( 
      <div>
        <p> Consumer can be arbitrary levels deep </p>
        <Consumer> 
          {value => <p> The toolbar will be in color {value.color} </p>}
        </Consumer>
      </div>
    );
  }
}

1 https://facebook.github.io/react/docs/context.html


9
与被接受的答案不同,即使Parent下包含其他元素,这个答案也能正常工作。这绝对是最好的答案。 - Zaptree
13
道具(Props)并不等同于上下文(context)。 - Petr Peller
1
也许我理解有误,但是说“上下文使道具可用”不对吗?当我上次使用上下文时,它是一个单独的东西(即this.context) - 它没有神奇地将上下文与道具合并。您必须有意设置和使用上下文,这是完全不同的事情。 - Josh
你理解得很好,之前是错误的。我已经编辑了我的回答。 - Lyubomir
我必须编辑这个答案,因为他们有了新的上下文API,但我目前正在写我的论文。很快 :-) - Lyubomir
显示剩余8条评论

75

向嵌套子组件传递属性

使用 React Hooks 的更新,您现在可以使用 React.createContextuseContext

import * as React from 'react';

// React.createContext accepts a defaultValue as the first param
const MyContext = React.createContext(); 

functional Parent(props) {
  const doSomething = React.useCallback((value) => {
    // Do something here with value
  }, []);

  return (
     <MyContext.Provider value={{ doSomething }}>
       {props.children}
     </MyContext.Provider>
  );
}
 
function Child(props: { value: number }) {
  const myContext = React.useContext(MyContext);

  const onClick = React.useCallback(() => {
    myContext.doSomething(props.value);
  }, [props.value, myContext.doSomething]);

  return (
    <div onClick={onClick}>{props.value}</div>
  );
}

// Example of using Parent and Child

import * as React from 'react';

function SomeComponent() {
  return (
    <Parent>
      <Child value={1} />
      <Child value={2} />
    </Parent>
  );
}

React.createContext 函数在处理嵌套组件时比 React.cloneElement 方法更加有效。

function SomeComponent() {
  return (
    <Parent>
      <Child value={1} />
      <SomeOtherComp>
        <Child value={2} />
      </SomeOtherComp>
    </Parent>
  );
}

3
你能解释为什么使用箭头函数是一种不好的做法吗?箭头函数帮助绑定事件处理程序以获取 this 上下文。 - Kenneth Truong
1
@KennethTruong 因为每次渲染时都会创建一个函数。 - itdoesntwork
9
@itdoesntwork那并不是真的。它只在类被创建时创建一个新函数,而不是在渲染函数期间创建。 - Kenneth Truong
@KennethTruong 我认为你在谈论渲染中的箭头函数。 - itdoesntwork
我会在你的代码中删除 value={2}。原因:1)它不必要来解释上下文。2)它会分散注意力,而实际上关键点是使用 myContext。 :) - DarkTrick

50

最好的方法是使用函数模式作为children,这样可以实现属性传递。

https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9

代码片段:https://stackblitz.com/edit/react-fcmubc

示例:

const Parent = ({ children }) => {
    const somePropsHere = {
      style: {
        color: "red"
      }
      // any other props here...
    }
    return children(somePropsHere)
}

const ChildComponent = props => <h1 {...props}>Hello world!</h1>

const App = () => {
  return (
    <Parent>
      {props => (
        <ChildComponent {...props}>
          Bla-bla-bla
        </ChildComponent>
      )}
    </Parent>
  )
}


3
这对我来说似乎更加直接(而且性能更好?)比被接受的答案。 - Shikyo
5
需要将孩子组件作为一个函数来实现,对于深层嵌套的组件则无法使用。 - digital illusion
2
你是对的,深度嵌套子组件的情况也可以处理,使用<Parent>{props => <Nest><ChildComponent /></Nest>}</Parent>代替(无法工作的)<Parent><Nest>{props => <ChildComponent />}</Nest></Parent>,所以我同意这是最好的答案。 - digital illusion
1
尝试时,我收到以下错误信息:TypeError: children is not a function - Ryan Prentiss
@RyanPrentiss 我可以看一下你的代码。你能分享一些片段吗? - Nick Ovchinnikov
显示剩余3条评论

36
你可以使用React.cloneElement,在你开始在你的应用中使用它之前最好了解它的工作原理。它是在React v0.13中引入的,继续阅读以获取更多信息,所以这个工作对你来说可能会有所帮助:
<div>{React.cloneElement(this.props.children, {...this.props})}</div>

所以,我为你带来了React文档中的一些代码,让你了解它们是如何工作的,以及如何使用它们:
在React v0.13 RC2中,我们将引入一个新的API,类似于React.addons.cloneWithProps,具有以下签名:
React.cloneElement(element, props, ...children);

与cloneWithProps不同,这个新函数没有任何魔法内置行为来合并样式和类名,原因与transferPropsTo没有该功能相同。没有人确定魔法事物的完整列表,这使得对代码的推理和在样式具有不同签名时的重用变得困难(例如在即将到来的React Native中)。React.cloneElement几乎等同于:
<element.type {...element.props} {...props}>{children}</element.type>

然而,与JSX和cloneWithProps不同的是,它还保留了引用。这意味着,如果你获取一个带有引用的子元素,你不会意外地从祖先那里偷走它。你将获得相同的引用附加到你的新元素上。
一个常见的模式是遍历你的子元素并添加一个新的属性。关于cloneWithProps丢失引用的问题报告了很多,这使得你的代码更难理解。现在,使用cloneElement遵循相同的模式将按预期工作。例如:
var newChildren = React.Children.map(this.props.children, function(child) {
  return React.cloneElement(child, { foo: true })
});

注意:React.cloneElement(child, { ref: 'newRef' })会覆盖ref,因此两个父组件无法引用同一个子组件,除非使用回调引用。
这是一个重要的功能,需要在React 0.13中实现,因为props现在是不可变的。升级路径通常是克隆元素,但这样做可能会丢失ref。因此,我们需要一个更好的升级路径。在Facebook的升级过程中,我们意识到我们需要这个方法。我们从社区得到了同样的反馈。因此,我们决定在最终发布之前再发布一个RC,以确保我们能够实现这个功能。
我们计划最终废弃React.addons.cloneWithProps。目前我们还没有这样做,但这是一个很好的机会来开始考虑你自己的使用情况,并考虑使用React.cloneElement代替。在我们真正删除它之前,我们将确保在发布中包含废弃通知,因此不需要立即采取行动。
更多这里...

14

方法一 - 克隆子元素

const Parent = (props) => {
   const attributeToAddOrReplace= "Some Value"
   const childrenWithAdjustedProps = React.Children.map(props.children, child =>
      React.cloneElement(child, { attributeToAddOrReplace})
   );

   return <div>{childrenWithAdjustedProps }</div>
}

完整演示

方法2 - 使用可组合的上下文

上下文允许您将一个属性传递给深层子组件,而无需通过中间组件显式地将其作为属性传递。

但是,上下文也有一些缺点:

  1. 数据不会按照常规方式通过 props 传递。
  2. 使用上下文会在消费者和提供者之间创建一个合同。更难理解和复制重用组件所需的要求。

使用可组合的上下文

export const Context = createContext<any>(null);

export const ComposableContext = ({ children, ...otherProps }:{children:ReactNode, [x:string]:any}) => {
    const context = useContext(Context)
    return(
      <Context.Provider {...context} value={{...context, ...otherProps}}>{children}</Context.Provider>
    );
}

function App() {
  return (
      <Provider1>
            <Provider2> 
                <Displayer />
            </Provider2>
      </Provider1>
  );
}

const Provider1 =({children}:{children:ReactNode}) => (
    <ComposableContext greeting="Hello">{children}</ComposableContext>
)

const Provider2 =({children}:{children:ReactNode}) => (
    <ComposableContext name="world">{children}</ComposableContext>
)

const Displayer = () => {
  const context = useContext(Context);
  return <div>{context.greeting}, {context.name}</div>;
};


有点晚了,但你能解释一下{children}:{children:ReactNode}中的符号吗? - camille
@camille,这是一个Typescript的问题。现在看来,我会用Javascript来回答,即使我会写Typescript,我也会以不同的方式来做。将来可能会进行编辑。 - Ben Carp
1
@camille,基本上它意味着具有键“children”的值是类型为ReactNode的。 - Ben Carp
@Ben Carp,使用方法1,我如何访问attributeToAddOrReplace并将其添加或替换到子文件中? - Samiksha Jagtap
嗨@SamikshaJagtap,我添加了一个演示来更清楚地展示如何完成。希望这可以帮助你!https://stackblitz.com/edit/react-te35fc?file=src/App.js - Ben Carp

13

我需要修正上面被接受的答案,使用that指针代替this指针使其能够工作。在map函数范围内,this没有定义doSomething函数。

var Parent = React.createClass({
doSomething: function() {
    console.log('doSomething!');
},

render: function() {
    var that = this;
    var childrenWithProps = React.Children.map(this.props.children, function(child) {
        return React.cloneElement(child, { doSomething: that.doSomething });
    });

    return <div>{childrenWithProps}</div>
}})

更新:此修复适用于ECMAScript 5,在ES6中不需要使用var that=this


13
或者只需使用 bind() - plus-
1
或者使用箭头函数绑定词法作用域,我更新了我的答案。 - Dominic
如果 doSomething 接受一个对象作为参数,如 doSomething: function(obj) { console.log(obj) },在子组件中你可以调用 this.props.doSomething(obj) 来打印出 "obj" - conor909
4
我知道这篇文章有点老,但在这里使用bind方法是一个糟糕的想法。bind方法会创建一个将上下文绑定到新函数的新函数,并调用apply方法。在render函数中使用bind()会在每次调用render方法时创建一个新函数。 - Bamieh

11

没有一个答案解决了拥有 React 组件子元素的问题,比如文本字符串。一个解决办法可能是这样的:

// Render method of Parent component
render(){
    let props = {
        setAlert : () => {alert("It works")}
    };
    let childrenWithProps = React.Children.map( this.props.children, function(child) {
        if (React.isValidElement(child)){
            return React.cloneElement(child, props);
        }
          return child;
      });
    return <div>{childrenWithProps}</div>

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接