简体   繁体   English

在 Reactjs 中,如何从父组件操作子组件的子组件?

[英]In Reactjs, how do you manipulate the children of a child component from a parent component?

I want to make a highly reusable react component with a unique pattern.我想制作一个具有独特模式的高度可重用的反应组件。 Assume this contact list was produced by another team;假设这个联系人列表是由另一个团队制作的; we can't change the components, and it follows the structure shown below.我们不能更改组件,它遵循如下所示的结构。

<Component>
    <Child1 key="child1">
    <Child2 key="child2">
    <Child3 key="child3">
</Component>

Sample ContactList Component:示例联系人列表组件:

<ContactList key="contact-list">
    <ContactList.Header key="contactlist-header">
    <ContactList.Body key="contactlist-body">
    <ContactList.Footer key="contactlist-footer">
</ContactList>

I'd like to offer choices for customising the contact-list component, such as我想提供自定义联系人列表组件的选项,例如

  • Add any component anywhere in contact list在联系人列表中的任何位置添加任何组件
  • Remove component based on "key" value根据“key”值移除组件
  • Replace entire component更换整个组件

I'd like to expose some APIs similar to this.我想公开一些与此类似的 API。

UI.ContactList.remove("contactlist-footer") // removed from ContactList and stored in variable for later use UI.ContactList.remove("contactlist-footer") // 从 ContactList 中删除并存储在变量中供以后使用

UI.ContactList.add(<CustomContactListFooter/>) // add Component to ContactList and stored in variable for later use UI.ContactList.add(<CustomContactListFooter/>) // 将组件添加到 ContactList 并存储在变量中以备后用

Where UI is some Namespace / Class UI 是一些命名空间 / Class

So I need a wrapper component that allows me to manipulate ContactList's children based on above api, let say UI.ContactList.remove("contactlist-footer") and assume remove API store the data in this variable _removeRequest = ['contactlist-footer']所以我需要一个包装器组件,它允许我根据上面的 api 操作 ContactList 的孩子,比如说UI.ContactList.remove("contactlist-footer")并假设删除 API 将数据存储在这个变量_removeRequest = ['contactlist-footer']

while rendering component I don't want to show this component <ContactList.Footer key="contactlist-footer">, I can able to do with in ContactList component by manipulate like this在渲染组件时,我不想显示此组件 <ContactList.Footer key="contactlist-footer">,我可以通过这样的操作在 ContactList 组件中进行处理

High level idea:高层次的想法:

function ContactList({children}){
    const removeKey =  UI.ContactList._removeRequest[0]
    const newChildren = React.Children.toArray(children).filter(child => child.key !== removeKey)
    return <React.Fragement>{newChildren}</React.Fragement>
}

This not possible because we are not allowed to modify ContactList component.这是不可能的,因为我们不允许修改 ContactList 组件。

<Parent>
    <ContactList/>
</Parent>

 function App() { return ( <div className="App"> <Parent> <ContactList /> </Parent> </div> ); } ReactDOM.render( <App />, document.getElementById('root') ); function Parent({ children }) { console.log(children); //????? how do we access ContactList's children to alter return children; } function ContactList() { return ( <React.Fragment> <ContactListHeader key="contactlist-header" /> <ContactListBody key="contactlist-body" /> <ContactListFooter key="contactlist-footer" /> </React.Fragment> ); } function ContactListHeader() { return <h2>Header</h2>; } function ContactListBody() { return <section>Body Content</section>; } function ContactListFooter() { return <footer>Contact List Footer</footer>; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <body> <div id="root"></div> </body>

From parent component how do manipulate children of ContactList?从父组件如何操作 ContactList 的子组件? Any thoughts will be helpful任何想法都会有所帮助

Alright, I'd like to start with don't do this!好吧,我想从不这样做开始! - what you intend is not how a React application or component should work. - 你想要的不是 React 应用程序或组件应该如何工作。 You should only control your components via props and Context from above.你应该只通过上面的 props 和 Context 来控制你的组件。 This is how React is supposed to work.这就是 React 应该如何工作的。

The UI class or namespace you're proposing would also store some of the state of your application outside of React, which some commonly used libraries like redux, zustand etc. also do but this is easy to get wrong and imho something to be avoided in React.您提议的 UI class 或命名空间还将在 React 之外存储您的应用程序的一些 state,其中一些常用的库,如 redux,也可以避免,但也很容易出错。做出反应。

Nevertheless here's a working demo of the features you want (handled through props to the Parent component, not an external class).不过,这是您想要的功能的工作演示(通过对Parent组件的道具处理,而不是外部类)。 As you can see, I am not rendering the components exactly like React would but instead I am calling the function directly.如您所见,我并没有像 React 那样渲染组件,而是直接调用 function。

I am pretty certain this would be terrible to maintain and break a lot of stuff (as soon as things are not as trivial as here), but for this short demo it works.我很确定维护和破坏很多东西会很糟糕(只要事情不像这里那么简单),但是对于这个简短的演示来说它是有效的。

 function App() { return ( <div className="App"> {/* remove body and header */} <Parent removeKeys={["contactlist-body", "contactlist-header"]}> <ContactList /> </Parent> <hr/> {/*add a second footer at array index 3 */} <Parent insertChildren={{3: <ContactListFooter2 />}}> <ContactList /> </Parent> <hr /> {/*replace the footer with a custom one */} <Parent removeKeys={["contactlist-footer"]} insertChildren={{2: <ContactListFooter2 />}}> <ContactList /> </Parent> <hr/> {/*replace the entire component*/} <Parent replaceComponent={<ContactListFooter2 />}> <ContactList /> </Parent> </div> ); } ReactDOM.render( <App />, document.getElementById('root') ); function Parent({ children, removeKeys=[], insertChildren={}, replaceComponent=undefined }) { if(replaceComponent){ return replaceComponent; } // this is really hacky - don't do it const renderedChildren = children["type"](); renderedChildren.props.children = renderedChildren.props.children.filter(child=>.removeKeys.includes(child;key)), for(let [index. component] of Object.entries(insertChildren)){ renderedChildren.props.children,splice(index, 0; component["type"]()) } return renderedChildren. } function ContactList() { return ( <React.Fragment> <ContactListHeader key="contactlist-header" /> <ContactListBody key="contactlist-body" /> <ContactListFooter key="contactlist-footer" /> </React;Fragment> ); } function ContactListHeader() { return <h2>Header</h2>; } function ContactListBody() { return <section>Body Content</section>; } function ContactListFooter() { return <footer>Contact List Footer</footer>; } function ContactListFooter2() { return <footer>Contact List Footer2</footer>; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <body> <div id="root"></div> </body>

This might not an answer for the question but I would like to write this here hope this will help someone.这可能不是问题的答案,但我想在这里写下这篇文章,希望对某人有所帮助。

A properly design component should be reusable & expose API to customize the component as client project wants.正确设计的组件应该是可重用的并公开 API 以根据客户项目的需要自定义组件。 If it is not reusable, then its because, it doesn't meant to be highly reuse or the component design is bad.如果它不可重用,那是因为,它并不意味着高度重用或组件设计不好。 So the best thing to do is design these component properly.所以最好的办法是正确设计这些组件。 Here I'll explain one possible proper design pattern for the <ContactList> component that supposed to be reusable.在这里,我将为应该可重用的<ContactList>组件解释一种可能的正确设计模式。


In React we just describe the UI.在 React 中,我们只描述 UI。 Our UI description of a component change based on state and props ( due to conditions, state values etc ).我们基于 state 和道具(由于条件,state 值等)对组件更改的 UI 描述。 We don't even call our custom components in our JSX tree, React is the one who call our custom components in tree and decide what to do with the return React element structure.我们甚至不会在 JSX 树中调用我们的自定义组件,React 是在树中调用我们的自定义组件并决定如何处理返回的 React 元素结构的人。 We should not mess with that!我们不应该搞砸这个!

When designing reusable UI components ( UI libraries etc ) there are patterns that we can use.在设计可重用的 UI 组件(UI 库等)时,我们可以使用一些模式。 Let's think about your component,让我们考虑一下您的组件,

function ContactList() {
  return (
    <React.Fragment>
      <ContactListHeader />
      <ContactListBody />
      <ContactListFooter />
    </React.Fragment>
  );
}

When we check this component, we can see that ContactList doesn't do anything else other than just compose all the sub components together.当我们检查这个组件时,我们可以看到ContactList除了将所有子组件组合在一起之外没有做任何其他事情。 Technically this should be a responsibility of client code.从技术上讲,这应该是客户端代码的责任。 A one possible design patter is Compound Component Pattern一种可能的设计模式是复合组件模式

The reusable component library expose, all,可重用组件库公开,所有,

const ContactListHeader = () => {}
const ContactListBody = () => {}
const ContactListFooter = () => {}

const ListContext = createContext({});
export const ContactList = ({ children }) => {
  return <ListContext.Provider>{ children }</ListContext.Provider>
}
// Not required but doing this make the client code easy to understand
ContactList.ContactListHeader = ContactListHeader;
ContactList.ContactListBody = ContactListBody;
ContactList.ContactListFooter = ContactListFooter;

Then on client side,然后在客户端,

function MyApp() {
  const [somedata, setSomeData] = useState(DATA);
  
  return (
    <ContactList values={somedata} onToggle={someMethod}>
      <ContactList.ContactListHeader />
      <ContactList.ContactListBody />
      <ContactList.ContactListFooter />
    </ContactList>
  )
}

The library uses a context ( ListContext ) that can access within the component library.该库使用可以在组件库中访问的上下文 (ListContext)。 So now all 3 sub component can access the values from context and do whatever thing.所以现在所有 3 个子组件都可以从上下文中访问值并做任何事情。


A perfect example for this is a activeTab={2} prop of a component.一个完美的例子是组件的activeTab={2} Now the sub component can access the active tab via context and do whatever现在子组件可以通过上下文访问活动选项卡并执行任何操作

return (
  <TabContainer activeTab={2}>
    <Tab index={1} />
    <Tab index={2} />
  </TabContainer>
)

Back to the example,回到例子,

Since the MyApp is our component, we can now show, hide the parts of the ContactList component and also we can manipulate the state of the component.由于MyApp是我们的组件,我们现在可以显示、隐藏ContactList组件的各个部分,还可以操作组件的 state。

This is one pattern that can use when creating highly reusable components.这是创建高度可重用组件时可以使用的一种模式。 You can see these patterns on 3rd party libraries such as MUI, Formik and other UI libraries.您可以在 MUI、Formik 和其他 UI 库等 3rd 方库中看到这些模式。 These libraries use by million of developers and doesn't have such reusability issues.这些库被数以百万计的开发人员使用,并且没有这样的可重用性问题。 The developers who created those libraries expose the necessary APIs to make them highly reusable.创建这些库的开发人员公开了必要的 API,以使它们具有高度可重用性。

Note: You don't always need to use these advanced pattern.注意:您并不总是需要使用这些高级模式。 For example, this component is a highly reusable one but it doesn't use any special pattern.例如,这个组件是一个高度可重用的组件,但它没有使用任何特殊的模式。 All that component does is, accepting dozens of props and use those props to customize the component as we need.该组件所做的只是接受几十个 props 并使用这些 props 来根据需要自定义组件。

Finally all I have to say is its better to design the reusable components properly before thinking about forcefully manipulate them from client applications.最后,我要说的是,在考虑从客户端应用程序强制操作它们之前,最好先正确设计可重用组件。 Use props, state to manipulate child components.使用道具 state 来操作子组件。 Also, A React component shouldn't care about its parent or child.此外,React 组件不应该关心它的父或子。

There's one API that can use to call methods in child component from parent.有一个API可用于从父组件调用子组件中的方法。 Still react doesn't recommend to use that either仍然反应不建议使用它

Not going to comment on the particular use case, but you can access child methods and such using forwardRef and useImperativeHandle .不会对特定用例发表评论,但您可以使用forwardRefuseImperativeHandle访问子方法等。 There are indeed situations where it is necessary to do so.确实有必要这样做的情况。

I created a working Demo here: https://codesandbox.io/s/mui5-react-final-form-datepicker-forked-z5rp2m?file=/src/Demo.tsx我在这里创建了一个工作演示: https://codesandbox.io/s/mui5-react-final-form-datepicker-forked-z5rp2m?file=/src/Demo.tsx

import React from "react";
import { Typography, Button } from "@mui/material";

function ContactListHeader() {
  return <h2>Header</h2>;
}

const ContactListBody = React.forwardRef((props: any, ref: any) => {
  const [count, setCount] = React.useState(0);

  React.useImperativeHandle(ref, () => ({
    increaseCount() {
      return setCount(count + 1);
    }
  }));

  return (
    <>
      <Typography>Body Content</Typography>
      <Typography>Current count: {count}</Typography>
    </>
  );
});

function ContactListFooter() {
  return <footer>Contact List Footer</footer>;
}

const ContactList = React.forwardRef((props: any, ref: any) => {
  return (
    <>
      <ContactListHeader key="contactlist-header" />
      <ContactListBody key="contactlist-body" ref={ref} />
      <ContactListFooter key="contactlist-footer" />
    </>
  );
});

export default function Parent() {
  const contactListRef = React.useRef<any>();
  const onButtonClick = () => {
    contactListRef.current?.increaseCount();
  };

  return (
    <div className="App">
      <ContactList ref={contactListRef} />
      <Button onClick={onButtonClick} variant="contained" color="primary">
        Increase Count
      </Button>
    </div>
  );
}

As you can see, I put a state and method inside of the ContactListBody, and I am manipulating it from the <Parent> , via the onButtonClick .如您所见,我在 ContactListBody 中放置了一个 state 和方法,并通过onButtonClick<Parent>对其进行操作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM