Search code examples
javascriptreactjstypescriptreact-children

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


Solution

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