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

5

我希望制作一个高度可重用的React组件,采用独特的模式。假设这个联系人列表是由另一个团队制作的;我们不能更改这些组件,并且它们遵循下面所示的结构。

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

示例联系人列表组件:

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

我想提供自定义联系人列表组件的选择,例如:

  • 在联系人列表中任意位置添加任何组件
  • 根据“key”值删除组件
  • 替换整个组件

我想要类似于以下API的一些公开接口。

UI.ContactList.remove("contactlist-footer")// 从ContactList中删除并存储到变量以备后用

UI.ContactList.add(<CustomContactListFooter/>)// 添加组件到ContactList并存储到变量以备后用

其中UI是某个命名空间/类

所以我需要一个包装组件来允许我基于上述API操作ContactList的子级,比如说 UI.ContactList.remove("contactlist-footer") 并假设删除API将数据存储在此变量中:_removeRequest = ['contactlist-footer']

在呈现组件时,我不想显示此组件 <ContactList.Footer key="contactlist-footer"> ,我可以通过 ContactList 组件内部的操作来实现,例如:

高层次的思路:

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>
}

这是不可能的,因为我们不允许修改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>

如何从父组件操作 ContactList 的子组件? 有什么建议吗?


你也可以问一个问题,为什么同一家公司中具有相同技能(React)的团队不被允许为他们实际依赖的代码建议适当的更改。而是通过对 React 进行黑客式的下游工作来保留不足的组件。也许那个组件不能被更改,因为之前实施的解决方法会被打破... - inwerpsel
3个回答

8

首先,我要强调一点不要这样做!——你的意图并不符合React应用程序或组件的工作方式。你应该通过上方传入 props 和 context 控制您的组件。这才是 React 所设计的正确方式。

你提出的 UI 类或命名空间还将在 React 之外存储应用程序的某些状态,这与一些常用的库(如 redux、zustand 等)类似,但易于出错,因此在 React 中应避免使用。

尽管如此,下面是你想要的功能的演示(通过 props 提供到Parent组件,而非外部类处理)。正如您所看到的,我并没有像React那样完全呈现组件,而是直接调用函数。

我非常确信这样的代码很难维护,即使稍微复杂一些也可能会引起许多问题,但对于这个短暂的演示来说,它还是能够正常工作的。

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>


谢谢@taxel,这非常接近我想要做的事情。我知道这不是一个建议的模式,但是感谢您的指导。 - Raja Jaganathan

2
这可能不是对问题的回答,但我想在这里写下这个希望能帮助到某些人。
一个正确设计的组件应该是可重用的,并公开API以便客户端项目自定义组件。如果它不能被重用,那么要么它不是高度可重用的,要么组件设计不好。所以最好的做法是正确地设计这些组件。在这里,我将解释一个可能适用于可重用的<ContactList>组件的正确设计模式。
在React中,我们只是描述UI。我们的组件的UI描述基于状态和属性更改(由于条件、状态值等)。我们甚至不会在JSX树中调用我们的自定义组件,React会在树中调用我们的自定义组件,并决定如何处理返回的React元素结构。我们不应该干扰这个过程!
当设计可重用的UI组件(UI库等)时,有一些可以使用的模式。让我们考虑你的组件。
function ContactList() {
  return (
    <React.Fragment>
      <ContactListHeader />
      <ContactListBody />
      <ContactListFooter />
    </React.Fragment>
  );
}

当我们检查这个组件时,我们可以看到ContactList除了将所有子组件组合在一起之外,什么也不做。从技术上讲,这应该是客户端代码的责任。一个可能的设计模式是复合组件模式
可重用的组件库公开了所有内容。
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;

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

图书馆使用一个上下文(ListContext),可以在组件库内进行访问。因此,现在所有三个子组件都可以从上下文中访问值并执行任何操作。
一个很好的例子是组件的activeTab={2}属性。现在,子组件可以通过上下文访问活动标签,并进行任何操作。
return (
  <TabContainer activeTab={2}>
    <Tab index={1} />
    <Tab index={2} />
  </TabContainer>
)

回到这个例子,由于MyApp是我们的组件,现在我们可以展示、隐藏ContactList组件的部分,并且我们也可以操纵组件的状态。这是创建高度可重用组件时可以使用的一种模式。你可以在第三方库(如MUI、Formik和其他UI库)中看到这些模式。这些库被数百万开发人员使用,没有这样的可重用性问题。创建这些库的开发人员公开了必要的API,使它们高度可重用。
请注意:您并不总是需要使用这些高级模式。例如,this组件是一个高度可重用的组件,但它并没有使用任何特殊模式。该组件所做的所有事情就是接受几十个props,并使用这些props来自定义组件以满足我们的需求。
最后我想说的是,在强制从客户端应用程序操纵可重用组件之前,最好先正确地设计可重用组件。使用props、state来操作子组件。此外,React组件不应关心其父组件或子组件。
有一个API可以用来从父组件调用子组件中的方法。但React也不推荐使用它。

2

不会针对特定用例发表评论,但是您可以使用forwardRefuseImperativeHandle访问子方法等。确实有必要这样做的情况。

我在此处创建了一个工作演示: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>
  );
}

正如您所看到的,我在ContactListBody中放置了一个状态和方法,并通过onButtonClick<Parent>进行操作。


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