为什么在 React 中,当 parent 组件 re-renders(children 未被 React.memo 包裹时,children 而不是 re-render?
Why, in React, do children not re-render when parent component re-renders(children are not wrapped by React.memo)?
在这篇文章React Hooks - Understanding Component Re-renders中了解到,当我们在parent组件中使用useContext
Hook时,只有children组件消耗上下文 会 re-render。
并且文章给出了context的两种消费方式。看看片段:
Efficient consumption of useContext\
import React from "react";
import ReactDOM from "react-dom";
import TickerComponent from "./tickerComponent";
import ThemedTickerComponent from "./themedTickerComponent";
import { ThemeContextProvider } from "./themeContextProvider";
import ThemeSelector from "./themeSelector";
import "./index.scss";
import logger from "./logger";
function App() {
logger.info("App", `Rendered`);
return (
<ThemeContextProvider>
<ThemeSelector />
<ThemedTickerComponent id={1} />
<TickerComponent id={2} />
</ThemeContextProvider>
);
}
import React, { useState } from "react";
const defaultContext = {
theme: "dark",
setTheme: () => {}
};
export const ThemeContext = React.createContext(defaultContext);
export const ThemeContextProvider = props => {
const setTheme = theme => {
setState({ ...state, theme: theme });
};
const initState = {
...defaultContext,
setTheme: setTheme
};
const [state, setState] = useState(initState);
return (
<ThemeContext.Provider value={state}>
{props.children}
</ThemeContext.Provider>
);
};
import React from "react";
import { useContext } from "react";
import { ThemeContext } from "./themeContextProvider";
function ThemeSelector() {
const { theme, setTheme } = useContext(ThemeContext);
const onThemeChanged = theme => {
logger.info("ThemeSelector", `Theme selection changed (${theme})`);
setTheme(theme);
};
return (
<div style={{ padding: "10px 5px 5px 5px" }}>
<label>
<input
type="radio"
value="dark"
checked={theme === "dark"}
onChange={() => onThemeChanged("dark")}
/>
Dark
</label>
<label>
<input
type="radio"
value="light"
checked={theme === "light"}
onChange={() => onThemeChanged("light")}
/>
Light
</label>
</div>
);
}
module.exports = ThemeSelector;
import React from "react";
import { ThemeContext } from "./themeContextProvider";
import TickerComponent from "./tickerComponent";
import { useContext } from "react";
function ThemedTickerComponent(props) {
const { theme } = useContext(ThemeContext);
return <TickerComponent id={props.id} theme={theme} />;
}
module.exports = ThemedTickerComponent;
import React from "react";
import { useState } from "react";
import stockPriceService from "./stockPriceService";
import "./tickerComponent.scss";
function TickerComponent(props) {
const [ticker, setTicker] = useState("AAPL");
const currentPrice = stockPriceService.fetchPricesForTicker(ticker);
const componentRef = React.createRef();
setTimeout(() => {
componentRef.current.classList.add("render");
setTimeout(() => {
componentRef.current.classList.remove("render");
}, 1000);
}, 50);
const onChange = event => {
setTicker(event.target.value);
};
return (
<>
<div className="theme-label">
{props.theme ? "(supports theme)" : "(only dark mode)"}
</div>
<div className={`ticker ${props.theme || ""}`} ref={componentRef}>
<select id="lang" onChange={onChange} value={ticker}>
<option value="">Select</option>
<option value="NFLX">NFLX</option>
<option value="FB">FB</option>
<option value="MSFT">MSFT</option>
<option value="AAPL">AAPL</option>
</select>
<div>
<div className="ticker-name">{ticker}</div>
<div className="ticker-price">{currentPrice}</div>
</div>
</div>
</>
);
}
module.exports = TickerComponent;
Inefficient consumption of useContext
import React from "react";
import ReactDOM from "react-dom";
import { useContext } from "react";
import TickerComponent from "./tickerComponent";
import ThemedTickerComponent from "./themedTickerComponent";
import { ThemeContextProvider } from "./themeContextProvider";
import { ThemeContext } from "./themeContextProvider";
function App() {
const { theme, setTheme } = useContext(ThemeContext);
const onThemeChanged = theme => {
setTheme(theme);
};
return (
<>
<div style={{ padding: "10px 5px 5px 5px" }}>
<label>
<input
type="radio"
value="dark"
checked={theme === "dark"}
onChange={() => onThemeChanged("dark")}
/>
Dark
</label>
<label>
<input
type="radio"
value="light"
checked={theme === "light"}
onChange={() => onThemeChanged("light")}
/>
Light
</label>
</div>
<ThemedTickerComponent id={1} />
<TickerComponent id={2} theme="" />
</>
);
}
在useContext低效消费例子中,child组件TickerComponent (2)
没有消费context re-rendered,因为 parent <App />
消耗了 context 和 re-rendered。但是在高效使用useContext的例子中,childTickerComponent(2)没有re-render即使是parent <ThemeContxtProvider>
re-render因为上下文的消耗。
了解到children没有React.memo会re-render当parentre-render,那么为什么在高效消费useContext 没有发生的例子?
你的问题是你正在考虑像
这样的代码
function ComponentToRender() {
const count = React.useRef(0)
React.useEffect(() => {
console.log('component rendered', count.current++)
})
return null
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h2>You clicked {count} times!</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ComponentToRender />
</div>
);
}
和
function ComponentToRender() {
const count = React.useRef(0)
React.useEffect(() => {
console.log('component rendered', count.current++)
})
return null
}
function Clicker({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<h2>You clicked {count} times!</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
{children}
</div>
);
}
function App() {
return (
<Clicker>
<ComponentToRender />
</Clicker>
);
}
等效。虽然它们做同样的事情,并且或多或少地以相同的方式表现,但第二个示例将只渲染 ComponentToRender
一次,即使在多次按下“增量”按钮之后也是如此。 (而第一个将 re-render 每次按下按钮。)
这个概念也适用于您的示例。您的“低效消耗”将从 App
触发 re-render,并强制刷新该组件的每个直接 child。 “有效消费”没有,因为事实并非如此。在我的简化示例中,ComponentToRender
实际上是由 App
渲染的,而不是 Clicker
。因此 Clicker
的状态变化不会影响 ComponentToRender
(刚刚作为 children 传递)
App
的另一种写法,在第二个例子中,是:
function App() {
const componentToRenderWithinApp = <ComponentToRender />
return (
<Clicker>
{componentToRenderWithinApp}
</Clicker>
);
}
这个相当于<Clicker><ComponentToRender /></Clicker>
在这篇文章React Hooks - Understanding Component Re-renders中了解到,当我们在parent组件中使用useContext
Hook时,只有children组件消耗上下文 会 re-render。
并且文章给出了context的两种消费方式。看看片段:
Efficient consumption of useContext\
import React from "react";
import ReactDOM from "react-dom";
import TickerComponent from "./tickerComponent";
import ThemedTickerComponent from "./themedTickerComponent";
import { ThemeContextProvider } from "./themeContextProvider";
import ThemeSelector from "./themeSelector";
import "./index.scss";
import logger from "./logger";
function App() {
logger.info("App", `Rendered`);
return (
<ThemeContextProvider>
<ThemeSelector />
<ThemedTickerComponent id={1} />
<TickerComponent id={2} />
</ThemeContextProvider>
);
}
import React, { useState } from "react";
const defaultContext = {
theme: "dark",
setTheme: () => {}
};
export const ThemeContext = React.createContext(defaultContext);
export const ThemeContextProvider = props => {
const setTheme = theme => {
setState({ ...state, theme: theme });
};
const initState = {
...defaultContext,
setTheme: setTheme
};
const [state, setState] = useState(initState);
return (
<ThemeContext.Provider value={state}>
{props.children}
</ThemeContext.Provider>
);
};
import React from "react";
import { useContext } from "react";
import { ThemeContext } from "./themeContextProvider";
function ThemeSelector() {
const { theme, setTheme } = useContext(ThemeContext);
const onThemeChanged = theme => {
logger.info("ThemeSelector", `Theme selection changed (${theme})`);
setTheme(theme);
};
return (
<div style={{ padding: "10px 5px 5px 5px" }}>
<label>
<input
type="radio"
value="dark"
checked={theme === "dark"}
onChange={() => onThemeChanged("dark")}
/>
Dark
</label>
<label>
<input
type="radio"
value="light"
checked={theme === "light"}
onChange={() => onThemeChanged("light")}
/>
Light
</label>
</div>
);
}
module.exports = ThemeSelector;
import React from "react";
import { ThemeContext } from "./themeContextProvider";
import TickerComponent from "./tickerComponent";
import { useContext } from "react";
function ThemedTickerComponent(props) {
const { theme } = useContext(ThemeContext);
return <TickerComponent id={props.id} theme={theme} />;
}
module.exports = ThemedTickerComponent;
import React from "react";
import { useState } from "react";
import stockPriceService from "./stockPriceService";
import "./tickerComponent.scss";
function TickerComponent(props) {
const [ticker, setTicker] = useState("AAPL");
const currentPrice = stockPriceService.fetchPricesForTicker(ticker);
const componentRef = React.createRef();
setTimeout(() => {
componentRef.current.classList.add("render");
setTimeout(() => {
componentRef.current.classList.remove("render");
}, 1000);
}, 50);
const onChange = event => {
setTicker(event.target.value);
};
return (
<>
<div className="theme-label">
{props.theme ? "(supports theme)" : "(only dark mode)"}
</div>
<div className={`ticker ${props.theme || ""}`} ref={componentRef}>
<select id="lang" onChange={onChange} value={ticker}>
<option value="">Select</option>
<option value="NFLX">NFLX</option>
<option value="FB">FB</option>
<option value="MSFT">MSFT</option>
<option value="AAPL">AAPL</option>
</select>
<div>
<div className="ticker-name">{ticker}</div>
<div className="ticker-price">{currentPrice}</div>
</div>
</div>
</>
);
}
module.exports = TickerComponent;
Inefficient consumption of useContext
import React from "react";
import ReactDOM from "react-dom";
import { useContext } from "react";
import TickerComponent from "./tickerComponent";
import ThemedTickerComponent from "./themedTickerComponent";
import { ThemeContextProvider } from "./themeContextProvider";
import { ThemeContext } from "./themeContextProvider";
function App() {
const { theme, setTheme } = useContext(ThemeContext);
const onThemeChanged = theme => {
setTheme(theme);
};
return (
<>
<div style={{ padding: "10px 5px 5px 5px" }}>
<label>
<input
type="radio"
value="dark"
checked={theme === "dark"}
onChange={() => onThemeChanged("dark")}
/>
Dark
</label>
<label>
<input
type="radio"
value="light"
checked={theme === "light"}
onChange={() => onThemeChanged("light")}
/>
Light
</label>
</div>
<ThemedTickerComponent id={1} />
<TickerComponent id={2} theme="" />
</>
);
}
在useContext低效消费例子中,child组件TickerComponent (2)
没有消费context re-rendered,因为 parent <App />
消耗了 context 和 re-rendered。但是在高效使用useContext的例子中,childTickerComponent(2)没有re-render即使是parent <ThemeContxtProvider>
re-render因为上下文的消耗。
了解到children没有React.memo会re-render当parentre-render,那么为什么在高效消费useContext 没有发生的例子?
你的问题是你正在考虑像
这样的代码function ComponentToRender() {
const count = React.useRef(0)
React.useEffect(() => {
console.log('component rendered', count.current++)
})
return null
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h2>You clicked {count} times!</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ComponentToRender />
</div>
);
}
和
function ComponentToRender() {
const count = React.useRef(0)
React.useEffect(() => {
console.log('component rendered', count.current++)
})
return null
}
function Clicker({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<h2>You clicked {count} times!</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
{children}
</div>
);
}
function App() {
return (
<Clicker>
<ComponentToRender />
</Clicker>
);
}
等效。虽然它们做同样的事情,并且或多或少地以相同的方式表现,但第二个示例将只渲染 ComponentToRender
一次,即使在多次按下“增量”按钮之后也是如此。 (而第一个将 re-render 每次按下按钮。)
这个概念也适用于您的示例。您的“低效消耗”将从 App
触发 re-render,并强制刷新该组件的每个直接 child。 “有效消费”没有,因为事实并非如此。在我的简化示例中,ComponentToRender
实际上是由 App
渲染的,而不是 Clicker
。因此 Clicker
的状态变化不会影响 ComponentToRender
(刚刚作为 children 传递)
App
的另一种写法,在第二个例子中,是:
function App() {
const componentToRenderWithinApp = <ComponentToRender />
return (
<Clicker>
{componentToRenderWithinApp}
</Clicker>
);
}
这个相当于<Clicker><ComponentToRender /></Clicker>