【山竹记账前端-react】7.使用 Zustand.js 做状态管理
大纲链接 §
[toc]
1. 用LocalStorage记录用户行为 ⇧
实现「跳过」功能:如果看过广告,就不用再看
创建 /home 路由 @/route/routes.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import { NotFoundPage } from '@/pages/NotFoundPage'
import { MainLayout } from '@/layouts/MainLayout'
import { welcomeRouter } from '@/route/welcomeRouter'
import type { RouteObject } from 'react-router-dom'
import { redirect } from 'react-router-dom'
export const routes: RouteObject[] = [
{
path: '/home',
element: (<div>home</div>),
},
{
path: '/',
element: <MainLayout />,
errorElement: (<NotFoundPage />),
loader: () => redirect('/welcome/1'),
},
welcomeRouter,
]
|
在 @/layouts/MainLayout.tsx 添加判断 hasRead
1
2
3
4
5
6
7
8
9
10
11
12
|
import { Navigate, Outlet } from 'react-router-dom'
export const MainLayout: React.FC = () => {
const hasRead = JSON.parse(localStorage.getItem('hasReadWelcome') ?? 'false')
if (hasRead) {
return <Navigate to="/home" />
} else {
return (
<div><Outlet /></div>
)
}
}
|
- 临时写法注意警告
Do not call 'localStorage.getItem('hasReadWelcome')' during render.
Components and hooks must be pure. Move this call into an event handler, effect, or state initializer.eslintreact/purity
- 避免在组件渲染时直接执行导航逻辑
- 防止警告
You should call navigate()in a React.useEffect(),not when your component is first rendered.
- 避免在
useEffect 中写路由导航跳转逻辑,是由于在组件挂载完成后才会执行跳转,相当于用户需要先看到第一个欢迎页面,再立即跳转
- 直接使用组件
<Navigate to="/home" />
在跳转组件中改变记录状态 @/modules/welcome/ToStartPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import { cn } from 'cn'
import { NavLink } from 'react-router-dom'
export const ToStartPage: React.FC = () => {
const onSkip = () => {
localStorage.setItem('hasReadWelcome', 'false')
console.log('存储了 hasReadWelcome')
}
return (
<section
className={cn(
'fixed right-[0.5em] top-[0.25em]',
'text-[#d4d4ee] text-[1.5em]',
)}>
<NavLink to="/home" onClick={onSkip}>跳过</NavLink>
</section>
)
}
|
- 客户端存储
localStorage.setItem('hasReadWelcome', true)
- 注意存储布尔时,最终存储的结果都是一个JSON字符串,
'true' 或者 'false'
- 直接读取并判断时,即使
'false' 也会判断为 true
- 可以变通存储为
yes 或者 no ,即 localStorage.setItem('hasReadWelcome', 'yes')
- 或者读取时转换
JSON.parse(localStorage.getItem('hasReadWelcome') ?? 'false')
2. 自动跳转到子路由 ⇧
实现自动跳转到 /welcome/1,先分离根路由 @/route/routes.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { NotFoundPage } from '@/pages/NotFoundPage'
import { MainLayout } from '@/layouts/MainLayout'
import { welcomeRouter } from '@/route/welcomeRouter'
import { Outlet } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'
export const routes: RouteObject[] = [
{
path: '/',
element: <MainLayout />,
},
{
path: '/home',
element: (<div>home</div>),
},
{
path: '/',
element: <Outlet />,
errorElement: <NotFoundPage />,
children: [welcomeRouter],
},
]
|
- 注意根路径
/ 的顺序必须排在最先,这个决定了匹配的顺序
- 先匹配
/,未匹配到再会去匹配 /welcome/1
修改根路径组件的跳转判断逻辑 @/layouts/MainLayout.tsx
修改引用的路由表 @/route/routes.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import { Root } from '@/components/Root'
import { welcomeRouter } from '@/route/welcomeRouter'
import type { RouteObject } from 'react-router-dom'
export const routes: RouteObject[] = [
{
path: '/',
element: <Root />,
},
{
path: '/home',
element: (<div>home</div>),
},
welcomeRouter,
]
|
3. 引入 zustand 状态管理 ⇧
安装 pnpm install zustand@5.0.15,使用 ts 示例 @/stores/useBearStore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import { create } from 'zustand'
interface BearState {
bears: number
getBears: () => number
increasePopulation: () => void
removeAllBears: () => void
updateBears: (newBears: number) => void
}
export const useBearStore = create<BearState>()((set, get) => ({
bears: 0,
getBears: () => get().bears,
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears: number) => set({ bears: newBears }),
}))
|
使用状态 @/pages/Home.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { useBearStore } from '@/stores/useBearStore'
export const Home: React.FC = () => {
const { bears, increasePopulation } = useBearStore()
return (
<div text-6xl>
<div>{bears}</div>
<button type="button" onClick={increasePopulation}>
+1
</button>
</div>
)
}
|
- 省去了
Redux 的 dispatch、action,接口极度简洁
pinia 就是抄袭了它的理念
- 状态变量可以直接解构使用
4. 用 zustand 封装 LocalStorage ⇧
LocalStorage 的问题
- 无法统一收集和管理,逻辑分散
- 如果 LocalStorage 变化了,所有引用的组件如何知道?
使用 zustand.js 改造一下 @/stores/useLocalStore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { create } from 'zustand'
interface Local {
hasReadWelcome: boolean | null
setHasReadWelcome: (read: boolean) => void
}
const init = localStorage.getItem('hasReadWelcome') ?? 'false'
export const useLocalStore = create<Local>()(set => ({
hasReadWelcome: JSON.parse(init === 'true' ? 'true' : 'false'),
setHasReadWelcome: (read: boolean) => {
localStorage.setItem('hasReadWelcome', read ? 'true' : 'false')
set({ hasReadWelcome: read })
},
}))
|
读取 @/components/Root.tsx
1
2
3
4
5
6
7
|
import { useLocalStore } from '@/stores/useLocalStore'
import { Navigate } from 'react-router-dom'
export const Root: React.FC = () => {
const { hasReadWelcome } = useLocalStore()
return <Navigate to={hasReadWelcome ? '/home' : '/welcome/1'} />
}
|
写 @/modules/welcome/ToStartPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { useLocalStore } from '@/stores/useLocalStore'
import { cn } from 'cn'
import { NavLink } from 'react-router-dom'
export const ToStartPage: React.FC = () => {
const { setHasReadWelcome } = useLocalStore()
const onSkip = () => { setHasReadWelcome(true) }
return (
<section
className={cn(
'fixed right-[0.5em] top-[0.25em]',
'text-[#d4d4ee] text-[1.5em]',
)}>
<NavLink to="/home" onClick={onSkip}>跳过</NavLink>
</section>
)
}
|
写 @/pages/Welcome4.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import { NavLink } from 'react-router-dom'
import cloud from '@/assets/icons/cloud.svg'
import { useLocalStore } from '@/stores/useLocalStore'
export const Welcome4: React.FC = () => {
const { setHasReadWelcome } = useLocalStore()
return (
<div className="h-[100%] overflow-clip flex flex-col justify-around items-center">
<img alt="cloud" src={cloud} w-129px h-83px className="translate-y-[120%]" />
<article>
<h2 className="text-center flex flex-col items-center text-[2em]">
<p>云备份</p>
<p>再也不怕数据丢失</p>
</h2>
</article>
<section className="text-[var(--primary-color)] text-[2em] font-bold translate-y-[-100%]">
<NavLink to="/home" onClick={() => setHasReadWelcome(true)}>开启应用</NavLink>
</section>
</div>
)
}
|
使用优点
- 读写接口封装了
- 任何组件使用同一个对象,只要有一个组件写入改变了状态,其他组件都会改变,UI 自动刷新
·未完待续·
参考文章
相关文章
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名