【山竹记账前端-react】7.使用 Zustand.js 做状态管理


大纲链接 §

[toc]


1. 用LocalStorage记录用户行为 ⇧

实现「跳过」功能:如果看过广告,就不用再看

  • 用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

  • 未标记看过欢迎页,则跳转到第一页

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    import { Navigate } 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 <Navigate to="/welcome/1" />
    }
    }
    
  • 由于跳转逻辑改变了,需要重命名、移动组件 @/layouts/MainLayout.tsx -> @/components/Root.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 变化了,所有引用的组件如何知道?
    • 手动监听Storage事件,繁琐

使用 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
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名