#javascript #reactjs #react-hooks
#javascript #reactjs #реагирующие хуки
Вопрос:
Насколько я понимаю, поставщик контекста React обновляет своих потребителей всякий раз, когда изменяется значение контекста.
Все потребители, которые являются потомками поставщика, будут повторно отображаться при каждом изменении
value
реквизита поставщика. Распространение от поставщика к его потребителям-потомкам не зависит отshouldComponentUpdate
метода, поэтому потребитель обновляется, даже когда компонент-предок завершает обновление.Изменения определяются путем сравнения нового и старого значений с использованием того же алгоритма, что и Object.is.
Однако следующий код, похоже, указывает на обратное:
var themes = {
light: {
name: "Light",
foreground: "#000000",
background: "#eeeeee"
},
dark: {
name: "Dark",
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext({
theme: themes.light,
updateTheme: () => {}
});
let prevTheme = undefined;
function App() {
console.log("RE-RENDERING App...");
const stateArray = React.useState(themes.light);
const [theme, setTheme] = stateArray;
const [otherState, setOtherState] = React.useState(true);
function handleSetOtherState() {
console.log("SETTING OTHER STATE.....");
setOtherState(prevState => !prevState);
}
console.log("theme:", theme);
console.log("prevTheme:", prevTheme);
console.log(`Object.is(prevTheme, theme): ${Object.is(prevTheme, theme)}`);
prevTheme = theme;
return (
<ThemeContext.Provider value={stateArray}>
<Toolbar />
<button onClick={handleSetOtherState}>Change OtherState</button>
</ThemeContext.Provider>
);
}
class Toolbar extends React.PureComponent {
render() {
console.log("RE-RENDERING Toolbar (DOES NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
return (
<div>
<ThemedButton />
</div>
);
}
}
function ThemedButton() {
console.log("RE-RENDERING ThemedButton (SHOULD NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
const themeContext = React.useContext(ThemeContext);
const [theme, setTheme] = themeContext;
console.log("themeContext:", themeContext);
console.log("theme.name:", theme.name);
console.log("setTheme:", setTheme);
function handleToggleTheme() {
console.log("SETTING THEME STATE.....");
setTheme(
prevState =>
themes.dark
);
}
return <button onClick={handleToggleTheme}>Click me: {theme.name}</button>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"/>
Как видно, при нажатии Change OtherState
:
- Родительский компонент, заключающий в себе контекст,
Provider
выполнит повторный рендеринг, тем самым позволяяProvider
увидеть, что в значении контекста действительно не было никаких изменений - Дочерние элементы будут повторно отображаться, потому что это сделал их родитель, но этот процесс останавливается на полпути,
Toolbox
будучиPureComponent
- Итак, вся идея контекста
Provider
заключается в том, что он должен обновлять его толькоConsumers
в том случае, если изменилось значение в контекстном - Проверка изменений выполняется с помощью
Object.is
, как указано в документах (см. Выше) - Независимо от этого факта,
Consumer
(ThemedButton
) все еще обновляется приOtherState
изменениях - Этого не должно произойти, потому что значение контекста фактически не изменилось, и повторный рендеринг дочернего элемента останавливается посередине с помощью
PureComponent
- Обновление должно происходить только при изменении значения контекста
Consumers
, даже если повторный рендеринг остановлен в промежуточных компонентах сPureComponent
PS: Вы можете видеть, что значение контекста не изменяется при Change OtherState
нажатии, просмотрев Object.is
журнал консоли.
Вопрос
Почему выполняется ThemedButton
повторный рендеринг, когда значение контекста не изменилось?
Ответ №1:
useState
возвращает новый массив при каждом вызове. Таким образом, вы передаете новый массив в контекст при каждом рендеринге. Используйте MEMO для устранения проблемы.
var themes = {
light: {
name: "Light",
foreground: "#000000",
background: "#eeeeee"
},
dark: {
name: "Dark",
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext({
theme: themes.light,
updateTheme: () => {}
});
let prevTheme = undefined;
let prevStateArray = undefined;
function App() {
console.log("RE-RENDERING App...");
const stateArray = React.useState(themes.light);
console.log('stateArray', prevStateArray, stateArray, Object.is(prevStateArray, stateArray));
prevStateArray = stateArray;
const [theme, setTheme] = stateArray;
const memoState = React.useMemo(() => [theme, setTheme], [theme, setTheme]);
const [otherState, setOtherState] = React.useState(true);
function handleSetOtherState() {
console.log("SETTING OTHER STATE.....");
setOtherState(prevState => !prevState);
}
console.log("theme:", theme);
console.log("prevTheme:", prevTheme);
console.log(`Object.is(prevTheme, theme): ${Object.is(prevTheme, theme)}`);
prevTheme = theme;
return (
<ThemeContext.Provider value={memoState}>
<Toolbar />
<button onClick={handleSetOtherState}>Change OtherState</button>
</ThemeContext.Provider>
);
}
class Toolbar extends React.PureComponent {
render() {
console.log("RE-RENDERING Toolbar (DOES NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
return (
<div>
<ThemedButton />
</div>
);
}
}
function ThemedButton() {
console.log("RE-RENDERING ThemedButton (SHOULD NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
const themeContext = React.useContext(ThemeContext);
const [theme, setTheme] = themeContext;
console.log("themeContext:", themeContext);
console.log("theme.name:", theme.name);
console.log("setTheme:", setTheme);
function handleToggleTheme() {
console.log("SETTING THEME STATE.....");
setTheme(
prevState =>
themes.dark
);
}
return <button onClick={handleToggleTheme}>Click me: {theme.name}</button>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"/>
Комментарии:
1. Спасибо, очень полезно. Является ли это наиболее распространенным / типичным способом гарантировать, что потребители обновляют только измененный контекст (предполагая, что мы хотим предоставить потребителям возможность самим обновлять контекст)?
2. Краткое продолжение: выполняется ли функция внутри useMemo (первый аргумент) только при изменении зависимостей? Меня смутили документы, в которых говорится: «Помните, что функция, переданная useMemo, выполняется во время рендеринга» — Источник: reactjs.org/docs/hooks-reference.html#usememo .
3. Это зависит от значения, переданного
Context.Provider
. Вам не нужно использовать два отдельных контекста дляtheme
иsetTheme
,useMemo
потому чтоsetTheme
гарантируется, что при каждом рендеринге будет одна и та же функция. Обратный вызов, переданныйuseMemo
, не запускает каждый рендеринг. Но когда это происходит, это происходит во время рендеринга.4. Спасибо, это проясняет ситуацию. Кстати, я думаю, что придумал лучший способ сделать это. По сути, я просто помещаю свою функцию обновления состояния внутри самого состояния. Потому что объект состояния не изменится при повторном рендеринге. Таким образом, мне не нужен useMemo. Смотрите мой пример кода здесь: codesandbox.io/s/7jv99l7okj