React + Material UI - 在切换 parent 主题时防止 child 树重新挂载的最佳方法
React + Material UI - Best way to prevent child tree from remount when toggling parent theme
背景
我想遵循 Material UI 切换 UI 的 dark/light 模式主题的实现。 Link.
我已将其实现封装到一个自定义挂钩中,调用时该挂钩具有 returns theme-related 属性。这是我的 App()
水平。
import { createContext, useState, useMemo } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
export default function useTheme() {
const ColorModeContext = createContext({ toggleColorMode: () => {}, mode: null})
const [ mode, setMode ] = useState('light')
const colorMode = useMemo(
() => ({
toggleColorMode : () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'))
},
})
, [])
// recreate theme everytime mode changes
const appTheme = useMemo( () => (createTheme({
palette: {
mode: mode,
primary: ...
},
})), [mode])
return {
ColorModeContext,
colorMode,
ThemeProvider,
appTheme
}
}
在 App()
级别,我有 returns 以下应用程序组件:
...
const {
ColorModeContext,
colorMode,
ThemeProvider,
appTheme
} = useTheme()
// another custom hook to return auth related properties
const {
AuthContext,
authed,
...
} = useAuth()
return (
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={appTheme}>
<CssBaseline />
<AuthContext.Provider value=...>
<Box>
<Router>
<Header
ColorModeContext={ColorModeContext}
theme={appTheme}
authed={authed}
...
/>
<Routes>
<Route
path="/"
element={
<Signin AuthContext={AuthContext} />
}
/>
<Route
path="/create"
element={
<RequiredAuth authed={authed}>
<Create />
</RequiredAuth>
}
/>
<Route
path="/query"
element={
<RequiredAuth authed={authed}>
<Query />
</RequiredAuth>
}
/>
</Routes>
...
</Router>
</Box>
</AuthContext.Provider>
</ThemeProvider>
</ColorModeContext.Provider>
)
最后,在我的 header 组件中(作为上下文消费者,在 header 中公开切换回调函数)
...
const { ColorModeContext, authed, ... } = props;
return (
<ColorModeContext.Consumer>
{
({toggleColorMode}) => (
<Box>
<AppBar position="static" enableColorOnDark>
<Toolbar>
...
<IconButton
size="small"
onClick={toggleColorMode}
color="inherit"
>
{
theme.palette.mode === 'dark' ?
<LightModeIcon /> : <Brightness3Icon />
}
</IconButton>
...
</Toolbar>
</AppBar>
</Box>
)
}
</ColorModeContext.Consumer>
)
问题
切换主题有效,但是,如果我在我的组件 <Create />
或 <Query />
中切换主题,整个组件树(创建或查询)将重新挂载,并且所有状态在组件内刷新为初始状态。
基本上,如果我在我的 <Create />
组件内,并且我正在填写创建表单,该表单的值由 useState
维护,只要我切换主题,所有的值都重置为它们的初始值(传递到 useState
挂钩)
问题
有没有办法防止这种重新挂载的发生?我知道这不仅仅是重新渲染组件,因为状态已重新初始化。如果这不是重新渲染问题,那么 React.memo
可以在这里工作吗?如果没有,什么是在 child 组件内切换主题(parent 组件级别的上下文)而无需重新安装组件的最佳方法。
添加一个codesandboxlink。它应该包括我遇到的问题的一个简单示例。当我切换 parent 的主题模式时,child 组件重新安装(由 Child 的 useEffect
记录)。
useTheme
挂钩应该 return 提供的上下文值,而不是上下文本身。这是重新创建上下文提供程序并重新安装子项。
示例:
使用主题
import { createContext, useState, useMemo, useContext } from "react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
const ColorModeContext = createContext({
toggleColorMode: () => {},
mode: null
});
const ColorModeContextProvider = ({ children }) => {
const [mode, setMode] = useState("light");
const toggleColorMode = () => {
setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
};
// recreate theme everytime mode changes
const appTheme = useMemo(
() =>
createTheme({
palette: {
mode
}
}),
[mode]
);
return (
<ColorModeContext.Provider
value={{
toggleColorMode,
mode
}}
>
<ThemeProvider theme={appTheme}>{children}</ThemeProvider>
</ColorModeContext.Provider>
);
};
export const useTheme = () => useContext(ColorModeContext);
export default ColorModeContextProvider;
应用程序
导入新的 ColorModeContextProvider
provider 组件以提供主题和颜色模式上下文,useTheme
挂钩将在 Header
组件中使用以访问 toggleColorMode
回调。
import { useState, useEffect } from "react";
import { Container, CssBaseline } from "@mui/material";
import ColorModeContextProvider, { useTheme } from "./useTheme";
import useAuth from "./useAuth";
function App() {
const { AuthContext, authed, user, login, logout } = useAuth();
return (
<ColorModeContextProvider>
<CssBaseline />
<AuthContext.Provider value={{ authed, user, login, logout }}>
<Container maxWidth="lg">
<Header />
</Container>
<Child />
</AuthContext.Provider>
</ColorModeContextProvider>
);
}
function Header() {
const { toggleColorMode } = useTheme();
return <button onClick={toggleColorMode}>ToggleTheme</button>;
}
function Child() {
const [counter, setCounter] = useState(0);
useEffect(() => {
return () => {
console.log("Child component unmounting");
};
}, []);
return (
<>
<button
onClick={() => {
setCounter((count) => count + 1);
}}
>
Add
</button>
<div>{counter}</div>
</>
);
}
export default App;
useAuth
挂钩也需要对其应用类似的重构。
背景
我想遵循 Material UI 切换 UI 的 dark/light 模式主题的实现。 Link.
我已将其实现封装到一个自定义挂钩中,调用时该挂钩具有 returns theme-related 属性。这是我的 App()
水平。
import { createContext, useState, useMemo } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
export default function useTheme() {
const ColorModeContext = createContext({ toggleColorMode: () => {}, mode: null})
const [ mode, setMode ] = useState('light')
const colorMode = useMemo(
() => ({
toggleColorMode : () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'))
},
})
, [])
// recreate theme everytime mode changes
const appTheme = useMemo( () => (createTheme({
palette: {
mode: mode,
primary: ...
},
})), [mode])
return {
ColorModeContext,
colorMode,
ThemeProvider,
appTheme
}
}
在 App()
级别,我有 returns 以下应用程序组件:
...
const {
ColorModeContext,
colorMode,
ThemeProvider,
appTheme
} = useTheme()
// another custom hook to return auth related properties
const {
AuthContext,
authed,
...
} = useAuth()
return (
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={appTheme}>
<CssBaseline />
<AuthContext.Provider value=...>
<Box>
<Router>
<Header
ColorModeContext={ColorModeContext}
theme={appTheme}
authed={authed}
...
/>
<Routes>
<Route
path="/"
element={
<Signin AuthContext={AuthContext} />
}
/>
<Route
path="/create"
element={
<RequiredAuth authed={authed}>
<Create />
</RequiredAuth>
}
/>
<Route
path="/query"
element={
<RequiredAuth authed={authed}>
<Query />
</RequiredAuth>
}
/>
</Routes>
...
</Router>
</Box>
</AuthContext.Provider>
</ThemeProvider>
</ColorModeContext.Provider>
)
最后,在我的 header 组件中(作为上下文消费者,在 header 中公开切换回调函数)
...
const { ColorModeContext, authed, ... } = props;
return (
<ColorModeContext.Consumer>
{
({toggleColorMode}) => (
<Box>
<AppBar position="static" enableColorOnDark>
<Toolbar>
...
<IconButton
size="small"
onClick={toggleColorMode}
color="inherit"
>
{
theme.palette.mode === 'dark' ?
<LightModeIcon /> : <Brightness3Icon />
}
</IconButton>
...
</Toolbar>
</AppBar>
</Box>
)
}
</ColorModeContext.Consumer>
)
问题
切换主题有效,但是,如果我在我的组件 <Create />
或 <Query />
中切换主题,整个组件树(创建或查询)将重新挂载,并且所有状态在组件内刷新为初始状态。
基本上,如果我在我的 <Create />
组件内,并且我正在填写创建表单,该表单的值由 useState
维护,只要我切换主题,所有的值都重置为它们的初始值(传递到 useState
挂钩)
问题
有没有办法防止这种重新挂载的发生?我知道这不仅仅是重新渲染组件,因为状态已重新初始化。如果这不是重新渲染问题,那么 React.memo
可以在这里工作吗?如果没有,什么是在 child 组件内切换主题(parent 组件级别的上下文)而无需重新安装组件的最佳方法。
添加一个codesandboxlink。它应该包括我遇到的问题的一个简单示例。当我切换 parent 的主题模式时,child 组件重新安装(由 Child 的 useEffect
记录)。
useTheme
挂钩应该 return 提供的上下文值,而不是上下文本身。这是重新创建上下文提供程序并重新安装子项。
示例:
使用主题
import { createContext, useState, useMemo, useContext } from "react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
const ColorModeContext = createContext({
toggleColorMode: () => {},
mode: null
});
const ColorModeContextProvider = ({ children }) => {
const [mode, setMode] = useState("light");
const toggleColorMode = () => {
setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
};
// recreate theme everytime mode changes
const appTheme = useMemo(
() =>
createTheme({
palette: {
mode
}
}),
[mode]
);
return (
<ColorModeContext.Provider
value={{
toggleColorMode,
mode
}}
>
<ThemeProvider theme={appTheme}>{children}</ThemeProvider>
</ColorModeContext.Provider>
);
};
export const useTheme = () => useContext(ColorModeContext);
export default ColorModeContextProvider;
应用程序
导入新的 ColorModeContextProvider
provider 组件以提供主题和颜色模式上下文,useTheme
挂钩将在 Header
组件中使用以访问 toggleColorMode
回调。
import { useState, useEffect } from "react";
import { Container, CssBaseline } from "@mui/material";
import ColorModeContextProvider, { useTheme } from "./useTheme";
import useAuth from "./useAuth";
function App() {
const { AuthContext, authed, user, login, logout } = useAuth();
return (
<ColorModeContextProvider>
<CssBaseline />
<AuthContext.Provider value={{ authed, user, login, logout }}>
<Container maxWidth="lg">
<Header />
</Container>
<Child />
</AuthContext.Provider>
</ColorModeContextProvider>
);
}
function Header() {
const { toggleColorMode } = useTheme();
return <button onClick={toggleColorMode}>ToggleTheme</button>;
}
function Child() {
const [counter, setCounter] = useState(0);
useEffect(() => {
return () => {
console.log("Child component unmounting");
};
}, []);
return (
<>
<button
onClick={() => {
setCounter((count) => count + 1);
}}
>
Add
</button>
<div>{counter}</div>
</>
);
}
export default App;
useAuth
挂钩也需要对其应用类似的重构。