5

在 React 中使用 Redux 的四种写法

 2 years ago
source link: https://developer.51cto.com/article/711255.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
244a844707410c9339e98204f718d7a81d74bc.png

大家好,我是前端西瓜哥。今天我们看看React 中使用 Redux 的 4 种写法。

Redux 是一种状态容器 JS 库,提供可预测的状态管理,经常和 React 配合来管理应用的全局状态,进行响应式组件更新。

Redux 一般来说并不是必须的,只有在项目比较复杂的时候,比如多个分散在不同地方的组件使用同一个状态。对于这种情况,如果通过 props 层层传递,代码会变得不可维护,这时候我们可以考虑使用 Redux 这类状态管理库。

不使用 Redux 的写法

我们创建一个 User 组件,显示用户名,并支持设置用户名。先看看不使用 Redux 的写法。

import { Component, createRef } from 'react';
class User extends Component {
  state = { username: '前端西瓜哥' };
  inputRef = createRef();
  setUsername = () => {
    this.setState({ username: this.inputRef.current.value });
  };
  render() {
    return (
      <div>
        <div>用户名: {this.state.username}</div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.setUsername}>设置用户名</button>
      </div>
    );
  }
}
export default User;

下面我们改造一下这个组件,将状态迁移到 Redux 里。

最底层的写法

Redux 是和框架无关的,我们先看看只用 Redux 库的写法。

demo:https://codesandbox.io/s/redux-plain-demo-bc4kv0。

首先我们创建一个 reducer。

// user_reducer.js
import { SET_USERNAME } from './constants';
// 初始值
const defaultState = {
  name: '前端西瓜哥',
  age: 88
};
// 用于修改 user 状态的 reducer
export const userReducer = (preState = defaultState, action) => {
  switch (action.type) {
    case SET_USERNAME: // type 值都统一放到 constants
      return { ...preState, name: action.payload };
    // 这里还可以根据需要,添加类似 setUserAga 等逻辑
    default:
      return preState;
  }
};
// constants.js
export const SET_USERNAME = 'SET_USERNAME';

reducer 是一个用于更新状态的函数,接收原来的状态 preState 和一个更新动作对象 action。

action 对象有一个 表示此次操作的描述 type 和 其他数据属性(通常为 payload)。payload 会以某种方式去计算出一个新的状态,替换掉 redux 中原来的 state。

{
 type: 'SET_USERNAME',
 payload: '新用户名'
}

type 通常是一个字符串,比如我们会用 'COUNT_INCREMENT' 来给一个计数器加一,或用 'SET_USERNAME' 来更新用户名。reducer 会根据不同的 type 来执行不同的更新 state 行为。

action 的构造我们通常会用一个函数帮忙构建,这种函数称为 Action Creator:

// user_action.js
import { SET_USERNAME } from './constants';
export const setUsernameAction = (data) => {
  return {
    type: SET_USERNAME,
    payload: data
  };
};

有了 reducer,我们可以用它们来构建我们的 store。store 可以访问所有的保存在 redux 状态:

import { combineReducers, createStore } from 'redux';
import { userReducer } from './user_reducer';
const store = createStore(
  combineReducers({
    user: userReducer
  })
);
export default store;

combineReducers 可以将多个 reducer 组合在一起,有各自对应的属性名。比如上面的代码,我们可以通过 store.getState().user 来拿到用户对象。

如果你又新增了 counter 状态对象,只需再加上 counter: counterReducer,就可以用 store.getState().counter 来拿到这个对象。

createStore 用于创建应用中所有的 state,然后这些 state 都会存放到这个被返回的 store 里。

现在我们的 User 组件就变成这样了:

import { Component, createRef } from 'react';
import store from '../store/store';
import { setUsernameAction } from '../store/user_action';
class User extends Component {
  inputRef = createRef();
  componentDidMount() {
    store.subscribe(() => {
      this.setState({});
    });
  }
  setUsername = () => {
    store.dispatch(setUsernameAction(this.inputRef.current.value));
  };
  render() {
    return (
      <div>
        <div>用户名: {store.getState().user.name}</div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.setUsername}>设置用户名</button>
      </div>
    );
  }
}
export default User;
  • store.getState()可以拿到 state 对象,通过它,我们获取到其下我们需要的对象,比如 user 对象。
  • store.dispatch(action)派发 action 对象,触发状态的更新。
  • store.subscribe(fn)订阅状态的变化,执行回调函数。这里我们一发现状态发生了变化,就立刻重新渲染组件。

Redux 本质是发布订阅模式,状态集中在一起,状态可以通过 store.getState() 访问,通过 store.dispatch(action) 改变状态,通过 store.subscribe(fn) 订阅状态变化(React 组件监听到变化后,重新渲染组件)。

这种写法是最原始的写法,可以用在任何框架中。

缺点很明显:用到 redux 的组件要订阅 state 变化,一变化就重新渲染组件。​有时候其他组件的 state 变化了,当前组件也会进行不必要的重新渲染。

自己去判断吧,又太繁琐,容易写错,也容易忘记订阅。对于忘记订阅的问题,我们也可以直接把让根组件来监听和重新渲染,但这样性能很差。

接下来西瓜哥要讲的 React-Redux 库可以解决这个问题。它能够在当前组件用到的特定 state 发生改变时,才重新渲染组件。

React-Redux

发现大家都很喜欢在 React 里用 Redux,于是 Facebook 出了一个 React-Redux 库,让大家能够更好更正确地在 React 中使用 Redux。

React-Redux 配合 connect 高阶组件

我们先看看使用 connect 的写法。

demo:https://codesandbox.io/s/react-redux-with-connect-demo-kfvxt6。

React-Redux 引入了一个容器组件的概念,这个组件专门负责和 redux 打交道 。容器组件其实是一个高阶组件,将真正的 UI 组件做一个封装,在上面做了以下工作:

  • 将 state 和 dispatch 映射到 props,注入到 UI 组件中。
  • 监听 state 变化,必要时重新渲染 UI 组件。

高阶组件:一个函数,它会接收组件参数,然后返回一个新的组件。高阶组件的作用是对真正的 UI 组件做一些复用的逻辑的封装,通常用于做功能增强。

随着 React Hooks 愈发流行,大家现在更喜欢用 React Hooks 来取代高阶函数,写法更优雅。

const ContainerComponent = connect(
  mapStateToProps,
  mapDispatchToProps,
)(UIComponent);

现在开始改造项目。

我们创建一个 container 文件夹,里面放上 User.jsx 文件,里面写上如下内容:

// containers/User.jsx
import { connect } from 'react-redux';
import UserUI from '../components/User';
import { setUsernameAction } from '../store/user_action';
export default connect(
  // mapStateToProps
  (state) => ({ user: state.user }),
  // mapDispatchToProps
  (dispatch) => ({
    setUsername: (newName) => dispatch(setUsernameAction(newName))
  })
)(UserUI);

然后记得在使用该容器的地方,传入我们的 store 对象,如下:

import UserContainer from './containers/User';
import store from './store/store';
import './styles.css';
export default function App() {
  return <UserContainer store={store} />;
}

当然每个容器组件都要传入 store 未免太麻烦,我们通常会使用另一种做法:使用 redux-react 提供的 Context Provider,包裹住根组件,如下:

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

然后是 UI 组件的改造:

import { Component, createRef } from 'react';
class User extends Component {
  inputRef = createRef();
  render() {
    return (
      <div>
        <div>用户名: {this.props.user.name}</div>
        <input ref={this.inputRef} type="text" />
        <button
          onClick={() => this.props.setUsername(this.inputRef.current.value)}
        >
          设置用户名
        </button>
      </div>
    );
  }
}
export default User;

UI 组件的 props 会拿到 user 对象、setUsername 方法以及我们注入的 store 对象(如果用 Context 的方式则取不到)。

在 React 中使用 Redux 的 4 种写法

​使用了 connect 后,只有组件用到的 state 改变了,才会触发组件的更新。

这里有个需要特别注意的地方,就是你要 保证新的状态对象和旧状态不相等,这样才能触发组件重新渲染,这在处理对象方法时容易出错。你需要拷贝一个新的对象作为新的状态,推荐使用扩展运算符的写法。

// user_reducer.js
// 错误的写法,新的 state 依旧指向原来的对象
preState.name = action.payload;
return preState;
// 正确的写法
return { ...preState, name: action.payload };

或者可以考虑使用 immer 这种不可变数据结构库。

React-Rudex 配合 React Hooks

前面我们用了 connect 这么一个高阶组件,是为了给 UI 组件增强功能。

说到增强功能,react-redux 也提供了现在非常流行的 React Hooks 写法,写起来更优雅,也是目前西瓜哥我所在公司的做法。

这里我们就不需要 connect 高阶组件了,也就是说不需要容器组件。

demo:https://codesandbox.io/s/react-redux-with-hooks-demo-8ixl0h。

// User.js
import { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setUsernameAction } from '../store/user_action';
const User = () => {
  // 获取状态
  const user = useSelector((state) => state.user);
  // 获取 dipatch 方法
  const dipatch = useDispatch();
  const inputRef = useRef(null);
  return (
    <div>
      <div>用户名: {user.name}</div>
      <input ref={inputRef} type="text" />
      <button
        onClick={() => {
          const newName = inputRef.current.value;
          dipatch(setUsernameAction(newName));
        }}
      >
        设置用户名
      </button>
    </div>
  );
};
export default User;

通过 useSelector 我们可以拿到通过上下文绑定的 state,然后从中获取我们需要用到的状态。

const user = useSelector((state) => state.user);

如果有多个,我们可以写成对象的形式:

const { user, counter } = useSelector((state) => ({
  user: state.user,
  counter: state.counter
}));

是不是有点像 connect 的 mapStateToProps。

然后是获取 dispatch 方法:

const dipatch = useDispatch();

hook 非常优雅,但我也发现,相比 connect 写法,我们的 redux 状态逻辑和组件耦合在一起了。不过一般我们的组件都是业务组件,还是可以接受的。

Redux Toolkit

我们可以看到,我们要维护一个状态,我们要写 reducer 方法、action creator 方法,还要用一个 contants.js 文件集中式管理所有的 actionType 字符串。

​你发现你写了非常多的 模板代码,每加一个 state 就要创建上面这些东西,各个文件里跑来跑去,人都麻了。

于是 redux 又出了一个工具集库 Redux Toolkit,来解决这个问题。

demo:https://codesandbox.io/s/redux-toolkit-demo-8x0391

Redux Toolkit 提供了 createSlice 方法,可以帮你用更少的代码生成配套的 reducer 和 action,而且有很好的可维护性。

// userSlice.js
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
  name: 'user',
  initialState: {
    name: '前端西瓜哥',
    age: 88
  },
  reducers: {
    setUsername: (state, action) => {
      // 因为 Redux Toolkit 内置使用了 immer,所以可以直接改。
      state.name = action.payload;
    }
  }
});
// actions
export const { setUsername } = userSlice.actions;
// 获取自己需要的 state,用在组件的 userSeletor hook 上。
export const selectUser = (state) => state.user;
// reducer
export default userSlice.reducer;

createSlice 传入 name(标识符,生成 actions 要用到)、initialState(初始值)、reducers(变成了对象形式)参数,然后返回一个对象。

这个返回的 slice 对象有 actions 对象属性,比如上面的代码,actions 下有一个 setUsername 的方法,执行后会返回 {type: "user/setUsername", payload: "新名字"} 。

可以看到 action 的 type 是根据 name 和 reducers 的属性生产的,确保唯一性。

slice 还有一个 reducer 对象,其实就是将前面传入的 reducers 配合自动生成的 action 转换为了函数的形式。

createSlice 干了什么事?createSlice 将原来管理一个状态但代码却是分离的 action 和 reducer 集中在了一起,不用自己去起 actionType 的名字。

然后是生成 store 也要改成 configureStore 的写法:

// store.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
const store = configureStore({
  reducer: {
    user: userReducer
  }
});
export default store;

简单总结一下:

  • 原始写法,过于简陋,需要自己通过 store.subscribe(fn)来判断一个组件是否要重新渲染,写起来麻烦、性能堪忧。
  • 配合 Redux React 库,通过 connect 来注入 redux 状态,要多写一个 connect 高阶组件生成的容器组件,但降低了耦合度。Redux React 会只在组件需要的状态改变时,重新渲染组件。这里要注意改变时,新旧状态不能相同,尤其是对象的情况,否则重新渲染不会工作。
  • 如果你的项目主要使用函数组件,可以不用 connect,直接用 useSelector 来获取状态,以及用 userDispatch 来改变状态。非常优雅。
  • Redux 又推出了 Redux Toolkit,解决了配置复杂、需要写太多模板、需要手动安装大量相关包的问题。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK