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



我想制作一个具有独特模式的高度可重用的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>

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

  • 在联系人列表中的任何位置添加任何组件
  • 删除基于";键";价值
  • 更换整个组件

我想公开一些类似的API。

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

UI.ContactList.add(<CustomContactListFooter/>)//将组件添加到ContactList并存储在变量中以供以后使用

其中UI是一些命名空间/类

因此,我需要一个包装器组件,它允许我基于上面的api来操作ContactList的子项,比如UI.ContactList.remove("contactlist-footer"),并假设remove api将数据存储在这个变量_removeRequest = ['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的工作原理。

你提议的UI类或命名空间也会在React之外存储你的应用程序的一些状态,一些常用的库,如redux、zustand等,也会这样做,但这很容易出错,因此在React中需要避免。

尽管如此,这里有一个您想要的功能的工作演示(通过道具处理到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>

这可能不是问题的答案,但我我想在这里写这篇文章,希望这能帮助到别人。

一个设计得当的组件应该是可重用的&公开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)。因此,现在所有3个子组件都可以从上下文访问值,并执行任何操作。


一个完美的例子是组件的activeTab={2}道具。现在,子组件可以通过上下文访问活动选项卡,并执行任何操作

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

返回示例,

由于MyApp是我们的组件,我们现在可以显示、隐藏ContactList组件的各个部分,还可以操纵组件的状态。

这是在创建高度可重用组件时可以使用的一种模式。您可以在第三方库(如MUI、Formik和其他UI库)上看到这些模式。这些库被数以百万计的开发人员使用,并且没有这样的可重用性问题。创建这些库的开发人员公开了必要的API,使它们具有高度可重用性。

注意:您并不总是需要使用这些高级模式。例如这组件是一个高度可重用的组件,但它不使用任何特殊的图案该组件所做的就是接受数十个道具并使用根据我们的需要定制组件的那些道具。

最后,我要说的是,在考虑从客户端应用程序强制操作可重用组件之前,最好正确地设计它们。使用道具、状态来操纵子组件。此外,React组件不应该关心它的父级或子级。

有一个API可以用来从父组件调用子组件中的方法。仍然反应不建议使用任何

不打算对特定用例发表评论,但您可以使用forwardRef和useImperativeHandle访问子方法等。确实存在这样做的必要性

我在这里创建了一个工作演示: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>对其进行操作。

最新更新