译:101个React技巧#5高效状态管理

83 阅读8分钟

28. 不要为可以从其他状态或属性派生的值创建状态

状态越多,麻烦越多。

每一个状态都可能触发重新渲染,并且会让状态重置变得麻烦。

因此,如果一个值可以从状态或属性派生出来,就不要添加新的状态。

❌ 错误做法: filteredPosts 不需要放在状态中。

function App({ posts }) {
  const [filters, setFilters] = useState();
  const [filteredPosts, setFilteredPosts] = useState([]);

  useEffect(() => {
    setFilteredPosts(filterPosts(posts, filters));
  }, [posts, filters]);

  return (
    <Dashboard>
      <Filters filters={filters} onFiltersChange={setFilters} />
      {filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
    </Dashboard>
  );
}

✅ 正确做法: filteredPostspostsfilters 派生而来。

function App({ posts }) {
  const [filters, setFilters] = useState({});
  const filteredPosts = filterPosts(posts, filters);

  return (
    <Dashboard>
      <Filters filters={filters} onFiltersChange={setFilters} />
      {filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
    </Dashboard>
  );
}

29. 将状态保持在尽可能低的层级,以减少重新渲染

每当组件内部的状态发生变化时,React 会重新渲染该组件及其所有子组件(用 memo 包装的子组件除外)。

即使这些子组件没有使用发生变化的状态,也会发生重新渲染。为了减少重新渲染,应尽可能将状态下移到组件树中。

❌ 错误做法:sortOrder 改变时,LeftSidebarRightSidebar 都会重新渲染。

function App() {
  const [sortOrder, setSortOrder] = useState("popular");
  return (
    <div className="App">
      <LeftSidebar />
      <Main sortOrder={sortOrder} setSortOrder={setSortOrder} />
      <RightSidebar />
    </div>
  );
}

function Main({ sortOrder, setSortOrder }) {
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        热门
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        最新
      </Button>
    </div>
  );
}

✅ 正确做法: sortOrder 的改变只会影响 Main

function App() {
  return (
    <div className="App">
      <LeftSidebar />
      <Main />
      <RightSidebar />
    </div>
  );
}

function Main() {
  const [sortOrder, setSortOrder] = useState("popular");
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        热门
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        最新
      </Button>
    </div>
  );
}

30. 明确初始状态和当前状态的区别

❌ 错误做法: 不清楚 sortOrder 只是初始值,这可能会导致状态管理方面的混淆或错误。

function Main({ sortOrder }) {
  const [internalSortOrder, setInternalSortOrder] = useState(sortOrder);
  return (
    <div>
      <Button
        onClick={() => setInternalSortOrder("popular")}
        active={internalSortOrder === "popular"}
      >
        热门
      </Button>
      <Button
        onClick={() => setInternalSortOrder("latest")}
        active={internalSortOrder === "latest"}
      >
        最新
      </Button>
    </div>
  );
}

✅ 正确做法: 命名清晰地表明了什么是初始状态,什么是当前状态。

function Main({ initialSortOrder }) {
  const [sortOrder, setSortOrder] = useState(initialSortOrder);
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        热门
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        最新
      </Button>
    </div>
  );
}

31. 基于前一个状态更新状态,特别是在使用 useCallback 进行记忆化时

React 允许你将一个更新函数传递给 useState 中的 set 函数。

这个更新函数使用当前状态来计算下一个状态。

每当我需要基于前一个状态更新状态时,特别是在使用 useCallback 包装的函数内部,我都会使用这种方法。事实上,这种方法可以避免将状态作为钩子的依赖项之一。

❌ 错误做法: handleAddTodohandleRemoveTodo 会在 todos 改变时发生变化。

function App() {
  const [todos, setToDos] = useState([]);
  const handleAddTodo = useCallback(
    (todo) => {
      setToDos([...todos, todo]);
    },
    [todos]
  );

  const handleRemoveTodo = useCallback(
    (id) => {
      setToDos(todos.filter((todo) => todo.id !== id));
    },
    [todos]
  );

  return (
    <div className="App">
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
    </div>
  );
}

✅ 正确做法: 即使 todos 改变,handleAddTodohandleRemoveTodo 也保持不变。

function App() {
  const [todos, setToDos] = useState([]);
  const handleAddTodo = useCallback((todo) => {
    setToDos((prevTodos) => [...prevTodos, todo]);
  }, []);

  const handleRemoveTodo = useCallback((id) => {
    setToDos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  }, []);

  return (
    <div className="App">
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
    </div>
  );
}

32. 在 useState 中使用函数进行惰性初始化并提高性能,因为它们只被调用一次

useState 中使用函数可以确保初始状态只计算一次。

这可以提高性能,特别是当初始状态是从一个 “昂贵” 的操作(如从本地存储中读取)派生而来时。

❌ 错误做法: 每次组件渲染时,我们都会从本地存储中读取主题。

const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";

function PageWrapper({ children }) {
  const [theme, setTheme] = useState(
    localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const handleThemeChange = (theme) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div
      className="page-wrapper"
      style={{ background: theme === "dark" ? "black" : "white" }}
    >
      <div className="header">
        <button onClick={() => handleThemeChange("dark")}>深色</button>
        <button onClick={() => handleThemeChange("light")}>浅色</button>
      </div>
      <div>{children}</div>
    </div>
  );
}

✅ 正确做法: 我们只在组件挂载时从本地存储中读取数据。

function PageWrapper({ children }) {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const handleThemeChange = (theme) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div
      className="page-wrapper"
      style={{ background: theme === "dark" ? "black" : "white" }}
    >
      <div className="header">
        <button onClick={() => handleThemeChange("dark")}>深色</button>
        <button onClick={() => handleThemeChange("light")}>浅色</button>
      </div>
      <div>{children}</div>
    </div>
  );
}

33. 使用 React 上下文来处理广泛需要的静态状态,以避免属性穿透

每当我有一些数据满足以下条件时,我就会使用 React 上下文:

  • 在多个地方都需要(例如主题、当前用户等)
  • 大部分是静态的或只读的(即用户不经常更改这些数据)

这种方法有助于避免属性穿透(即通过组件层次结构的多个层级传递数据或状态)。

请查看下面沙箱中的示例 👇。

🏖 沙箱

34. React Context:将上下文拆分为频繁变化的部分和不常变化的部分,以提高应用性能

React 上下文的一个挑战是,每当上下文数据发生变化时,所有消费该上下文的组件都会重新渲染,即使它们没有使用发生变化的那部分上下文 🤦♀️。

解决方案是什么? 使用单独的上下文。

在下面的示例中,我们创建了两个上下文:一个用于操作(这些操作是常量),另一个用于状态(状态可能会发生变化)。

🏖 沙箱

35. React Context:当值的计算不简单时,引入一个 Provider 组件

❌ 错误做法: App 内部有太多逻辑来管理主题。

const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";
const DEFAULT_THEME = "light";

const ThemeContext = createContext({
  theme: DEFAULT_THEME,
  setTheme: () => null,
});

function 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 [selectedPostId, setSelectedPostId] = useState(undefined);
  const onPostSelect = (postId) => {
    // TODO: 一些日志记录
    setSelectedPostId(postId);
  };

  const posts = useSWR("/api/posts", fetcher);

  return (
    <div className="App">
      <ThemeContext.Provider value={themeContextValue}>
        <Dashboard
          posts={posts}
          onPostSelect={onPostSelect}
          selectedPostId={selectedPostId}
        />
      </ThemeContext.Provider>
    </div>
  );
}

✅ 正确做法: 主题逻辑封装在 ThemeProvider 中。

function App() {
  const [selectedPostId, setSelectedPostId] = useState(undefined);
  const onPostSelect = (postId) => {
    // TODO: 一些日志记录
    setSelectedPostId(postId);
  };

  const posts = useSWR("/api/posts", fetcher);

  return (
    <div className="App">
      <ThemeProvider>
        <Dashboard
          posts={posts}
          onPostSelect={onPostSelect}
          selectedPostId={selectedPostId}
        />
      </ThemeProvider>
    </div>
  );
}

function 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>
  );
}

36. 考虑使用 useReducer 钩子作为轻量级的状态管理解决方案

每当我的状态中有太多值,或者状态很复杂,又不想依赖外部库时,我就会使用 useReducer

当它与上下文结合使用以满足更广泛的状态管理需求时,尤其有效。

示例: 请参阅 #提示 34

37. 使用 useImmeruseImmerReducer 简化状态更新

使用 useStateuseReducer 等钩子时,状态必须是不可变的(即所有更改都需要创建一个新状态,而不是修改当前状态)。

这通常很难实现。

这就是 useImmeruseImmerReducer 提供更简单替代方案的原因。它们允许你编写 “可变” 的代码,这些代码会自动转换为不可变的更新。

❌ 繁琐的做法: 我们必须仔细确保创建一个新的状态对象。

export function 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>
      <p>
        邮箱: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        密码:
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

✅ 更简单的做法: 我们可以直接修改 draftState

import { useImmer } from "use-immer";

export function 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;
    });
  };

  /// 其余逻辑
}

38. 对于跨多个组件访问的复杂客户端状态,使用 Redux(或其他状态管理解决方案)

每当满足以下条件时,我就会使用 Redux

  • 我有一个复杂的前端应用,有很多共享的客户端状态(例如仪表盘应用)
  • 我希望用户能够回退并撤销更改
  • 我不希望我的组件像使用 React 上下文时那样不必要地重新渲染
  • 我有太多的上下文开始变得难以控制

为了获得更流畅的体验,我建议使用 redux-tooltkit

💡 注意:你也可以考虑其他 Redux 的替代方案,如 ZustandRecoil

39. Redux:使用 Redux DevTools 调试你的状态

Redux DevTools 浏览器扩展 是调试 Redux 项目的有用工具。

它允许你实时可视化状态和操作,在刷新页面时保持状态持久化,等等。

要了解其用法,请观看这个很棒的 YouTube 视频