Home > OS >  How to change child state from another child without re-rendering parent
How to change child state from another child without re-rendering parent

Time:11-04

I want to change the content of a child component in response to a user event in another child component without causing the parent to re-render.

I've tried storing the child state setter to a variable in the parent but it is not defined by the time the children have all rendered, so it doesn't work.

Is there a minimal way to accomplish this (without installing a state management library)?

ChildToChild.tsx

import React, {
  Dispatch,
  SetStateAction,
  useEffect,
  useRef,
  useState,
} from "react";

export default function ChildToChild() {
  const renderCounter = useRef(0);
  renderCounter.current = renderCounter.current   1;

  let setChildOneContent;
  const childOneContentController = (
    setter: Dispatch<SetStateAction<string>>
  ) => {
    setChildOneContent = setter;
  };

  return (
    <div>
      <h1>Don't re-render me please</h1>
      <p>No. Renders: {renderCounter.current}</p>
      <ChildOne childOneContentController={childOneContentController} />
      <ChildTwo setChildOneContent={setChildOneContent} />
    </div>
  );
}

function ChildOne({
  childOneContentController,
}: {
  childOneContentController: (setter: Dispatch<SetStateAction<string>>) => void;
}) {
  const [content, setContent] = useState("original content");

  useEffect(() => {
    childOneContentController(setContent);
  }, [childOneContentController, setContent]);

  return (
    <div>
      <h2>Child One</h2>
      <p>{content}</p>
    </div>
  );
}

function ChildTwo({
  setChildOneContent,
}: {
  setChildOneContent: Dispatch<SetStateAction<string>> | undefined;
}) {
  return (
    <div>
      <h2>Child Two</h2>
      <button
        onClick={() => {
          if (setChildOneContent) setChildOneContent("content changed");
        }}
      >
        Change Child One Content
      </button>
    </div>
  );
}

CodePudding user response:

You should be able to achieve by

  1. Using state in child components
  2. Using a callback function in the parent component that calls the setState function of the child component. This will trigger re-render of the child but not of itself (parent).

Demo: https://codesandbox.io/s/child-re-rendering-only-x37ol

CodePudding user response:

For an one-off, you could use the following (another ref to store & use the ChildOne setContent:

import React, {
  Dispatch,
  SetStateAction,
  useEffect,
  useRef,
  useState
} from "react";

function ChildOne({
  setChildOneRef
}: {
  setChildOneRef: React.MutableRefObject<React.Dispatch<
    React.SetStateAction<string>
  > | null>;
}) {
  const [content, setContent] = useState("original content");

  useEffect(() => {
    setChildOneRef.current = setContent;
  }, [setChildOneRef]);

  return (
    <div>
      <h2>Child One</h2>
      <p>{content}</p>
    </div>
  );
}

function ChildTwo({
  setChildOneRef
}: {
  setChildOneRef: React.MutableRefObject<React.Dispatch<
    React.SetStateAction<string>
  > | null>;
}) {
  return (
    <div>
      <h2>Child Two</h2>
      <button
        onClick={() => {
          setChildOneRef.current?.("content changed");
        }}
      >
        Change Child One Content
      </button>
    </div>
  );
}

export function ChildToChild() {
  const renderCounter = useRef(0);
  renderCounter.current = renderCounter.current   1;

  const setChildOneRef = useRef<Dispatch<SetStateAction<string>> | null>(null);
  return (
    <div>
      <h1>Don't re-render me please</h1>
      <p>No. Renders: {renderCounter.current}</p>
      <ChildOne setChildOneRef={setChildOneRef} />
      <ChildTwo setChildOneRef={setChildOneRef} />
    </div>
  );
}

If this pattern is common in your code, you may still want to use state management library or evaluate if "childs" should be really separated.

CodePudding user response:

You can pass a hook back to the parent by passing a callback to the first child, then pass that to the next child. It's pretty sketch, but yeah it's possible. I have not verified that this does not re-render the parent, but I'm fairly certain it would not considering the hook is attached to the child and not the parent.

import './App.css';
import { useState } from "react";
   
const Child1 = ( stateCb ) => {
    const [count, setCount] = useState( 0 )
    stateCb( count, setCount )
    return <div>
        {count}
    </div>
}

const Child2 = ( count, setCount ) => {
    return <button onClick={() => setCount( count   1 )}>
        clickme
    </button>
}

function App() {
    let count, setCount
    return (
        <div className="App">
            {Child1(( c, sc ) => {
                count = c;
                setCount = sc
            })}
            {Child2(count, setCount)}
        </div>
    );
}

export default App;

CodePudding user response:

The event could be propagated over the RxJS Subject, with value if it is needed.

// in parent

// const subject = new rxjs.Subject();
const subject = { // or create lightweight alternative
  nextHandlers: [],
  next: function(value) {
    this.nextHandlers.forEach(handler => handler(value));
  },
  subscribe: function(nextHandler) {
    this.nextHandlers.push(nextHandler);
    return { 
      unsubscribe: () => {
        this.nextHandlers.splice(
          this.nextHandlers.indexOf(nextHandler),
          1
        );
      }
    };
  }
};

// provide next method to the childOne
onClick={value => subject.next(value)}

// provide subscribe method to the childTwo
subscribeOnClick={(nextHandler) => subject.subscribe(nextHandler)}

// in childTwo
useEffect(() => {
  const subscription = subscribeOnClick(value => setContent(value));
  return () => subscription.unsubscribe();
}, [subscribeOnClick]);

CodePudding user response:

You can store the Dispatch<SetStateAction<T>> function in a ref in the parent:

import ReactDOM from 'react-dom';

import {
  Dispatch,
  MutableRefObject,
  ReactElement,
  SetStateAction,
  useEffect,
  useRef,
  useState,
} from 'react';

type ChildProps = {
  setValueRef: MutableRefObject<Dispatch<SetStateAction<string>> | undefined>;
};

function Child1 (props: ChildProps): ReactElement {
  const [dateString, setDateString] = useState(new Date().toISOString());

  useEffect(() => {
    props.setValueRef.current = setDateString;
  }, [props.setValueRef, setDateString]);

  return <div>{dateString}</div>;
}

function Child2 (props: ChildProps): ReactElement {
  const handleClick = (): void => {
    props.setValueRef.current?.(new Date().toISOString());
  };

  return <button onClick={handleClick}>Update</button>;
}

function Parent (): ReactElement {
  const setValueRef = useRef<Dispatch<SetStateAction<string>> | undefined>();
  const renderCountRef = useRef(0);
  renderCountRef.current  = 1;

  return (
    <div>
      <div>Render count: {renderCountRef.current}</div>
      <Child1 {...{setValueRef}} />
      <Child2 {...{setValueRef}} />
    </div>
  );
}

ReactDOM.render(<Parent />, document.getElementById('root'));

Here's the same code in a runnable snippet (with types erased and import statements converted to use the UMD version of React):

Show code snippet

<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>

<div id="root"></div>

<script type="text/babel" data-type="module" data-presets="react">

const {useEffect, useRef, useState} = React;

function Child1 (props) {
  const [dateString, setDateString] = useState(new Date().toISOString());

  useEffect(() => {
    props.setValueRef.current = setDateString;
  }, [props.setValueRef, setDateString]);

  return <div>{dateString}</div>;
}

function Child2 (props) {
  const handleClick = () => {
    props.setValueRef.current?.(new Date().toISOString());
  };

  return <button onClick={handleClick}>Update</button>;
}

function Parent () {
  const setValueRef = useRef();
  const renderCountRef = useRef(0);
  renderCountRef.current  = 1;

  return (
    <div>
      <div>Render count: {renderCountRef.current}</div>
      <Child1 {...{setValueRef}} />
      <Child2 {...{setValueRef}} />
    </div>
  );
}

ReactDOM.render(<Parent />, document.getElementById('root'));

</script>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

That being said, this is a strange pattern, and the scenario can be solved idiomatically using React.memo (this is the case it's designed for).

  • Related