【问题标题】:Update state from deeply nested component without re-rendering parents从深度嵌套的组件更新状态而不重新渲染父组件
【发布时间】:2020-05-20 11:23:55
【问题描述】:

我有一个大致如下结构的表单页面:

<Layout>
  <Page>
    <Content>
      <Input />
      <Map />
    </Content>
  </Page>
  <Button />
</Layout>

地图组件应该只渲染一次,因为渲染时会触发动画。这意味着内容、页面和布局根本不应该重新渲染。

当输入为空时,布局内的按钮应该被禁用。 Input 的值不受 Content 控制,因为状态更改会导致重新渲染 Map。

我尝试了一些不同的方法(使用 refs、useImperativeHandle 等),但没有一个解决方案对我来说很干净。在不更改布局、页面或内容状态的情况下,将输入状态连接到按钮状态的最佳方法是什么?请记住,这是一个相当小的项目,代码库使用“现代” React 实践(例如钩子),并且没有像 Redux、MobX 等那样的全局状态管理。

【问题讨论】:

    标签: reactjs react-hooks next.js react-context react-state-management


    【解决方案1】:

    这是一个示例 (click here to play with it),它可以避免重新渲染 Map。但是,它会重新渲染其他组件,因为我传递了children。但是,如果地图是最重的,那应该可以解决问题。为了避免渲染其他组件,您需要摆脱 children 属性,但这很可能意味着您将需要 redux。您也可以尝试使用上下文,但我从未使用过它,所以我不知道它会如何影响一般渲染

    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>
      );
    }
    

    更新

    下面是version,其上下文不会重新渲染除输入和按钮之外的组件:

    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>
      );
    }
    

    解决方案依赖于使用memo 来避免在父级重新渲染时进行渲染,并最大限度地减少传递给组件的属性数量。 Ref 仅用于渲染计数器

    【讨论】:

    • 干得好,我也喜欢你的渲染计数器解决方案。
    • 第二种解决方案正是我想要的。我完全忘记了 React.memo!
    • 这个回答对我影响很大,谢谢分享!
    【解决方案2】:

    我有一个确定的方法来解决它,但有点复杂。 使用 createContext 和 useContext 将数据从布局传输到输入。这样你就可以在不使用 Redux 的情况下使用全局状态。 (redux 也使用上下文来分发它的数据)。使用上下文可以防止 Layout 和 Imput 之间所有组件的属性更改。

    我有第二个更简单的选择,但我不确定它是否适用于这种情况。您可以将 Map 包装到 React.memo 以防止在其属性未更改时进行渲染。快速尝试,它可能会奏效。

    更新

    我在 Map 组件上试用了 React.memo。我修改了根纳迪的例子。它在没有上下文的情况下工作得很好。您只需将 value 和 setValue 传递给链中的所有组件。您可以轻松传递所有属性,例如:&lt;Content {...props} /&gt; 这是最简单的解决方案。

    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

    【讨论】:

      猜你喜欢
      • 2021-04-06
      • 2022-06-29
      • 1970-01-01
      • 2019-01-31
      • 2018-06-05
      • 2023-03-09
      • 1970-01-01
      • 2020-12-30
      • 2019-10-06
      相关资源
      最近更新 更多