Redux基础

Redux+typescript是官方也推荐的方式,此系列也将结合起来介绍。

安装

使用create-react-app安装并选装redux+typescript:

yarn create react-app redux-ts-demo --template redux-typescript
npm init react-app redux-ts-demo --template redux-typescript
npx create-react-app redux-ts-demo --template redux-typescript

初学时可以先忽略工程中原官方案例代码。在src/index.ts内完成基本内容的探索。

redux的核心

stateactionreducerdispatchProvidersubscribe。其中Providerreact-redux包中提供的组件,用于连接React组件Redux Store的通信。

reducer可以理解为事件管理者,dispathch则是事件通知者,action是触发的动作,state是状态的容器。这样看起来,reducer是一个核心成员,关于如何处理state的逻辑都在这里完成。

import React from 'react';
import {createStore} from 'redux';
import {Provider} from 'react-redux';

type Action = { type: "increment" }

// 1. 定义处理和响应"事件"的管理中心函数
function reducers(state = 0 /** 初始化 state 的值 */, action: Action) {
  const {type} = action;
  switch (type) {
    case "increment": 
      return state + 1;
    case "decrement": 
      return state -1;
    default: 
      return state;
  }
}

// 2. 创建 store
const store = createStore(reducers);

// 3. 获取state状态
console.log(store.getState());

// 4. 订阅 state 的改变
store.subscribe(() => {
  render();
})

const App: React.FC = () => {
  return (
    <>
      <p>{store.getState()}</p>
      <button onClick={() => store.dispatch({
        type: "increment",
      })}>increment</button>
      <button onClick={() => store.dispatch({
        type: "decrement"
      })}>decrement</button>
    </>
  )
}

// 5. 渲染UI
render();

function render() {
  ReactDOM.render(
    // 根组件连接,并关联 store 
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById("root")
  )
}

此时的组件还不具有自动更新UI的能力,因此需要我们通过render函数并在subscribe监听到state发生变化时手动更新UI。

以上即为Redux运行的主要过程。

createStore在创建store时可以传入第二个参数,用于初始化reducers函数参数1state的值。当在createStore中传入初始值后,reducers内state的初始值就失效了!

拆分Reducers

上面的例子state只有一个number的状态值,通常的应用会有多个数据状态需要保存,而在拥有多个全局状态时,把处理逻辑都放到一个reducer中,那么在工程不断拓展后会使整个文件变得异常庞大使得难以阅读和维护!此时就需要将各个独立的状态操作放到一个单独的reducer中进行维护,通常会在项目根目录创建一个reducers目录来进行管理。

创建userReducer:

// src/reducers/user.ts
import { AnyAction } from "redux";

export type UserState = {
  username: string,
  gender: number,
  age: number
}

export type UserAction = {type: "login" ,user: UserState}

const initUserState: UserState = {
  username: '',
  gender: 0,
  age: 0
}

const userReducer = (state: UserState = initUserState, action: AnyAction) => {
  const {type, user} = action as UserAction;
  switch(type) {
    case "login": 
      return {...state, ...user};
    default: 
      return state;
  }
}

export default userReducer;

创建postReducer:

// src/reducers/post.ts
import { AnyAction } from "redux";

export type PostState = {
  id: string,
  title: string,
  content: string 
}

export type PostAction = {type: "add_post" , post: PostState}

const initPostState: PostState = {
  id: '',
  title: '',
  content: ''
}

const postReducer = (state: PostState = initPostState, action: AnyAction) => {
  const {type, post} = action as PostAction;
  switch(type) {
    case "add_post": 
      return {...state, ...post};
    default: 
      return state;
  }
}

export default postReducer;

把子reducer合并到一个文件中:

// src/reducers/index.ts
import { post, PostState, PostAction } from "reducers/post";
import { user, UserAction, UserState } from "reducers/user";
import { combineReducers } from "redux";

export type RootAction = UserState | PostAction;

export type RootState = {
  user: UserState,
  post: PostState
};

const initState: RootState = {
  user: {
    username: '',
    gender: 0,
    age: 0
  },
  post: {
    id: '',
    title: '',
    content: ''
  }
};

export default function rootReducers(
  state: RootState = initState,
  action: RootAction
) {
  return {
    user: user(state.user, action),
    post: post(state.post, action)
  };
}

export const combineReducer = combineReducers({
  user,
  post,
});

在子reducer中,需导出当前reducerstateaction的类型,以供合并文件reduders内使用。并且子reducer函数参数2的action类型需设置为anyActionopen in new window然后在使用时将其强转为本reducer的action的类型即可。

你可能会好奇为何不直接引入RootAction来作为子reducer action的类型,原因如下:

  1. 为了使dispatch时可以明确载荷类型,不想所有的action除了type外都使用payload来作为载荷的键名!(如何你想这么做也不是不可以)。如果用RootAction那么将必须统一action的类型,比如是:{type: string, payload: any}

  2. 将子reducer函数参数action设置为anyAction后,通过类型强转(as)可以将action的类型重新圈定回当前reducer中,在文件内编写代码时可以得到只包含当前reducer的类型提示。要是使用RootAction,那么类型就不再纯粹,将包含其他文件的action内容。

初始化reducer

细致的你可能已经注意到每个子reducer函数参数1state都被赋予了默认值,你是否疑问这是必须的吗?是的,这是必须的!

请为reducer赋一个初始值! 这非常重要,这在你使用combineReducers时是必须的!未进行初始化赋值,那么你的state将有可能是undefined,redux将会提示你这个错误,告诉你须提供一个初始值:

Error: The slice reducer for key "post" returned undefined during initialization.
If the state passed to the reducer is undefined, you must explicitly return the 
initial state. The initial state may not be undefined. If you don't want to set a 
value for this reducer, you can use null instead of undefined.

combineReducers是一个辅助函数,帮助我们简化手动书写整合reducers的代码,当我们不使用combineReducers时,可以忽略在子reducer中初始化state,但也必须给rootReducers提供一个初始state。与其在index.ts中写,不如在子reducer中直接赋初始值更好,不仅自己管理自己的状态,还可以简化index中的代码!

每个应用程序只有一个 Redux Store

每个应用程序只有一个 Redux Storeopen in new window

通常的应用整个组件树都只需一个store,也仅是在<App />组件外用<Provider>进行包裹。建议是不要将createStore创建出的store进行导出,多处引入store将可能导致行为不可预测。为避免错误的发生,可以只在项目src/index.tsx内使用createStore

使用ReduxDevTools监控状态改变

像VueDevTools,在浏览器安装完成后即可监听Vuex的改变。但react+redux则不仅需要react devtools,还需要redux devtools协作配合状态的监听:

  1. 安装浏览器ReduxDevToolsopen in new window插件;

  2. npm安装redux-devtools-extensionopen in new window yarn add redux-devtools-extension -D;

提示

如果不需要一些高级配置的话,直接在创建store时,这样做即可。

 const store = createStore(
   reducer, /* preloadedState, */
   window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 );

写这么长的代码有些繁琐,如果涉及一些配置的话就更麻烦了,此时可以利用安装的redux-devtools-extension插件提供的函数进行配置。

如果不需要配置中间件(middleware)或增强器(enhancers)的话,可以仅做这样的配置达成浏览器插件的状态监听。

import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension';

const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer({});

如果想要进行代码追踪的话,可以配置成这样:devToolsEnhancer({trace: true})

追踪action调用

图片引用自:https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Features/Trace.mdopen in new window

提示

如果使用RTK(Redux ToolsKit),已默认进行了一些配置,无需我们再手动配置!详阅RTK DevToolsopen in new window

更多配置请参考: redux-devtools-extensionopen in new windowRedux DevToolsopen in new window

最近更新:
Contributors: untilthecore