为什么我们需要Zustand ?
在之前的文章中,我们知道了React是单向数据流,数据信息只能由祖先传递给子孙,而子孙如果想要传递信息给父代,只能通过调用父代的函数来对数据进行操作。 后来,我们学习了useContext和useReducer,这下我们可以进行全局的状态管理了,createContext可以创建一个全局的上下文,只需要在某组件内引入并利用Provider传输信息,再在响应子孙组件利用useContext就可以拿到信息。就像下面的例子一样:
// UserContext.js
import React, { createContext, useReducer } from 'react';
const UserContext = createContext();
const userReducer = (state, action) => {
switch (action.type) {
case 'SET_USERS':
return { ...state, users: action.payload };
default:
return state;
}
};
export const UserProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, { users: [] });
const setUsers = (users) => {
dispatch({ type: 'SET_USERS', payload: users });
};
return (
<UserContext.Provider value={{ state, setUsers }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
虽然能够实现全局通信了,但是它的逻辑依然很繁琐,useContext和useReducer结合起来,还是令人看的头晕眼花,于是,为了实现组件之间的便捷通讯,我们选择在项目中把数据状态和修改数据状态的方法作为全局式来声明。 而Zustand就是一个很好的工具。
Zustand长什么样 ?
当然,刚刚我们说Zustand是一个很便捷的工具,大家要有一个直观的感受比较好啊,所以我们先来看看用了Zustand后的数据管理是什么样子的: (上面的例子改写后的结果)
import create from 'zustand';
const userStore = create(set => ({
users:[]
setUsers: (users) => set(state => ({...state, users})),
}));
export default useStore;
子组件如果要使用:
import {userStore} from './store/userStore'
const Childcomponent = ()=>{
const {users} = userStore((state) => state.users);
const {setUsers} = userStore((state) => state.setUsers);
return(
<>
users.map(user=>(
<div>{user}</div>
))
</>
)
}
export default Childcomponent
是的,没错,我们只需要将数据状态和对这个数据状态的操作单独取出来放在一个文件,其他文件只要import就能够访问和修改,是不是比之前简单多了!
接下来我们来解析一下它的语法!
Zustand 用法解析
创造一个store
import create from 'zustand';
const userStore = create(set => ({
users:[]
setUsers: (users) => set(state => ({...state, users})),
}));
export default useStore;
首先是原文件,我们要引入zustand库中的create方法,来创建一个存储仓库,用来存储数据和方法。
(先在文件根目录下用npm install zustand安装zustand)
这个create函数接收一个set函数作为参数,set函数被用来更新数据状态,它可以接收一个函数或者一个对象来实现对象的更新。
我们一般都是传递一个回调函数,例如上面的例子中:
setUsers: (users) => set(state => ({...state, users}))
setUsers接收一个参数,并执行set函数,set函数接收一个state参数,这个state代表着当前未更新的store的各个数据状态和方法。
现在一个store就完整了,包含着一些数据,以及对数据进行修改的方法。
使用一个store
如何使用一个store呢?我们只需要在子组件中引入即可:
import {userStore} from './store/userStore'
const Childcomponent = ()=>{
const {users} = userStore((state) => state.users);
const {setUsers} = userStore((state) => state.setUsers);
return(
<>
</>
)
}
export default Childcomponent
在这里我们直接引入,并解构赋值。
在这里我们有两种解构方法:
第一种就是上面这一种const {users} = userStore((state) => state.users)
userStore接收一个回调函数,这个是选择器函数, 参数state代表当前store的状态。
第二种是这样:const {users,setUsers} = userStore()
也是利用解构拿到信息,但是呢,这种方法有一种坏处就是:当store任何一条数据发生变化的时候,都会触发组件的渲染,导致无意义的渲染,而且如果store信息太多,它会逐条遍历,浪费性能和时间。
这就是Zustand基础的用法了,下面我们来用fetch github API的例子来看一看吧!
例子
store的创建
目标:我们要拿到github主页所存在的各个项目名称
首先我们在src文件下创建一个store文件夹,再在其中创建一个repos.js,
在它里面创建一个store:
import { create } from 'zustand';
export const useReposStore = create((set) => ({
repos: [],
loading: false,
error: null,
fetchRepos:
}))
首先我们把基本的数据项创建好,我们需要一个repos数组来储存项目名,其次用loading来表示是否处在加载中,因为fetch操作是异步,是耗时的,其次我们创建一个error变量,表示是否出错,最后创建fetchRepos方法,来实现对 api 的操作。
既然存在对 api 的操作,我们为何不用axios对其进行系统化封装管理呢?
对api操作的系统化管理
axios相当于是为了api管理而生的zustand,它让api管理变得容易和清晰了。
首先我们在src目录下创建一个api文件夹,首先创建config.js,在其中创建baseURL:
import axios from 'axios';
axios.defaults.baseURL = 'https://api.github.com';
export default axios
其次我们创建一个repos.js文件,里面包含对这个api的各种操作:
import axios from './config';
export const getRepoList = async (owner) => {
const res = await axios.get(`/users/${owner}/repos`);
return res.data;
}
axios VS fetch
axios简化了一些流程,比如我们直接使用axios.get()就能拿到一个返回的数据,而这个数据还不需要toJSON()来解码,我们直接拿到的就是json格式的数据,相应的,我们利用axios.post()可以直接发送json格式的数据,也不需要我们JSON.stringify()来转换格式再发送。
它还有好多可以详细讲解的,关于它们俩的对比,我们来放在后面的文章吧!
fetchRepos
OK,网络请求这一块也封装完了,现在我们可以来撰写store的fetchRepos方法了:
import { getRepoList } from '../api/repo';
import { create } from 'zustand';
export const useReposStore = create((set) => ({
repos: [],
loading: false,
error: null,
fetchRepos: async()={
try{
set({loading:true,error:null})
const res = await getRepoList('xxxx'); // 拿取谁的仓库就写谁的名字
set({repos:res,loading:false})
}catch(error){
set({ error: error.message, loading: false })
}
}
}))
首先我们设置loading:true,error:null,作为开始请求时的状态,之后利用try-catch,用异步变同步来等待结果,之后利用set函数更新store中的状态。
接下来就是RepoList组件了:
import { useReposStore } from '../../store/repos';
import { useEffect } from 'react';
const RepoList = () => {
const { repos, loading, error, fetchRepos } = useReposStore();
useEffect(() => {
fetchRepos();
}, []);
if (loading) return <p>Loading</p>
if (error) return <p>{error}</p>
return (
<>
<h2>RepoList</h2>
<ul>{
repos.map(repo => (
<li key={repo.id}>
<a href={repo.html_url} target="_blank" rel='noreferrer'>{repo.name}</a>
<p>{repo.description || 'No description'}</p>
</li>
))
}</ul>
</>
)
}
export default RepoList;
将它挂载到App组件就能运行了。
总结
这就是 Zustand的一个基本应用了,它基本上就是把useContext和useReducer结合在一起了,useContext允许包裹的元素使用相应的数据,useReducer允许对数据进行操作,而Zustand集成了它们的功能,把数据和操作数据的方法都定义在了一个文件之中,大大简化了我们使用数据的流程。