• 作者:老汪软件技巧
  • 发表时间:2024-08-23 04:01
  • 浏览量:

一.ReactContextApi

useContext 是 React 内置的状态管理解决方案,在一定程度上解决了组件状态共享的问题,看一个简单的 demo

import { FC, PropsWithChildren, createContext, useContext, useState } from "react";
const Demo1 = () => {
  const { demo1, setDemo1 } = useContext(context);
  console.log(' demo1 渲染了');
  return <div>
    demo1: {demo1}
    <button onClick={() => setDemo1(demo1 + 1)}>加一button>
  div>;
};
const Demo2 = () => {
  const { demo2, setDemo2 } = useContext(context);
  const [num, setNum] = useState(0);
  console.log(" demo2 渲染了");
  return <div>
    demo2-Context: {demo2}
    <button onClick={() => setDemo2(demo2 + 1)}>加一button>
    <br />
    demo2-state: {num}
    <button onClick={() => setNum(num + 1)}>加一button>
  div>;
};
//加入contextProvide但是没有使用useContext
const Demo3 = () => {
  const [num, setNum] = useState(0);
  return <div>
    demo3-state: {num}
    <button onClick={() => setNum(num + 1)}>加一button>
  div>;
};
interface ContextType {
  demo1: number;
  demo2: number;
  setDemo1: (demo1: number) => void;
  setDemo2: (demo2: number) => void;
}
const context = createContext<ContextType>({
  demo1: 0,
  demo2: 0,
  setDemo1: () => { },
  setDemo2: () => { }
});
const Provider: FC<PropsWithChildren> = ({ children }) => {
  const [demo1, setDemo1] = useState(0);
  const [demo2, setDemo2] = useState(0);
  return (
    <context.Provider
      value={{
        demo1,
        demo2,
        setDemo1,
        setDemo2
      }}
    >
      {children}
    context.Provider>
  );
};
const App = () => (
  <Provider>
    < Demo1 />
    < Demo2 />
    <Demo3>Demo3>
  Provider>
);
export default App;

测试如下:

useContext最大的问题就是:只要context里面的值发生了变化,它包裹的子组件里面,只要使用了useContext,它都要重新渲染。比如:demo3加入了provide里面,但是它却没有受到影响。

解决方案只有一个,比如:demo3从provide里面拿出来,demo1和demo2只能把这两个值拆分成2个context了。现在又出现了多层嵌套,而且还会出现嵌套太深的问题。

终极解决方案: redux、zustand、jotai 等状态管理库。

const App = () => (
  <AaaProvider>
    <BbbProvider>
      <Aaa />
      <Bbb />
    BbbProvider>
  AaaProvider>
);

二.redux

redux是我最讨厌的状态管理库,太烦了用起来真的和说日语一样拗口,东一榔头西一棒槌,用着用着就想骂人,头顶一万个草泥马飘过。redux和context之间最大的区别就是,context烦的是浏览器,有不是我。redux烦的是我,而不是浏览器。好玩吧!

不发牢骚了,我简单的说一下吧!

redux并不是专属于react的状态管理库,它不但能在react里面使用,还能在vue里面使用。如果你要在react里面使用redux需要用react-redux来搭桥才能使用。

redux实现思路如下:

就是有一个仓库中心,我们把所有的state都放在store里面,然后你在编写纯函数reducer,这这个纯函数的主要功能就是操作store里面的state。在使用的时候,我们用dispatch去派发action就好了。如果dispatch派发的action是异步操作,就需要用到 redux-thunk 了,他是redux的中间件。

redux相关的知识点:

react-redux用connect将react组件和redux连接起来,它将UI组件变成了容器组件,connect方法接受两个参数:mapStateToProps和mapDispatchToProps,就是将redux里面的state和dispatch注入到组件的props里面去,我们就像使用父组件传入的值一样,直接使用仓库里面的值。

// 容器组件
import { connect } from "react-redux";
import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction,
} from "../../redux/count_action";
const mapStateToProps = (state) => ({ count: state });
const mapDispatchToProps = (dispatch) => ({
  increment: (number) => {
    dispatch(createIncrementAction(number));
  },
  incrementAsync: (number) => {
    dispatch(createIncrementAsyncAction(number, 500));
  },
  decrement: (number) => {
    dispatch(createDecrementAction(number));
  },
});
const CountUI = (props) => {
  return <>{props.count};
};
export default connect(mapStateToProps, mapDispatchToProps)(CountUI);

一般情况下redux都是绑定在最底层的main文件上的。

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from './redux/store'
import App from "./App";
ReactDOM.render(
  <Provider store={store}>
    <App />
  Provider>,
  document.getElementById("root")
);

这样整个应用所有组件都能够把数据存储到一个数据中心里面去了。其实它最大的优势就是集中管理数据,但是最大的劣势也是集中管理。当一个应用足够大的时候,你所有的数据都放在最底层去管理,到了最后项目成型需要扩展就成了难事,而且很多时候会牵一发而动全身,又变成了最大的缺点。

Redux采用全局状态管理的方式,将应用的状态存储在一个全局的store中。虽然这样可以方便地共享状态和数据,但也增加了状态管理的复杂性。状态的修改必须通过dispatch一个action来触发,这样的限制可能导致状态的更新变得不够直观和灵活。

缺点:

1.样板代码太多

2.有一定的学习成本

3.项目越大,数据越多,复杂性越高,难以扩展和调试

4.不适合小型项目和经常变动的项目

其实上面这些都是redux越来越没有人用的主要原因。

三.Zustand

其实zustand和redux是有点相似的,他们都使用 store 模式设计,但是zustand是react专用的,它内部使用了react的hook来解决state变化页面刷新的问题。

具体的原理参见:手写zustand

看看他的基本使用:

import { create } from 'zustand';
const useStore = create((set, get) => ({
  a: '',
  b: 0,
  updateA: (value) => set(() => ({ a: value })),
  updateB: (value) => set(() => ({ b: value })),
  getA: () => get().a,
  getB: () => get().b,
}));
export default function App() {
  const {
    a,
    b,
    updateA,
    updateB,
    getA,
    getB } = useStore();
  return (
    <div style={{ margin: '100px' }}>
      <input
        onChange={(e) => updateA(e.currentTarget.value)}
        value={a}
      />
      <p>hello, {a}p>
      <p>a 的值, {getA()}p>
      <button onClick={() => { updateB(b + 1); }}>点我button>
      <div>{b}div>
      <p>b 的值, {getB()}p>
    div>
  );
}

就是利用create方法创建一个数据仓库,它把state,还有修改state的所有的方法都放进了这个仓库里面,进行集中管理,最后它返回出来一个hook。store定义以后,不管在哪里使用useStore,他都能操作数据,而且不会引发context的副作用,也不会像redux一样繁琐。它是我的真爱!

既然是真爱,我就不想说它的缺点了,他们列出来的缺点我也不赞同,哈哈哈!

zustand这么可爱,你怎么能说他的坏话呢?

不过在ts里面它的中间件写法会有点不一样

四.jotai

jotai 是原子状态管理库的代表人物,看原子状态你有没有想起tailwind,他是原子化css的框架。就是把每个样式做一个类,然后丢给你用:

class="text-base p-1 border border-black border-solid">

jotai也是这个意思

就是用atom去定义值和方法,jotai收到值和方法以后,它会在内部创建一个容器,把他们都收集起来,我内部有多少值,你不用管,你只需要关注你定义的方法和值就好了。

jotai和zustand最大的区别就在这里,数据中心是我自己创建的,还是状态库那边创建的。

异步请求:

import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; 
async function getListById(id) {
    const data = {
        1: ['a1', 'a2', 'a3'],
        2: ['b1', 'b2', 'b3', 'b4']
    }
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(data[id]);
        }, 2000);
    });
}
const listAtom = atom([]);
const dataAtom = atom((get) => {
    return get(listAtom);
}, async (get, set, param) => {
    const data = await getListById(param);
    set(listAtom, data);
});
export default function App() {
    const [list, fetchListData] = useAtom(dataAtom);
    
    return <div>
        <button onClick={() => fetchListData(2)}>列表222button>
        <ul>
            {
                list.map(item => {
                    return <li key={item}>{item}li>
                })
            }
        ul>
    div>
}

多组件共用一个数据

import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; 
async function getListById(id) {
    const data = {
        1: ['a1', 'a2', 'a3'],
        2: ['b1', 'b2', 'b3', 'b4']
    }
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(data[id]);
        }, 2000);
    });
}
const listAtom = atom([]);
const dataAtom = atom((get) => {
    return get(listAtom);
}, async (get, set, param) => {
    const data = await getListById(param);
    set(listAtom, data);
});
export default function App() {
    const [list, fetchListData] = useAtom(dataAtom);
    
    return <div>
        <button onClick={() => fetchListData(2)}>列表222button>
        <ul>
            {
                list.map(item => {
                    return <li key={item}>{item}li>
                })
            }
        ul>
    div>
}

其实和zustand差不多

import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(persist((set) => ({
    count: 0,
    setCount: (value) => set({ count: value})
}), {
    name: 'count-key'
}))
export default function App() {
    const count = useStore(state => state.count);
    const setCount = useStore((state) => state.setCount);
    
    return <div>
        count: {count}
        <button onClick={() => setCount(count + 1)}>加一button>
    div>
}

对比zustand和jotai

zustand 是所有 state 放在全局 store 里,然后用到的时候 selector 取需要的部分。

jotai 是每个 state 单独声明原子状态,用到的时候单独用或者组合用。

一个自上而下,一个自下而上,这是两种思路。

不管是状态、派生状态、异步修改状态、中间件等方面,zustand 和 jotai 都是一样的。

区别只是一个是全局 store 里存储所有 state,一个是声明原子 state,然后组合。

这只是两种思路,没有好坏之分,看你业务需求,适合哪个就用那个,或者你习惯哪种思路就用哪个。