React中级教程(五)– 全局状态管理实践

我们已学习了全局状态管理的知识,这一篇文章我们来实践下,优化我们的game-hub项目。


选择正确的状态管理解决方案

打开game-hub的App.tsx查看,发现有一个本地查询对象gameQuery。通过Props传递该对象或该对象的属性到子组件。

子组件又会传递到它的子组件,比如NavBar组件又将gameQuery对象的属性传递到 SearchInput。这就属于典型的穿透传递 了。

// App.tsx
​
......
​
export interface GameQuery {
  genreID?: number;
  platformID?: number;
  sortOrder: string;
  searchText: string;
}
function App() {
  const [gameQuery, setGameQuery] = useState<GameQuery>({} as GameQuery);
  return (
    <Grid
      templateAreas={{
        base: `"nav""main"`,
        lg: `"nav nav""aside main"`,
      }}
      templateColumns={{
        base: "1fr",
        lg: "200px 1fr",
      }}
    >
      <GridItem area="nav">
        <NavBar
          onSearch={(searchText: string) =>
            setGameQuery({ ...gameQuery, searchText })
          }
        />
      </GridItem>
      <Show above="lg">
        <GridItem area="aside" paddingX={5}>
          <GenreList
            selectedGenreID={gameQuery.genreID}
            onSelectGenre={(genreID) => setGameQuery({ ...gameQuery, genreID })}
          />
        </GridItem>
      </Show>
      <GridItem area="main">
        <Box paddingLeft={2}>
          <GameHeading gameQuery={gameQuery} />
          <Flex paddingBottom={5}>
            <Box marginRight={5}>
              <PlatformSelector
                onSelectPlatform={(platformID) =>
                  setGameQuery({ ...gameQuery, platformID })
                }
                selectedPlatformID={gameQuery.platformID}
              />
            </Box>
            <SortSelector
              sortOrder={gameQuery.sortOrder}
              onSelectSortOrder={(sortOrder) =>
                setGameQuery({ ...gameQuery, sortOrder })
              }
            />
          </Flex>
        </Box>
        <GameGrid gameQuery={gameQuery} />
      </GridItem>
    </Grid>
  );
}
​
export default App;
​

另外,更新逻辑也非常分散。

显然,这里需要状态管理工具来协助。

哪种方案比较合适呢?

使用Reducer可以使相关更新逻辑集中处理。而状态传递用Context合适吗?

不合适!

gameQuery有genreID、platformID、sortOrder、searchText这些属性,如果使用Context的话,任何一个属性的改动将会导致使用到Context的组件重新渲染。比如:我们点击某个genre,NavBar就会重新渲染,platform选择框也会重新渲染,显然这是不必要的。

正如前一节讲过的,我们需要一种观察特定状态属性而进行渲染的工具。Zustand就是极好的一种选择。


设置Zustand 存储

第一步自然是安装:

npm i zustand@4.3.7

在App.tsx同目录下创建 store.ts文件。

首先将原来在App.tsx中的interface GameQuery移到store.ts中,然后创建store的结构,包括gameQuery以及操作函数setSearchText、setGenreID、setPlatformID、setSortOrder。接着使用zustand的create函数设置store结构的数据默认值以及函数实现细节。

// store.ts
​
import { create } from "zustand";
​
interface GameQuery {
    genreID?: number;
    platformID?: number;
    sortOrder?: string;
    searchText?: string;
  }
​
interface GameQueryStore{
    gameQuery:GameQuery;
    setSearchText:(searchText:string)=>void;
    setGenreID:(genreID:number)=>void;
    setplatformID:(platformID:number)=>void;
    setSortOrder:(sortOrder:string)=>void;
}
​
const useGameQueryStore = create<GameQueryStore>(set=>({
    gameQuery:{},
    setSearchText:(searchText)=>set(()=>({
        gameQuery:{searchText}
    })),
    setGenreID:(genreID)=>set(store=>({
        gameQuery:{...store.gameQuery,genreID}
    })),
    setplatformID:(platformID)=>set(store=>({
        gameQuery:{...store.gameQuery,platformID}
    })),
    setSortOrder:(sortOrder)=>set(store=>({
        gameQuery:{...store.gameQuery, sortOrder}
    }))
}))
​
export default useGameQueryStore;

移除Props

这一节修改的代码太多,但大多类似,主要就是检查App.tsx,去掉Props以及相关的Interface,然后逐层检查去除Props,在最后实现的时候,用useGameStore的某个解构属性来代替Props的实现即可。完成之后,App.tsx就非常简洁。没有数据结构GameQuery,也没有向下传递 Props。

// App.tsx
​
import { Box, Flex, Grid, GridItem, Show } from "@chakra-ui/react";
import GameGrid from "./components/GameGrid";
import GameHeading from "./components/GameHeading";
import GenreList from "./components/GenreList";
import NavBar from "./components/NavBar";
import PlatformSelector from "./components/PlatformSelector";
import SortSelector from "./components/SortSelector";
​
function App() {
  return (
    <Grid
      templateAreas={{
        base: `"nav""main"`,
        lg: `"nav nav""aside main"`,
      }}
      templateColumns={{
        base: "1fr",
        lg: "200px 1fr",
      }}
    >
      <GridItem area="nav">
        <NavBar />
      </GridItem>
      <Show above="lg">
        <GridItem area="aside" paddingX={5}>
          <GenreList />
        </GridItem>
      </Show>
      <GridItem area="main">
        <Box paddingLeft={2}>
          <GameHeading />
          <Flex paddingBottom={5}>
            <Box marginRight={5}>
              <PlatformSelector />
            </Box>
            <SortSelector />
          </Flex>
        </Box>
        <GameGrid />
      </GridItem>
    </Grid>
  );
}
​
export default App;
​
​

而App.tsx 的下层相应修改,由于较多修改且基本一致,仅列 GameHeading.tsx.

// GameHeading.tsx
.....
​
import useGameQueryStore from "../store";
​
const GameHeading = () => {
  const genreID = useGameQueryStore((s) => s.gameQuery.genreID);
  const platformID = useGameQueryStore((s) => s.gameQuery.platformID);
  ......
  
};
​
export default GameHeading;
​

完成修改后,测试页面各项功能,确保修改未产生错误。

提交。“Refactor- manage state with zustand”


讨论 – 构建可重用组件

利用全局状态管理可以极大简化我们的代码,但会降低可重用性。因为都要依赖于store。

而使用 Props ,每个组件基本是独立的,可重用性非常强。

就我们这个项目而言,本节的重构部分不太可能需要重用,所以选择zustand是合理的。即便后续有改变,也可以添加 Props来实现重用。

所以没有一种模式是通用的,我们要根据实际项目选择。


小结

本文利用Zustand重构了game-hub应用,简化了全局状态管理。

下一篇计划介绍React的路由。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注