以下是个人收集总结的一些使用 react 的小技巧第二篇。
五. 高效的状态管理
27. 永远不要为可以从其他 state 或 props 派生的值创建新的 state
state 越多 = 麻烦越多。
每个 state 都可能触发重新渲染,并使重置 state 变得麻烦。
因此,如果可以从 state 或 props 中派生出值,则跳过添加新的 state。
不好的做法:filteredUsers 不需要处于 state 中。
const FilterUserComponent = ({ users }) => {
const [filters, setFilters] = useState([]);
// 创建了新的state
const [filteredUsers, setFilteredUsers] = useState([]);
const filterUsersMethod = (filters, users) => {
// 过滤逻辑方法
};
useEffect(() => {
setFilteredUsers(filterUsersMethod(filters, users));
}, [users, filters]);
return (
<Card>
<Filters filters={filters} onChangeFilters={setFilters} />
{filteredUsers.length > 0 && <UserList users={filteredUsers} />}
</Card>
);
};
推荐做法: filteredUsers 由 users 和 filters 决定。
const FilterUserComponent = ({ users }) => {
const [filters, setFilters] = useState([]);
const filterUsersMethod = (filters, users) => {
// 过滤逻辑方法
};
const filteredUsers = filterUsersMethod(filters, users);
return (
<Card>
<Filters filters={filters} onChangeFilters={setFilters} />
{filteredUsers.length > 0 && <UserList users={filteredUsers} />}
</Card>
);
};
28. 将 state 创建在仅需要更新的组件内部,以减少组件的重新渲染
每当组件内部的状态发生变化时,React 都会重新渲染该组件及其所有子组件(包裹在 memo 中的子组件除外)。
即使这些子组件不使用已更改的状态,也会发生这种情况。为了最大限度地减少重新渲染,请尽可能将状态移到组件树的下方。
不好的做法: 当 type 发生改变时,会使不依赖 type 状态的 LeftList 和 RightList 组件也触发重新渲染。
const App = () => {
const [type, setType] = useState("");
return (
<Container>
<LeftList />
<Main type={type} setType={setType} />
<RightList />
</Container>
);
};
const mainBtnList = [
{
label: "首页",
value: "home",
},
{
label: "详情页",
value: "detail",
},
];
const Main = ({ type, setType }) => {
return (
<>
{mainBtnList.map((item, index) => (
<Button
className={`${type.value === type ? "active" : ""}`}
key={`${item.value}-${index}`}
onClick={() => setType(item.value)}
>
{item.label}
</Button>
))}
</>
);
};
推荐做法: 将状态耦合到 Main 组件内部,仅影响 Main 组件的重新渲染。
const App = () => {
return (
<Container>
<LeftList />
<Main />
<RightList />
</Container>
);
};
const mainBtnList = [
{
label: "首页",
value: "home",
},
{
label: "详情页",
value: "detail",
},
];
const Main = () => {
const [type, setType] = useState("");
return (
<>
{mainBtnList.map((item, index) => (
<Button
className={`${type.value === type ? "active" : ""}`}
key={`${item.value}-${index}`}
onClick={() => setType(item.value)}
>
{item.label}
</Button>
))}
</>
);
};
29. 定义需要明确初始状态和当前状态的区别
不好的做法: 不清楚 userInfo 只是初始值,这可能会导致状态管理的混乱或错误。
const UserInfo = ({ userInfo }) => {
const [userInfo, setUserInfo] = useState(userInfo);
return (
<Card>
<Title>当前用户: {userInfo?.name}</Title>
<UserInfoDetail detail={userInfo?.detail} />
</Card>
);
};
推荐做法: 命名可以清楚地表明什么是初始状态,什么是当前状态。
const UserInfo = ({ initialUserInfo }) => {
const [userInfo, setUserInfo] = useState(initialUserInfo);
return (
<Card>
<Title>当前用户: {userInfo?.name}</Title>
<UserInfoDetail detail={userInfo?.detail} />
</Card>
);
};
30. 根据之前的状态更新状态,尤其是在使用 useCallback 进行缓存时
React 允许你将更新函数从 useState 传递给 set 函数。
此更新函数使用当前状态来计算下一个状态。
每当需要根据之前状态更新状态时,都可以使用此行为,尤其是在使用 useCallback 包装的函数内部。事实上,这种方法可以避免将状态作为钩子依赖项之一。
不好的做法: 无论什么时候,当 todoList 变化的时候,onHandleAddTodo 和 onHandleRemoveTodo 都会跟着改变。
const App = () => {
const [todoList, setTodoList] = useState([]);
const onHandleAddTodo = useCallback(
(todo) => {
setTodoList([...todoList, todo]);
},
[todoList]
);
const onHandleRemoveTodo = useCallback(
(todo) => {
setTodoList([...todoList].filter((item) => item.id !== todo.id));
},
[todoList]
);
return (
<div className="App">
<TodoInput onAddTodo={onHandleAddTodo} />
<TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} />
</div>
);
};
推荐做法: 即使 todoList 发生变化,onHandleAddTodo 和 onHandleRemoveTodo 仍然保持不变。
const App = () => {
const [todoList, setTodoList] = useState([]);
const onHandleAddTodo = useCallback((todo) => {
setTodoList((prevTodoList) => [...prevTodoList, todo]);
}, []);
const onHandleRemoveTodo = useCallback((todo) => {
setTodoList((prevTodoList) =>
[...prevTodoList].filter((item) => item.id !== todo.id)
);
}, []);
return (
<div className="App">
<TodoInput onAddTodo={onHandleAddTodo} />
<TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} />
</div>
);
};
31. 使用 useState 中的函数进行延迟初始化并提高性能,因为它们只被调用一次。
在 useState 中使用函数可确保初始状态仅计算一次。
这可以提高性能,尤其是当初始状态来自“昂贵”操作(例如从本地存储读取)时。
不好的做法:每次组件渲染时,我们都会从本地存储读取主题。
const THEME_LOCAL_STORAGE_KEY = "page_theme_key";
const Theme = ({ theme, onChangeTheme }) => {
// ....
};
const App = ({ children }) => {
const [theme, setTheme] = useState(
localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const onChangeTheme = (theme: string) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div className={`app${theme ? ` ${theme}` : ""}`}>
<Theme onChange={onChangeTheme} theme={theme} />
<div>{children}</div>
</div>
);
};
推荐做法: 当组件挂载时,我们仅只会读取本地存储一次。
// ...
const App = ({ children }) => {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const onChangeTheme = (theme: string) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div className={`app${theme ? ` ${theme}` : ""}`}>
<Theme onChange={onChangeTheme} theme={theme} />
<div>{children}</div>
</div>
);
};
32. 使用 React 上下文来处理广泛需要的静态状态,以防止 prop 钻取
每当我有一些数据时,我都会使用 React 上下文:
- 在多个地方都需要(例如,主题、当前用户等)
- 主要是静态或只读的(即,用户不能/不会经常更改数据)
- 这种方法有助于避免 prop 钻取(即,通过组件层次结构的多个层传递数据或状态)。
来看一个示例的部分代码:
context.ts
// UserInfo接口来自测试数据
export const userInfoContext = createContext<string | UserInfoData>("loading");
export const useUserInfo = <T extends UserInfoData>() => {
const value = useContext(userInfoContext);
if (value == null) {
throw new Error("Make sure to wrap the userInfoContext inside provider");
}
return value as T;
};
App.tsx
function App() {
const [userInfoData, setUserInfoData] = useState<UserInfoData | string>(
"loading"
);
useEffect(() => {
getCurrentUser().then(setUserInfoData);
}, []);
if (userInfoData === "loading") {
return <Loading />;
}
return (
<div className="app">
<userInfoContext.Provider value={userInfoData}>
<Header />
<Sidebar />
<Main />
</userInfoContext.Provider>
</div>
);
}
header.tsx:
const Header: React.FC<HeaderProps> = (props) => {
// 使用context
const userInfo = useUserInfo();
return (
<header className="header" {...props}>
欢迎回来{userInfo?.name}
</header>
);
};
main.tsx:
const Main: React.FC<MainProps> = ({ title }) => {
const { posts } = useUserInfo();
return (
<div className="main">
<h2 className="title">{title}</h2>
<ul className="list">
{posts?.map((post, index) => (
<li className="list-item" key={`${post.id}-${index}`}>
{post.title}
</li>
))}
</ul>
</div>
);
};
完整示例代码前往这里查看。
33. React Context:将 react 上下文分为经常变化的部分和不经常变化的部分,以提高应用程序性能
React 上下文的一个挑战是,只要上下文数据发生变化,所有使用该上下文的组件都会重新渲染,即使它们不使用发生变化的上下文部分。
解决方案是什么?使用单独的上下文。
在下面的示例中,我们创建了两个上下文:一个用于操作(常量),另一个用于状态(可以更改)。
export interface TodosInfoItem {
id?: string;
title?: string;
completed?: boolean;
}
export interface TodosInfo {
search?: string;
todos: TodosInfoItem[];
}
export const todosStateContext = createContext<TodosInfo>(void 0);
export const todosActionContext = createContext<Dispatch<ReducerActionParams>>(
void 0
);
export interface ReducerActionParams extends TodosInfoItem {
type?: string;
value?: string;
}
export const getTodosReducer = (
state: TodosInfo,
action: ReducerActionParams
) => {
switch (action.type) {
case TodosActionType.ADD_TODO:
return {
...state,
todos: [
...state.todos,
{
id: crypto.randomUUID(),
title: action.title,
completed: false,
},
],
};
case TodosActionType.REMOVE_TODO:
return {
...state,
todos: [...state.todos].filter((item) => item.id !== action.id),
};
case TodosActionType.TOGGLE_TODO_STATUS:
return {
...state,
todos: [...state.todos].map((item) =>
item.id === action.id ? { ...item, completed: !item.completed } : item
),
};
case TodosActionType.SET_SEARCH_TERM:
return {
...state,
search: action.value,
};
default:
return state;
}
};
完整示例代码前往这里查看。
34. React Context:当值计算不直接时,引入 Provider 组件
不好的做法:App 内部有太多逻辑来管理 theme context。
const THEME_LOCAL_STORAGE_KEY = "current-project-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
theme: DEFAULT_THEME,
setTheme: () => null,
});
const App = () => {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
);
useEffect(() => {
if (theme !== "system") {
updateRootElementTheme(theme);
return;
}
// 我们需要根据系统主题获取要应用的主题类
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
updateRootElementTheme(systemTheme);
// 然后观察系统主题的变化并相应地更新根元素
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (event) => {
updateRootElementTheme(event.matches ? "dark" : "light");
};
darkThemeMq.addEventListener("change", listener);
return () => darkThemeMq.removeEventListener("change", listener);
}, [theme]);
const themeContextValue = {
theme,
setTheme: (theme) => {
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
setTheme(theme);
},
};
const [selectedUserId, setSelectedUserId] = useState(undefined);
const onUserSelect = (id) => {
// 待做:一些逻辑
setSelectedUserId(id);
};
const users = useSWR("/api/users", fetcher);
return (
<div className="App">
<ThemeContext.Provider value={themeContextValue}>
<UserList
users={users}
onUserSelect={onUserSelect}
selectedUserId={selectedUserId}
/>
</ThemeContext.Provider>
</div>
);
};
推荐:主题 context 相关的逻辑封装在 ThemeProvider 中。
const THEME_LOCAL_STORAGE_KEY = "current-project-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
theme: DEFAULT_THEME,
setTheme: () => null,
});
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
);
useEffect(() => {
if (theme !== "system") {
updateRootElementTheme(theme);
return;
}
// 我们需要根据系统主题获取要应用的主题类
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
updateRootElementTheme(systemTheme);
// 然后观察系统主题的变化并相应地更新根元素
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (event) => {
updateRootElementTheme(event.matches ? "dark" : "light");
};
darkThemeMq.addEventListener("change", listener);
return () => darkThemeMq.removeEventListener("change", listener);
}, [theme]);
const themeContextValue = {
theme,
setTheme: (theme) => {
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
setTheme(theme);
},
};
return (
<div className="App">
<ThemeContext.Provider value={themeContextValue}>
{children}
</ThemeContext.Provider>
</div>
);
};
const App = () => {
const [selectedUserId, setSelectedUserId] = useState(undefined);
const onUserSelect = (id) => {
// 待做:一些逻辑
setSelectedUserId(id);
};
const users = useSWR("/api/users", fetcher);
return (
<div className="App">
<ThemeProvider>
<UserList
users={users}
onUserSelect={onUserSelect}
selectedUserId={selectedUserId}
/>
</ThemeProvider>
</div>
);
};
35. 考虑使用 useReducer hook 作为轻量级状态管理解决方案
每当我的状态或复杂状态中的值太多并且不想依赖外部库时,我都会使用 useReducer。
当与上下文结合使用时,它对于更广泛的状态管理需求特别有效。
示例:这里。
36. 使用 useImmer 或 useImmerReducer 简化状态更新
使用 useState 和 useReducer 等钩子时,状态必须是不可变的(即,所有更改都需要创建新状态,而不是修改当前状态)。
这通常很难实现。
这就是 useImmer 和 useImmerReducer 提供更简单的替代方案的地方。它们允许你编写自动转换为不可变更新的“可变”代码。
不好的做法: 我们必须小心确保我们正在创建一个新的状态对象。
export const App = () => {
const [{ email, password }, setState] = useState({
email: "",
password: "",
});
const onEmailChange = (event) => {
setState((prevState) => ({ ...prevState, email: event.target.value }));
};
const onPasswordChange = (event) => {
setState((prevState) => ({ ...prevState, password: event.target.value }));
};
return (
<div className="App">
<h1>欢迎登陆</h1>
<div class="form-item">
<label>邮箱号: </label>
<input type="email" value={email} onChange={onEmailChange} />
</div>
<div className="form-item">
<label>密码:</label>
<input type="password" value={password} onChange={onPasswordChange} />
</div>
</div>
);
};
推荐做法: 更直接一点,我们可以直接修改 draftState。
import { useImmer } from "use-immer";
export const App = () => {
const [{ email, password }, setState] = useImmer({
email: "",
password: "",
});
const onEmailChange = (event) => {
setState((draftState) => {
draftState.email = event.target.value;
});
};
const onPasswordChange = (event) => {
setState((draftState) => {
draftState.password = event.target.value;
});
};
// 剩余代码
};
37. 使用 Redux(或其他状态管理解决方案)来跨多个组件访问复杂的客户端状态
每当出现以下情况时,我都会求助于 Redux:
我有一个复杂的 FE 应用程序,其中包含大量共享的客户端状态(例如,仪表板应用程序)
- 我希望用户能够回到过去并恢复更改。
- 我不希望我的组件像使用 React 上下文那样不必要地重新渲染。
- 我有太多开始难以控制的上下文。
为了获得简化的体验,我建议使用 redux-tooltkit。
💡 注意:你还可以考虑 Redux 的其他替代方案,例如 Zustand 或 Recoil。
38. Redux:使用 Redux DevTools 调试你的状态
Redux DevTools 浏览器扩展是调试 Redux 项目的有用工具。
它允许你实时可视化你的状态和操作,在刷新时保持状态持久性等等。
要了解它的用途,请观看这个精彩的视频。
六. React 代码优化
39. 使用 memo 防止不必要的重新渲染
当处理渲染成本高昂且父组件频繁更新的组件时,将它们包装在 memo 中可能会改变渲染规则。
memo 确保组件仅在其 props 发生变化时重新渲染,而不仅仅是因为其父组件重新渲染。
在以下示例中,我通过 useGetInfoData 从服务器获取一些数据。如果数据没有变化,将 UserInfoList 包装在 memo 中将阻止它在数据的其他部分更新时重新渲染。
export const App = () => {
const { currentUserInfo, users } = useGetInfoData();
return (
<div className="App">
<h1>信息面板</h1>
<CurrentUserInfo data={currentUserInfo} />
<UserInfoList users={users} />
</div>
);
};
const UserInfoList = memo(({ users }) => {
// 剩余实现
});
一旦 React 编译器变得稳定,这个小技巧可能就不再有用了。
40. 用 memo 指定一个相等函数来指示 React 如何比较 props。
默认情况下,memo 使用Object.is将每个 prop 与其先前的值进行比较。
但是,对于更复杂或特定的场景,指定自定义相等函数可能比默认比较或重新渲染更有效。
示例如下:
const UserList = memo(
({ users }) => {
return <div>{JSON.stringify(users)}</div>;
},
(prevProps, nextProps) => {
// 仅当最后一个用户或列表大小发生变化时才重新渲染
const prevLastUser = prevProps.users[prevProps.users.length - 1];
const nextLastUser = nextProps.users[nextProps.users.length - 1];
return (
prevLastUser.id === nextLastUser.id &&
prevProps.users.length === nextProps.users.length
);
}
);
41.声明缓存组件时,优先使用命名函数而不是箭头函数
定义缓存组件时,使用命名函数而不是箭头函数可以提高 React DevTools 中的清晰度。
箭头函数通常会导致像 _c2
这样的通用名称,这会使调试和分析更加困难。
不好的做法:对缓存组件使用箭头函数会导致 React DevTools 中的名称信息量较少。
const UserInfoList = memo(({ users }) => {
// 剩余实现逻辑
});
推荐做法: 该组件的名称将在 DevTools 中可见。
const UserInfoList = memo(function UserInfoList({ users }) {
// 剩余实现逻辑
});
42. 使用 useMemo 缓存昂贵的计算或保留引用
我通常会使用 useMemo:
- 当我有昂贵的计算,不应该在每次渲染时重复这些计算时。
- 如果计算值是非原始值,用作 useEffect 等钩子中的依赖项。
- 计算出的非原始值将作为 prop 传递给包裹在 memo 中的组件;否则,这将破坏缓存,因为 React 使用 Object.is 来检测 props 是否发生变化。
不好的做法:UserInfoList 的 memo 不会阻止重新渲染,因为每次渲染时都会重新创建样式。
export const UserInfo = () => {
const { profileInfo, users, baseStyles } = useGetUserInfoData();
// 每次重新渲染我们都会得到一个样式对象
const styles = { ...baseStyles, margin: 10 };
return (
<div className="App">
<h1>用户页</h1>
<Profile data={profileInfo} />
<UserInfoList users={users} styles={styles} />
</div>
);
};
const UserInfoList = memo(function UserInfoListFn({ users, styles }) {
/// 剩余实现
});
推荐做法: useMemo 的使用确保只有当 baseStyles 发生变化时,styles 才会发生变化,从而使 memo 能够有效防止不必要的重新渲染。
export const UserInfo = () => {
const { profileInfo, users, baseStyles } = useGetUserInfoData();
// 每次重新渲染我们都会得到一个样式对象
const styles = useMemo(() => ({ ...baseStyles, margin: 10 }), [baseStyles]);
return (
<div className="App">
<h1>用户页</h1>
<Profile data={profileInfo} />
<UserInfoList users={users} styles={styles} />
</div>
);
};
const UserInfoList = memo(function UserInfoListFn({ users, styles }) {
/// 剩余实现
});
43. 使用 useCallback 缓存函数
useCallback 与 useMemo 类似,但专为缓存函数而设计。
不好的做法:每当 theme 发生变化时,handleThemeChange 都会被调用两次,并且我们会将日志推送到服务器两次。
const useTheme = () => {
const [theme, setTheme] = useState("light");
// 每次渲染`handleThemeChange`都会改变
// 因此,每次渲染后都会触发该效果
const handleThemeChange = (newTheme) => {
sendLog(["Theme changed"], {
context: {
theme: newTheme,
},
});
setTheme(newTheme);
};
useEffect(() => {
const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
handleThemeChange(dqMediaQuery.matches ? "dark" : "light");
const listener = (event) => {
handleThemeChange(event.matches ? "dark" : "light");
};
dqMediaQuery.addEventListener("change", listener);
return () => {
dqMediaQuery.removeEventListener("change", listener);
};
}, [handleThemeChange]);
return theme;
};
推荐做法:将 handleThemeChange 包装在 useCallback 中可确保仅在必要时重新创建它,从而减少不必要的执行。
const handleThemeChange = useCallback((newTheme) => {
sendLog(["Theme changed"], {
context: {
theme: newTheme,
},
});
setTheme(newTheme);
}, []);
44. 缓存回调函数或使用程序钩子返回的值以避免性能问题
当你创建自定义钩子与他人共享时,记住返回的值和函数至关重要。
这种做法可以使你的钩子更高效,并防止任何使用它的人出现不必要的性能问题。
不好的做法:loadData 没有被缓存并产生了性能问题。
const useLoadData = (fetchData) => {
const [result, setResult] = useState({
type: "pending",
});
const loadData = async () => {
setResult({ type: "loading" });
try {
const data = await fetchData();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
};
return { result, loadData };
};
推荐做法: 我们缓存所有内容,因此不会出现意外的性能问题。
const useLoadData = (fetchData) => {
const [result, setResult] = useState({
type: "pending",
});
// 包裹在 `useRef` 中并使用 `ref` 值,这样函数就不会改变
const fetchDataRef = useRef(fetchData);
useEffect(() => {
fetchDataRef.current = fetchData;
}, [fetchData]);
// 包裹在 `useCallback` 中并使用 `ref` 值,这样函数就不会改变
const loadData = useCallback(async () => {
setResult({ type: "loading" });
try {
const data = await fetchDataRef.current();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
}, []);
// 使用useMemo缓存值
return useMemo(() => ({ result, loadData }), [result, loadData]);
};
45. 利用懒加载和 Suspense 让你的应用加载更快
构建应用时,请考虑对以下代码使用懒加载和 Suspense:
- 加载成本高。
- 仅与某些用户相关(如高级功能)。
- 对于初始用户交互而言并非立即需要。
在下面的示例,Slider 资源(JS + CSS)仅在你单击卡片后加载。
//...
const LazyLoadedSlider = lazy(() => import("./Slider"));
//...
const App = () => {
// ....
return (
<div className="container">
{/* .... */}
{selectedUser != null && (
<Suspense fallback={<div>Loading...</div>}>
<LazyLoadedSlider
avatar={selectedUser.avatar}
name={selectedUser.name}
address={selectedUser.address}
onClose={closeSlider}
/>
</Suspense>
)}
</div>
);
};
46. 限制网络以模拟慢速网络
你知道可以直接在 Chrome 中模拟慢速互联网连接吗?
这在以下情况下尤其有用:
- 用户报告加载时间缓慢,而你无法在更快的网络上复制。
- 你正在实施懒加载,并希望观察文件在较慢条件下的加载方式,以确保适当的加载状态。
47. 使用 react-window 或 react-virtuoso 高效渲染列表
切勿一次性渲染一长串项目,例如聊天消息、日志或无限列表。
这样做可能会导致浏览器卡死崩溃。相反,可以使用虚拟化列表,这意味着仅渲染可能对用户可见的项目子集。
react-window、react-virtuoso 或 @tanstack/react-virtual 等库就是为此目的而设计的。
不好的做法:NonVirtualList 会同时呈现所有 50,000 条日志行,即使它们不可见。
const NonVirtualList = ({ items }: { items: LogLineItem[] }) => {
return (
<div style={{ height: "100%" }}>
{items?.map((log, index) => (
<div
key={log.id}
style={{
padding: "5px",
borderBottom:
index === items.length - 1 ? "none" : "1px solid #535455",
}}
>
<LogLine log={log} index={index} />
</div>
))}
</div>
);
};
推荐做法: VirtualList
仅渲染可能可见的项目。
const VirtualList = ({ items }: { items: LogLineItem[] }) => {
return (
<Virtuoso
style={{ height: "100%" }}
data={items}
itemContent={(index, log) => (
<div
key={log.id}
style={{
padding: "5px",
borderBottom:
index === items.length - 1 ? "none" : "1px solid #535455",
}}
>
<LogLine log={log} index={index} />
</div>
)}
/>
);
};
你可以在这个完整的示例中在两个选项之间切换,并注意使用 NonVirtualList
时应用程序的性能有多糟糕。
七. 调试 react 代码
48. 在将组件部署到生产环境之前,使用 StrictMode 捕获组件中的错误
使用严格模式是一种在开发过程中检测应用程序中潜在问题的主动方法。它有助于识别以下问题:
- 效果中的清理不完整,例如忘记释放资源。
- React 组件中的杂质,确保它们在给定相同输入(props、state 和 context)的情况下返回一致的 JSX。
下面的示例显示了一个错误,因为从未调用过 clearInterval。 严格模式通过运行两次效果(创建两个间隔)来帮助捕获此错误。
export default function App() {
const [time, setTime] = useState<Date>(new Date());
const handleTimeChange = useCallback((newTime: Date) => {
// 这将被记录两次,因为 `useEffect`
// 使用 `StrictMode` 运行两次,并且我们从未清除定时器
console.log("这是当前时间", newTime);
setTime(newTime);
}, []);
useEffect(() => {
const intervalId = setInterval(() => {
handleTimeChange(new Date());
}, 1_000);
// 取消注释下面这行代码来修复错误
// return () => clearInterval(intervalId);
}, [handleTimeChange]);
return (
<div className="App">
<h1>当前时间: {time.toLocaleTimeString()}</h1>
</div>
);
}
49. 安装 React Developer Tools 浏览器扩展来查看/编辑你的组件并检测性能问题
React Developer Tools 是一款必备扩展程序(Chrome、Firefox)。此扩展程序可让你:
- 可视化并深入研究 React 组件的细节,检查从 props 到状态的所有内容。
- 直接修改组件的状态或 props,以查看更改如何影响行为和渲染。
- 分析你的应用程序以确定组件重新渲染的时间和原因,帮助你发现性能问题。
- 等等。
在指南 1和指南 2中了解如何使用它。
50. React DevTools 组件:突出显示渲染的组件以识别潜在问题
如果你的应用存在性能问题时,都可以使用这个技巧。你可以突出显示渲染的组件(高亮显示)以检测潜在问题(例如,渲染次数过多)。
下面的视频中显示,只要时间发生变化,App 组件就会重新渲染,这是错误的。
51. 在自定义 hooks 中利用 useDebugValue 可以在 React DevTools 中获得更好的可视性
useDebugValue 可以成为一种便捷的工具,用于在 React DevTools 中为自定义钩子添加描述性标签。这使得直接从 DevTools 界面监视它们的状态变得更加容易。
例如,考虑一下我用来获取和显示当前时间的这个自定义钩子,每秒更新一次:
const useCurrentTime = () => {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(intervalId);
}, [setTime]);
return time;
};
不好的做法:如果没有 useDebugValue,实际时间值不会立即可见;你需要扩展 CurrentTime 钩子:
推荐做法:使用 useDebugValue 可以很容易地看到当前时间:
const useCurrentTime = () => {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(intervalId);
}, [setTime]);
// 新增代码
useDebugValue(time);
return time;
};
注意:请谨慎使用 useDebugValue。它最好用于共享库中的复杂钩子,因为了解内部状态至关重要。
52. 使用 why-did-you-render 等相关库来跟踪组件渲染并识别潜在的性能瓶颈
有时,组件会重新渲染,但无法立即查明原因。虽然 React DevTools 很有用,但在大型应用中,它可能只会提供模糊的解释,例如“hook #1 已渲染”,这可能是无用的。
在这种情况下,你可以求助于 why-did-you-render 库。它提供了有关组件重新渲染原因的更详细见解,有助于更有效地查明性能问题。来看以下一个示例,多亏了这个库,我们可以找到 FollowersList 组件的问题。
53. 在严格模式下第二次渲染时隐藏日志
StrictMode 有助于在应用程序开发早期发现错误。
但是,由于它会导致组件渲染两次,因此可能会导致重复的日志,从而使控制台变得混乱。
你可以在 Strict Mode 的第二次渲染期间隐藏日志以解决此问题,勾选hide logs....
选项即可。如下图所示:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。