score:5

Accepted answer

Here is an example (click here to play with it) that avoids re-render of Map. However, it re-renders other components because I pass children around. But if map is the heaviest, that should do the trick. To avoid rendering of other components you need to get rid of children prop but that most probably means you will need redux. You can also try to use context but I never worked with it so idk how it would affect rendering in general

import React, { useState, useRef, memo } from "react";
import "./styles.css";

const GenericComponent = memo(
  ({ name = "GenericComponent", className, children }) => {
    const counter = useRef(0);
    counter.current += 1;

    return (
      <div className={"GenericComponent " + className}>
        <div className="Counter">
          {name} rendered {counter.current} times
        </div>
        {children}
      </div>
    );
  }
);

const Layout = memo(({ children }) => {
  return (
    <GenericComponent name="Layout" className="Layout">
      {children}
    </GenericComponent>
  );
});

const Page = memo(({ children }) => {
  return (
    <GenericComponent name="Page" className="Page">
      {children}
    </GenericComponent>
  );
});

const Content = memo(({ children }) => {
  return (
    <GenericComponent name="Content" className="Content">
      {children}
    </GenericComponent>
  );
});

const Map = memo(({ children }) => {
  return (
    <GenericComponent name="Map" className="Map">
      {children}
    </GenericComponent>
  );
});

const Input = ({ value, setValue }) => {
  const onChange = ({ target: { value } }) => {
    setValue(value);
  };
  return (
    <input
      type="text"
      value={typeof value === "string" ? value : ""}
      onChange={onChange}
    />
  );
};

const Button = ({ disabled = false }) => {
  return (
    <button type="button" disabled={disabled}>
      Button
    </button>
  );
};

export default function App() {
  const [value, setValue] = useState("");

  return (
    <div className="App">
      <h1>SO Q#60060672</h1>

      <Layout>
        <Page>
          <Content>
            <Input value={value} setValue={setValue} />
            <Map />
          </Content>
        </Page>
        <Button disabled={value === ""} />
      </Layout>
    </div>
  );
}

Update

Below is version with context that does not re-render components except input and button:

import React, { useState, useRef, memo, useContext } from "react";
import "./styles.css";

const ValueContext = React.createContext({
  value: "",
  setValue: () => {}
});

const Layout = memo(() => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Layout rendered {counter.current} times</div>
      <Page />
      <Button />
    </div>
  );
});

const Page = memo(() => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Page rendered {counter.current} times</div>
      <Content />
    </div>
  );
});

const Content = memo(() => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Content rendered {counter.current} times</div>
      <Input />
      <Map />
    </div>
  );
});

const Map = memo(() => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Map rendered {counter.current} times</div>
    </div>
  );
});

const Input = () => {
  const { value, setValue } = useContext(ValueContext);

  const onChange = ({ target: { value } }) => {
    setValue(value);
  };

  return (
    <input
      type="text"
      value={typeof value === "string" ? value : ""}
      onChange={onChange}
    />
  );
};

const Button = () => {
  const { value } = useContext(ValueContext);

  return (
    <button type="button" disabled={value === ""}>
      Button
    </button>
  );
};

export default function App() {
  const [value, setValue] = useState("");

  return (
    <div className="App">
      <h1>SO Q#60060672, method 2</h1>

      <p>
        Type something into input below to see how rendering counters{" "}
        <s>update</s> stay the same
      </p>

      <ValueContext.Provider value={{ value, setValue }}>
        <Layout />
      </ValueContext.Provider>
    </div>
  );
}

Solutions rely on using memo to avoid rendering when parent re-renders and minimizing amount of properties passed to components. Ref's are used only for render counters

score:1

I have a sure way to solve it, but a little more complicated. Use createContext and useContext to transfer data from layout to input. This way you can use a global state without using Redux. (redux also uses context by the way to distribute its data). Using context you can prevent property change in all the component between Layout and Imput.

I have a second easier option, but I'm not sure it works in this case. You can wrap Map to React.memo to prevent render if its property is not changed. It's quick to try and it may work.

UPDATE

I tried out React.memo on Map component. I modified Gennady's example. And it works just fine without context. You just pass the value and setValue to all component down the chain. You can pass all property easy like: <Content {...props} /> This is the easiest solution.

import React, { useState, useRef, memo } from "react";
import "./styles.css";

const Layout = props => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Layout rendered {counter.current} times</div>
      <Page {...props} />
      <Button {...props} />
    </div>
  );
};

const Page = props => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Page rendered {counter.current} times</div>
      <Content {...props} />
    </div>
  );
};

const Content = props => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Content rendered {counter.current} times</div>
      <Input {...props} />
      <Map />
    </div>
  );
};

const Map = memo(() => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <div className="GenericComponent">
      <div className="Counter">Map rendered {counter.current} times</div>
    </div>
  );
});

const Input = ({ value, setValue }) => {
  const counter = useRef(0);
  counter.current += 1;

  const onChange = ({ target: { value } }) => {
    setValue(value);
  };

  return (
    <>
      Input rendedred {counter.current} times{" "}
      <input
        type="text"
        value={typeof value === "string" ? value : ""}
        onChange={onChange}
      />
    </>
  );
};

const Button = ({ value }) => {
  const counter = useRef(0);
  counter.current += 1;

  return (
    <button type="button" disabled={value === ""}>
      Button (rendered {counter.current} times)
    </button>
  );
};

export default function App() {
  const [value, setValue] = useState("");

  return (
    <div className="App">
      <h1>SO Q#60060672, method 2</h1>

      <p>
        Type something into input below to see how rendering counters{" "}
        <s>update</s> stay the same, except for input and button
      </p>
      <Layout value={value} setValue={setValue} />
    </div>
  );
}

https://codesandbox.io/s/weathered-wind-wif8b


Related Query

More Query from same tag