Имея контекст React в отдельном файле, невозможно заставить компонент не перерисовывать - PullRequest
1 голос
/ 01 августа 2020

У меня есть простой пример React Context, который использует useMemo для запоминания функции, и все дочерние компоненты повторно визуализируются при нажатии на любой из них. Я пробовал несколько альтернатив (закомментированных), но ни одна из них не работает. См. Код в stackblitz и ниже.

https://stackblitz.com/edit/react-yo4eth

Индекс. js

import React from "react";
import { render } from "react-dom";
import Hello from "./Hello";

import { GlobalProvider } from "./GlobalState";

function App() {
  return (
    <GlobalProvider>
      <Hello />
    </GlobalProvider>
  );
}

render(<App />, document.getElementById("root"));

GlobalState. js

import React, {
  createContext,useState,useCallback,useMemo
} from "react";

export const GlobalContext = createContext({});

export const GlobalProvider = ({ children }) => {
  const [speakerList, setSpeakerList] = useState([
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true },
  ]);

  const clickFunction = useCallback((speakerIdClicked) => {
    setSpeakerList((currentState) => {
      return currentState.map((rec) => {
        if (rec.id === speakerIdClicked) {
          return { ...rec, favorite: !rec.favorite };
        }
        return rec;
      });
    });
  },[]);

  // const provider = useMemo(() => {
  //   return { clickFunction: clickFunction, speakerList: speakerList };
  // }, []);

  //const provider = { clickFunction: clickFunction, speakerList: speakerList };

  const provider = {
    clickFunction: useMemo(() => clickFunction,[]),
    speakerList: speakerList,
  };

  return (
    <GlobalContext.Provider value={provider}>{children}</GlobalContext.Provider>
  );
};

Привет. js

import React, {useContext} from "react";

import Speaker from "./Speaker";

import { GlobalContext } from './GlobalState';

export default () => {
  const { speakerList } = useContext(GlobalContext);

  return (
    <div>
      {speakerList.map((rec) => {
        return <Speaker speaker={rec} key={rec.id}></Speaker>;
      })}
    </div>
  );
};

Спикер. js

import React, { useContext } from "react";

import { GlobalContext } from "./GlobalState";

export default React.memo(({ speaker }) => {
  console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);

  const { clickFunction } = useContext(GlobalContext);

  return (
    <>
      <button
        onClick={() => {
          clickFunction(speaker.id);
        }}
      >
        {speaker.name} {speaker.id}{" "}
        {speaker.favorite === true ? "true" : "false"}
      </button>
    </>
  );
});

Ответы [ 2 ]

1 голос
/ 01 августа 2020

Пара проблем в вашем коде:

  • Вы уже запомнили clickFunction с помощью useCallback, не нужно использовать useMemo хук.

  • Вы потребляете компонент Context в Speaker. Это то, что вызывает повторный рендеринг всех экземпляров компонента Speaker.

Решение:

Поскольку вы не хотите чтобы передать clickFunction в качестве опоры из компонента Hello в компонент Speaker и вы хотите получить доступ к clickFunction непосредственно в компоненте Speaker, вы можете создать отдельный Context для clickFunction.

Это будет работать, потому что извлечение clickFunction в отдельный Context позволит компоненту Speaker не использовать GlobalContext. При нажатии любой кнопки GlobalContext будет обновлен, что приведет к повторному рендерингу всех компонентов, потребляющих GlobalContext. Поскольку компонент Speaker использует отдельный контекст, который не обновляется, он предотвратит повторный рендеринг всех экземпляров компонента Speaker при нажатии любой кнопки.

Demo

const GlobalContext = React.createContext({});

const GlobalProvider = ({ children }) => {
  const [speakerList, setSpeakerList] = React.useState([
    { name: "Crockford", id: 101, favorite: true },
    { name: "Gupta", id: 102, favorite: false },
    { name: "Ailes", id: 103, favorite: true }
  ]);

  return (
    <GlobalContext.Provider value={{ speakerList, setSpeakerList }}>
      {children}
    </GlobalContext.Provider>
  );
};

const ClickFuncContext = React.createContext();

const ClickFuncProvider = ({ children }) => {
  const { speakerList, setSpeakerList } = React.useContext(GlobalContext);

  const clickFunction = React.useCallback(speakerIdClicked => {
    setSpeakerList(currentState => {
      return currentState.map(rec => {
        if (rec.id === speakerIdClicked) {
          return { ...rec, favorite: !rec.favorite };
        }
        return rec;
      });
    });
  }, []);

  return (
    <ClickFuncContext.Provider value={clickFunction}>
      {children}
    </ClickFuncContext.Provider>
  );
};

const Speaker = React.memo(({ speaker }) => {
  console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
  const clickFunction = React.useContext(ClickFuncContext)

  return (
    <div>
      <button
        onClick={() => {
          clickFunction(speaker.id);
        }}
      >
        {speaker.name} {speaker.id}{" "}
        {speaker.favorite === true ? "true" : "false"}
      </button>
    </div>
  );
});

function SpeakerList() {
  const { speakerList } = React.useContext(GlobalContext);

  return (
    <div>
      {speakerList.map(rec => {
        return (
          <Speaker speaker={rec} key={rec.id} />
        );
      })}
    </div>
  );
};

function App() {
  return (
    <GlobalProvider>
      <ClickFuncProvider>
        <SpeakerList />
      </ClickFuncProvider>
    </GlobalProvider>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Вы также можете увидеть эту демонстрацию на StackBlitz

1 голос
/ 01 августа 2020

это не сработает, если вы получите доступ к clickFuntion в дочерних элементах из provider, потому что каждый раз, когда вы обновляете состояние, объект поставщика будет воссоздан, и если вы оберните этот объект в useMemo следующим образом:

  const provider = useMemo(()=>({
        clickFunction,
        speakerList,
      }),[speakerList])

он будет воссоздаваться каждый раз, когда запускается clickFunction.

вместо этого вам нужно передать его в качестве опоры дочерним элементам следующим образом:

import React, {useContext} from "react";

import Speaker from "./Speaker";

import { GlobalContext } from './GlobalState';

export default () => {
  const { speakerList,clickFunction } = useContext(GlobalContext);

  return (
    <div>
      {speakerList.map((rec) => {
        return <Speaker speaker={rec} key={rec.id} clickFunction={clickFunction }></Speaker>;
      })}
    </div>
  );
};

и для объекта поставщика не нужно добавлять useMemo в функцию clickFunction он уже заключен в useCallback, что эквивалентно useMemo(()=>fn,[]):

  const provider = {
        clickFunction,
        speakerList,
      }

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

import React from "react";

export default React.memo(({ speaker,clickFunction }) => {
  console.log("render")
  return (
    <>
      <button
        onClick={() => {
          clickFunction(speaker.id);
        }}
      >
        {speaker.name} {speaker.id}{" "}
        {speaker.favorite === true ? "true" : "false"}
      </button>
    </>
  );
});
...