React进阶——状态管理的应用与实践

React拥有着庞大的生态,衍生了许多的状态管理方案。React的状态可以按照两种方式去分类:

可访问性

  • 局部状态(local state
  • 全局状态(global state

  • 远程状态(remote state
  • 界面状态(UI state

我们要根据不同的情况选用合适的管理方案,以达到最佳实践。

局部状态管理

useState

useState是我们最常用的hooks之一,它返回一个数组,包含状态和设置状态的函数

jsx 复制代码
import { useState } from "react";

function Counter(){
  const [count, setCount] = useState(0)
  return(
   <>
     <span>{count}</span>
     <button onClick={()=>setCount((count)=>count+1)}>+</button>
   </>
 )
}

useState定义的状态只能在组件内部使用,或通过props传递给子组件,当传递层数过多时,会使代码变得复杂和难以理解(prop drilling

useReducer

在某些情况下,只使用useState去管理状态是不够的

  • 当组件拥有大量状态且事件处理程序中涉及到大量state的更新时
  • 当多个状态更新需要同时发生时(作为对同一事件的回调,如"开始游戏")
  • 当更新一个状态依赖于一个或多个其他状态时

在上述情况下使用useState,代码会非常的冗余,违背了DRY原则的同时,也会使代码难以阅读。此时,useReducer可以提供很大的帮助

useReducer是另一种设置状态的方法,非常适合复杂的状态和相关的状态片段,它将相关的状态片段存储在对象中,通过action.type批量更新状态

useReducer有三个相关函数和对象:

  1. reducer: 纯函数(没有副作用),接受当前状态和操作,并返回下一个状态
  2. action: 描述如何更新状态的对象
  3. dispatch: 通过从事件处理程序"发送"actionreducer来触发状态更新的函数(相当于setState)

顾名思义,useReducer会将操作state的行为合并,减少操作state的次数

我们可以把useReducer看做是现实中去银行柜台取钱,前台小姐姐就相当于dispatch,我们告诉她我们要"取钱以及账户和要取的金额"(action),然后操作人员会将金额取出并更新账户余额(reducer)

具体代码放在CodeSandbox上了,感兴趣的可以看一下银行demo

useState VS. useReducer

    • useState非常适合单个、独立的状态片段(数字、字符串、单个数组等)
    • useReducer非常适合多个相关的状态和复杂状态(例如,具有许多值的对象和嵌套的对象或数组)
    • useState更新状态的逻辑直接放在事件处理程序或效果中,分布在一个或多个组件中
    • useReducer更新状态的逻辑集中在一个位置,与组件解耦:reducer
    • useState通过调用setState(从useState返回的setter)来更新状态
    • useReducer通过向reducer分派一个action来更新状态
    • useState:命令式更新状态
    • useReducer:声明式更新状态

如何选择?

全局状态管理

Context API

Context API的帮助下, 系统在整个应用程序中传递数据,而无需手动在组件树中传递props, 它允许我们向整个应用"广播"全局状态

  • Provider:赋予所有子组件访问value的权限
  • value:我们想要提供的数据(通常是状态和函数)
  • Consumers: 读取使用Provider的上下文值的所有组件

如图所示: value的每一次更新都会导致所有Consumers的重新渲染

在使用Context API时, 我们通常将Provider抽离为单独的文件, 然后导出一个使用context的hook, 方便复用:

jsx 复制代码
import { createContext, useContext } from "react";

const PostContext = createContext();

function PostProvider({ children }) {
  const [posts, setPosts] = useState(() =>
    Array.from({ length: 30 }, () => createRandomPost())
  );
  const [searchQuery, setSearchQuery] = useState("");

  function handleAddPost(post) {
    setPosts((posts) => [post, ...posts]);
  }

  function handleClearPosts() {
    setPosts([]);
  }

  return 
     <PostContext.Provider
         value={{
           posts: searchedPosts,
           onAddPost: handleAddPost,
           onClearPosts: handleClearPosts,
           searchQuery,
           setSearchQuery,
         }}
     >
      {children}
    </PostContext.Provider>
}

const usePost = () => {
  const context = useContext(PostContext);
  //在PostProvider外使用context, 得到的是undefined
  if (context === undefined)  
    throw new Error("PostContext was used outside of the PostProvider");
  return context;
};

export { PostProvider, usePost };

Consumers使用全局状态也十分简单, 在确保被Provider包裹后, 直接调用导出的hook解构value对象即可

jsx 复制代码
import { PostProvider, usePost } from "./context/PostContext";

function App() {
  return (
      <PostProvider>
        <Header />
        <Main />
        <Archive />
        <Footer />
      </PostProvider>
  );
}

function Header() {
  const { onClearPosts } = usePost();

  return (
    <header>
      <h1>
        <span>⚛️</span>The Atomic Blog
      </h1>
      <div>
        <button onClick={onClearPosts}>Clear posts</button>
      </div>
    </header>
  );
}

function SearchPosts() {
  const { searchQuery, setSearchQuery } = usePost();
  return (
    <input
      value={searchQuery}
      onChange={(e) => setSearchQuery(e.target.value)}
      placeholder="Search posts..."
    />
  );
}

Redux

Redux是一个用来管理全局状态的第三方库, 它是一个独立的库 , 我们可以使用react-redux库将其集成到React应用程序中。它在概念上类似于使用Context API + useReducer

通过Redux,我们将所有全局状态都存储在一个全局可访问的store中,使用"aciton"对状态进行更新

从历史上看,Redux管理全局状态的首选方案。今天,这种情况发生了变化,因为有很多选择。许多应用不再需要Redux,除非它们需要大量的全局UI状态

useReducer VS. Redux

useReducer

Redux

可以看到,Redux相比useReducer的机制十分相似,只是增加了action creator函数(用于处理副作用),然后将多个reducer集中管理,使状态更新逻辑与应用程序的其余部分分离。

Redux中间件------thunk

Redux中间件(Middleware)位于调度操作和存储之间的函数。允许我们在dispatch之后,到达store中的reducer之前运行代码

thunk是Redux实现异步API调用(或其他异步操作)的中间件


目前Redux有着两种主流写法, 官方推荐使用Redux toolkit, 这里我们从一个银行案例中分析对比两种写法:

需求:

  • 账户: 余额, 贷款, 贷款目的, 存(涉及货币转换)取钱
  • 消费者: 姓名, ID, 创办时间

Redux经典写法

依赖安装:

js 复制代码
npm i redux
npm i react-redux
npm i redux-thunk  //异步操作中间件
npm i redux-devtools-extension //devtools

与账户相关的操作放在accountSlice.js中,注意点:

  • 默认规定在操作前要加前缀,如账户存钱:account/deposit
  • 为每个操作编写action creator函数, 返回对应action操作, 副作用在该函数中执行, 确保Reducer是纯函数
  • default不再是抛出一个异常, 而是返回state本身
js 复制代码
const initialStateAccount = {
  balance: 0,
  loan: 0,
  loanPurpose: "",
  isLoading: false,
};

export default function accountReducer(state = initialStateAccount, action) {
  switch (action.type) {
    case "account/deposit":
      return {
        ...state,
        balance: state.balance + action.payload,
        isLoading: false,
      };
    case "account/withdraw":
      return { ...state, balance: state.balance - action.payload };
    case "account/requestLoan":
      if (state.loan > 0) return state;
      return {
        ...state,
        loan: action.payload.amount,
        loanPurpose: action.payload.purpose,
        balance: state.balance + action.payload.amount,
      };
    case "account/payLoan":
      return {
        ...state,
        loan: 0,
        loanPurpose: "",
        balance: state.balance - state.loan,
      };
    case "account/convertingCurrency":
      return { ...state, isLoading: true };
    default:
      return state;
  }
}

export function deposit(amount, currency) {
  if (currency === "USD") return { type: "account/deposit", payload: amount };

  return async function (dispatch, getState) {
    dispatch({ type: "account/convertingCurrency" });
    //API call
    const res = await fetch(
      `https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
    );
    const data = await res.json();
    const converted = data.rates.USD;

    dispatch({ type: "account/deposit", payload: converted });
  };
}

export function withdraw(amount) {
  return { type: "account/withdraw", payload: amount };
}

export function requestLoan(amount, purpose) {
  return { type: "account/requestLoan", payload: { amount, purpose } };
}

export function payLoan() {
  return { type: "account/payLoan" };
}

customer相关操作与其类似, 放在customrSlice.js

js 复制代码
const initialStateCustomer = {
  fullName: "",
  nationalID: "",
  createdAt: "",
};

export default function customerReducer(state = initialStateCustomer, action) {
  switch (action.type) {
    case "customer/createCustomer":
      return {
        ...state,
        fullName: action.payload.fullName,
        nationalID: action.payload.nationalID,
        createdAt: action.payload.createdAt,
      };
    case "customer/updateName":
      return {
        ...state,
        fullName: action.payload,
      };
    default:
      return state;
  }
}

export function createCustomer(fullName, nationalID) {
  return {
    type: "customer/createCustomer",
    payload: { fullName, nationalID, createdAt: new Date().toISOString() },
  };
}

export function updateName(fullName) {
  return { type: "customer/updateName", payload: fullName };
}

store.js中, 我们将两个reducer合并为一个, 创建store并应用上中间件和devtools

js 复制代码
import { applyMiddleware, combineReducers, createStore } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";

import accountReducer from "./features/accounts/accountSlice";
import customerReducer from "./features/customers/customerSlice";

const rootReducer = combineReducers({
  account: accountReducer,
  customer: customerReducer,
});

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

export default store;

最后在index.js中使用Provider组件应用即可

jsx 复制代码
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";

import "./index.css";
import App from "./App";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Redux toolkit写法

Redux tookit是编写Redux代码的现代和首选方式, 它100%兼容Redux经典写法,允许我们一起使用它们, 我们可以通过Redux tookit编写更少的代码来实现相同的结果。

相比经典写法,Redux tookit主要有三个不同点

  • 我们可以在reducer内部编写"改变"状态的代码(将通过"Immer"库在幕后转换为不可变逻辑)
  • 自动创建action creator
  • 自动设置thunk中间件和DevTools

依赖安装:

js 复制代码
npm i @reduxjs/toolkit   

使用Redux tookit无需再写action creator, 会自动生成在xxxSlice的actions属性中

createSlice需要传递name属性,初始值和包含reducer函数的reducers对象

默认情况下,reducer函数的action.payload只接受一个参数 , 如果要传递多个参数, 需要写成对象的形式, 使用prepare函数返回payload对象, 此时便能在reducer函数中使用

accountSlice.js

这里我复用了经典写法中的deposit函数,说明是完全兼容经典写法的

js 复制代码
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  balance: 0,
  loan: 0,
  loanPurpose: "",
  isLoading: false,
};

const accountSlice = createSlice({
  name: "account",
  initialState,
  reducers: {
    deposit(state, action) {
      state.balance += action.payload;
      state.isLoading = false;
    },
    withdraw(state, action) {
      state.balance -= action.payload;
    },
    requestLoan: {
      prepare(amount, purpose) {
        return {
          payload: { amount, purpose },
        };
      },
      reducer(state, action) {
        if (state.loan > 0) return;
        state.loan = action.payload.amount;
        state.loanPurpose = action.payload.purpose;
        state.balance += action.payload.amount;
      },
    },
    payLoan(state) {
      state.balance -= state.loan;
      state.loan = 0;
      state.loanPurpose = "action.payload.purpose";
    },
    convertingCurrency(state) {
      state.isLoading = true;
    },
  },
});

export const { withdraw, requestLoan, payLoan } = accountSlice.actions;

export function deposit(amount, currency) {
  if (currency === "USD") return { type: "account/deposit", payload: amount };

  return async function (dispatch, getState) {
    dispatch({ type: "account/convertingCurrency" });
    //API call
    const res = await fetch(
      `https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
    );
    const data = await res.json();
    const converted = data.rates.USD;

    dispatch({ type: "account/deposit", payload: converted });
  };
}

export default accountSlice.reducer;

createSlice.js

js 复制代码
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  fullName: "",
  nationalID: "",
  createdAt: "",
};

const customerSlice = createSlice({
  name: "customer",
  initialState,
  reducers: {
    createCustomer: {
      prepare(fullName, nationalID) {
        return {
          payload: {
            fullName,
            nationalID,
            createdAt: new Date().toISOString(),
          },
        };
      },
      reducer(state, action) {
        state.fullName = action.payload.fullName;
        state.nationalID = action.payload.nationalID;
        state.createdAt = action.payload.createdAt;
      },
    },
    updateName(state, action) {
      state.fullName = action.payload;
    },
  },
});

export const { createCustomer, updateName } = customerSlice.actions;

export default customerSlice.reducer;

store.js

redux-toolkit会自动设置thunk中间件和Devtools, 相比经典写法要简单许多

js 复制代码
import { configureStore } from "@reduxjs/toolkit";

import accountReducer from "./features/accounts/accountSlice";
import customerReducer from "./features/customers/customerSlice";

const store = configureStore({
  reducer: {
    account: accountReducer,
    customer: customerReducer,
  },
});

export default store;

使用和更新store中的状态

redux给我们提供了两个hooks用于使用和更新状态useSelectoruseDispatch

注意, 我们更新状态时, 不能直接传入action, 而是调用action creator函数返回的action

js 复制代码
import { useDispatch, useSelector } from "react-redux";
import { deposit, withdraw, requestLoan, payLoan } from "./accountSlice";

const dispatch = useDispatch();
//选择account(name属性)
const account = useSelector((store) => store.account);

function handlePayLoan() {
    if (account.loan === 0) return;
    //dispatch接收返回action的函数
    dispatch(payLoan());
}

案例完整代码已上传github,欢迎查阅

Redux的优势

相比于Context API + useReducer, Redux无需手动优化, 它是开箱即用的, Immer.js这个库会在幕后帮我们进行优化, 而使用Context我们需要去考虑优化问题(memouseMemouseCallback)

相关推荐
neter.asia3 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫3 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
光影少年22 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_24 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891126 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾27 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking27 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu29 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym34 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫35 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js