Обновление состояния из глубоко вложенного компонента без повторной визуализации родителей - PullRequest
2 голосов
/ 04 февраля 2020

input and map inside content, content inside page, page and button inside layout

У меня есть страница формы, более или менее структурированная следующим образом:

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

Компонент карты должен отображаться только один раз, поскольку анимация, которая запускается при рендеринге Это означает, что Content, Page и Layout не должны повторно отображаться вообще.

Кнопка внутри Layout должна быть отключена, когда вход пуст. Значение Input не контролируется Content, поскольку изменение состояния может вызвать повторную визуализацию карты.

Я пробовал несколько разных вещей (используя refs, useImperativeHandle, et c) но ни одно из решений не кажется мне очень чистым. Как лучше всего go подключить состояние входа к состоянию кнопки, не изменяя состояние макета, страницы или содержимого? Имейте в виду, что это довольно маленький проект, и кодовая база использует "современные" методы React (например, перехватчики) и не имеет глобального управления состоянием, такого как Redux, MobX и т. Д. c.

Ответы [ 2 ]

2 голосов
/ 04 февраля 2020

Вот пример ( нажмите здесь, чтобы поиграть с ним ), чтобы избежать повторного рендеринга Map. Тем не менее, он перерисовывает другие компоненты, потому что я передаю children. Но если карта самая тяжелая, это должно сработать. Чтобы избежать рендеринга других компонентов, вам нужно избавиться от children prop, но это, скорее всего, означает, что вам понадобится приставка. Вы также можете попытаться использовать контекст, но я никогда не работал с ним, поэтому не знаю, как это повлияет на рендеринг в целом

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

Обновление

Ниже версия с контекстом, который не выполняет повторный рендеринг компонентов, кроме ввода и кнопки:

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, чтобы избежать рендеринга при повторном рендеринге родителей и минимизировать количество свойств, передаваемых компонентам. Ссылки используются только для счетчиков рендера

1 голос
/ 04 февраля 2020

У меня есть верный способ ее решить, но немного сложнее. Используйте createContext и useContext для передачи данных из макета для ввода. Таким образом, вы можете использовать глобальное состояние без Redux. (Редукс также использует контекст для распространения своих данных). Используя контекст, вы можете предотвратить изменение свойств во всех компонентах между Layout и Imput.

У меня есть второй более простой вариант, но я не уверен, что он работает в этом случае. Вы можете обернуть Map в React.memo, чтобы предотвратить рендер, если его свойство не изменилось. Это быстро попробовать, и это может сработать.

ОБНОВЛЕНИЕ

Я опробовал React.memo на компоненте Map. Я изменил пример Геннадия. И это прекрасно работает без контекста. Вы просто передаете значение и setValue всем компонентам по цепочке. Вы можете легко передать все свойства, например: <Content {...props} /> Это самое простое решение.

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

...