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

1.5k Views Asked by At

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
  • Replace entire component

I'd like to expose some APIs similar to this.

UI.ContactList.remove("contactlist-footer") // removed from ContactList and stored in variable for later use

UI.ContactList.add(<CustomContactListFooter/>) // add Component to ContactList and stored in variable for later use

Where UI is some Namespace / 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']

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

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.

<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 ? Any thoughts will be helpful

3

There are 3 best solutions below

1
On BEST ANSWER

Alright, I'd like to start with don't do this! - what you intend is not how a React application or component should work. You should only control your components via props and Context from above. This is how React is supposed to work.

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.

Nevertheless here's a working demo of the features you want (handled through props to the Parent component, not an external class). As you can see, I am not rendering the components exactly like React would but instead I am calling the function directly.

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>

0
On

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


In React we just describe the UI. Our UI description of a component change based on state and props ( due to conditions, state values etc ). 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. We should not mess with that!

When designing reusable UI components ( UI libraries etc ) there are patterns that we can use. 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. 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. So now all 3 sub component can access the values from context and do whatever thing.


A perfect example for this is a activeTab={2} prop of a component. 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.

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

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.

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. Also, A React component shouldn't care about its parent or child.

There's one API that can use to call methods in child component from parent. Still react doesn't recommend to use that either

0
On

Not going to comment on the particular use case, but you can access child methods and such using forwardRef and useImperativeHandle. 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

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.