我们已学习了全局状态管理的知识,这一篇文章我们来实践下,优化我们的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的路由。