React具有着巨大的生态,衍生了许多的状况办理方案。React的状况能够依照两种方式去分类:

可访问性

  • 部分状况(local state
  • 大局状况(global state

  • 长途状况(remote state
  • 界面状况(UI state

咱们要根据不同的状况选用适宜的办理方案,以到达最佳实践。

React进阶——状况办理的运用与实践

部分状况办理

useState

useState是咱们最常用的hooks之一,它回来一个数组,包括状况和设置状况的函数

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)

React进阶——状况办理的运用与实践

望文生义,useReducer会将操作state的行为合并,减少操作state的次数

React进阶——状况办理的运用与实践

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

React进阶——状况办理的运用与实践

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

useState VS. useReducer

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

怎么选择?

React进阶——状况办理的运用与实践

大局状况办理

Context API

Context API的帮助下, 体系在整个运用程序中传递数据,而无需手动在组件树中传递props, 它答应咱们向整个运用“播送”大局状况

  • Provider:赋予一切子组件访问value的权限
  • value:咱们想要提供的数据(通常是状况和函数)
  • Consumers: 读取运用Provider的上下文值的一切组件

React进阶——状况办理的运用与实践

如图所示: value的每一次更新都会导致一切Consumers的从头渲染

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

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目标即可

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

React进阶——状况办理的运用与实践

Redux

React进阶——状况办理的运用与实践

能够看到,Redux比较useReducer的机制非常相似,只是增加了action creator函数(用于处理副作用),然后将多个reducer会集办理,使状况更新逻辑与运用程序的其余部分分离。

Redux中间件——thunk

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

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

React进阶——状况办理的运用与实践


现在Redux有着两种主流写法, 官方推荐运用Redux toolkit, 这儿咱们从一个银行事例中分析比照两种写法:

需求:

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

Redux经典写法

依靠安装:

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本身
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

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

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组件运用即可

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

依靠安装:

npm i @reduxjs/toolkit

运用Redux tookit无需再写action creator, 会主动生成在xxxSlice的actions特点中

createSlice需要传递name特点,初始值和包括reducer函数的reducers目标

默许状况下,reducer函数的action.payload只承受一个参数, 假如要传递多个参数, 需要写成目标的方式, 运用prepare函数回来payload目标, 此刻便能在reducer函数中运用

accountSlice.js

这儿我复用了经典写法中的deposit函数,说明是彻底兼容经典写法的

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

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, 比较经典写法要简略许多

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

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)