React技术栈之Redux高阶运用

前端之家收集整理的这篇文章主要介绍了React技术栈之Redux高阶运用前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

参考资料:《深入React技术栈》


高阶reducer

高阶函数是指将函数作为参数或返回值的函数,高阶reducer就是指将reducer作为参数或返回值的函数

在Redux架构中,reducer是一个纯函数,它的职责是根据prevIoUsState和action计算出新的state。在复杂应用中,Redux提供的combineReducers让我们可以把顶层的reducer拆分成多个小的reducer,分别独立地操作state树的不同部门。而在一个应用中,很多小粒度的reducer往往有很多重复的逻辑,那么对于这些reducer,如何抽取公共逻辑,减少代码冗余呢?这种情况下,使用高阶reducer是一种较好的解决方案。

reducer复用

我们将顶层的reduce拆分成多个小的reducer,肯定会碰到reducer复用问题。例如有A和B两个模块,它们的UI部分相似,此时可以通过配置不同的props来区别它们。那么这种情况下,A和B模块能不能共用一个reducer呢?答案是否定的。我们先来看一个简单reducer:

  1. const LOAD_DATA = 'LOAD_DATA';
  2. const initialState = { ... };
  3.  
  4. function loadData() {
  5. return {
  6. type: LOAD_DATA,...
  7. };
  8. }
  9.  
  10. function reducer(state = initialState,action) {
  11. switch(action.type) {
  12. case LOAD_DATA:
  13. return {
  14. ...state,data: action.payload
  15. };
  16. default:
  17. return state;
  18. }
  19. }

如果我们将这个reducer绑定到A和B两个不同模块,造成的问题将会是,当A模块调用loadData来分发相应的action时,A和B的reducer都会处理这个action,然后A和B的内容就完全一致了。

这里我们必需意识到,在一个应用中,不同模块间的actionType必须是全局唯一的。

因此,要解决actionType唯一的问题,还有一个方法就是通过添加前缀的方式来做到:

  1. function generateReducer(prefix,state) {
  2. const LOAD_DATA = prefix + 'LOAD_DATA';
  3. const initialState = { ...state,...};
  4. return function reducer(state = initialState,action) {
  5. switch(action.type) {
  6. case LOAD_DATA:
  7. return {
  8. ...state,data: action.payload
  9. };
  10. default:
  11. return state;
  12. }
  13. }
  14. }

这样只要A和B模块分别调用generateReducer来生成相应的reducer,就能解决reducer复用的问题了。而对于prefix,我们可以根据自己的项目结构来决定,例如${页面名称}_${模块名称}。只要能够保证全局唯一性,就可以写成一种前缀。

reducer增强

除了解决复用问题,高阶reducer的另一个重要作用就是对原始的reducer进行增强。redux-undo就是典型的利用高阶reducer来增强reducer的例子,它主要作用是使任意reducer变成可以执行撤销和重做的全新reducer。我们来看看它的核心代码实现:

  1. function undoable(reducer) {
  2. const initialState = {
  3. // 记录过去的state
  4. past: [],// 以一个空的action调用reducer来产生当前值的初始值
  5. present: reducer(undefined,{}),// 记录后续的state
  6. future: []
  7. };
  8. return function(state = initialState,action) {
  9. const { past,present,future } = state;
  10. switch(action.type) {
  11. case '@@redux-undo/UNDO':
  12. const prevIoUs = past[past.length - 1];
  13. const newPast = past.slice(0,past.length - 1);
  14. return {
  15. past: newPast,present: prevIoUs,future: [ present,...future ]
  16. };
  17. case '@@redux-undo/REDO':
  18. const next = future[0];
  19. const newFuture = future.slice(1);
  20. return {
  21. past: [ ...past,present ],present: next,future: newFuture
  22. };
  23. default:
  24. // 将其他action委托给原始的reducer处理
  25. const newPresent = reducer(present,action);
  26. if(present === newPresent) {
  27. return state;
  28. }
  29. return {
  30. past: [ ...past,present: newPresent,future: []
  31. };
  32. }
  33. };
  34. }

有了这高阶reducer,就可以对任意一个reducer进行封装:

  1. import { createStore } from 'redux';
  2.  
  3. function todos(state = [],action) {
  4. switch(action.type) {
  5. case: 'ADD_TODO':
  6. // ...
  7. }
  8. }
  9.  
  10. const undoableTodos = undoable(todos);
  11. const store = createStore(undoableTodos);
  12.  
  13. store.dispatch({
  14. type: 'ADD_TODO',text: 'Use Redux'
  15. });
  16.  
  17. store.dispatch({
  18. type: 'ADD_TODO',text: 'Implement Undo'
  19. });
  20.  
  21. store.dispatch({
  22. type: '@@redux-undo/UNDO'
  23. });

查看高阶reducer undoable的实现代码可以发现,高阶reducer主要通过下面3点来增强reducer:

  • 能够处理额外的action;

  • 能够维护更多的state;

  • 将不能处理的action委托给原始reducer处理。

Redux与表单

React单向绑定的特性极大地提升了应用的执行效率,但是相比于简单易用的双向绑定,单向绑定在处理表单等交互的时候着实有些力不从心。具体到React应用中,单向绑定意味着你需要手动给每个表单控件提供onChange回调函数,同时需要将它们的状态初始化在this.state中。不仅如此,一个体验友好的表单还需要有明确的错误状态和错误信息,甚至某些输入项还需要异步校验功能。也就是说,表单里的一个有效字段至少需要2~3个本地状态。

在Angular.js中,表单相关的问题在框架层面已经得到了很好的解决。那么,对于React+Redux应用,有没有什么好的方案呢?

下面我们从两个层面来解答这个问题:对于简单的表单应用,为了减少重复冗余的代码,可以使用redux-form-utils这个工具库,它能利用高阶组件的特性为表单的每个字段提供value和onChange等必须值,而无需你手动创建;对于复杂的表单,则可以利用redux-form。虽然同样基于高阶组件的原理,但如果说redux-form-utils是一把水果刀的话,那么redux-form就是一把多功能的瑞士军刀。除了提供表单必须的字段外,redux-form还能实现表单同步验证、异步验证甚至嵌套表单等复杂功能

使用redux-form-utils减少创建表单的冗余代码

了解redux-form-utils之前,先来看看如何使用原生React处理表单:

  1. import React,{ Component } from 'react';
  2.  
  3. class Form extends Component {
  4. constructor(props) {
  5. super(props);
  6. this.handleChangeAddress = this.handleChangeAddress.bind(this);
  7. this.handleChangeGender = this.handleChangeGender.bind(this);
  8. this.state = {
  9. name: '',address: '',gender: ''
  10. };
  11. }
  12. handleChangeName(e) {
  13. this.setState({
  14. name: e.target.value
  15. });
  16. }
  17. handleChangeAddress(e) {
  18. this.setState({
  19. address: e.target.value
  20. });
  21. }
  22. handleChangeGender(e) {
  23. this.setState({
  24. gender: e.target.value
  25. });
  26. }
  27. render() {
  28. const { name,address,gender } = this.state;
  29. return (
  30. <form className="form">
  31. <input name="name" value={name} onChange={this.handleChangeName} />
  32. <input name="address" value={address} onChange={this.handleChangeAddress} />
  33. <select name="gender" value={gender} onChange={this.handleChangeGender}>
  34. <option value="male" />
  35. <option value="female" />
  36. </select>
  37. </form>
  38. );
  39. };
  40. }

可以看到,虽然我们的表单里只有3个字段,但是已经有非常多的冗余代码。如果还需要加上验证等功能,那么这个表单对应的处理代码将会更加膨胀。

仔细分析表单的代码实现,我们发现几乎所有的onChange处理器逻辑都很类似,只是需要改变表单字段即可。对于某些复杂的输入控件,比如自己封装了一个TimePicker组件,也许回调名称不是onChange,而是onSelect。同样,onSelect回调里提供的参数也许并不是React的合成事件,而是一个具体的值。通过分析表单控件可能的输入和输出,我们将通过使用redux-form-utils减少Redux处理表单应用时的冗余代码

  1. // components/MyForm.js
  2. import React,{ Component } from 'react';
  3. import { createForm } from 'redux-form-utils';
  4.  
  5. @createForm({
  6. form: 'my-form',fields: ['name','address','gender']
  7. })
  8.  
  9. class Form extends Component {
  10. render(){
  11. const { name,gender } = this.props.fields;
  12. return (
  13. <form className="form">
  14. <input name="name" value={...name} />
  15. <input name="address" value={...address} />
  16. <select {...gender}>
  17. <option value="male" />
  18. <option value="female" />
  19. </select>
  20. </form>
  21. );
  22. }
  23. }

可以看到,实现同样功能的表单,代码量减少了近一半以上。

redux-form-utils提供了两个方便的工具函数---createForm(config)和bindRedux(config),前者可以当作decorate使用,传入表单的配置,自动为被装饰的组件添加表单相关的props;而后者可以生成与Redux应用相关的reducer、initialState和actionCreator等。

下面先看看如何在reducer里整合redux-form-utils:

  1. // reducer/MyForm.js
  2. import { bindRedux } from 'redux-form-utils';
  3.  
  4. const { state: formState,reducer: formReducer } = bindRedux({
  5. form: 'my-form','gender'],});
  6.  
  7. const initialState = {
  8. foo: 1,bar: 2,...formState
  9. };
  10.  
  11. function myReducer(state = initialState,action) {
  12. switch(action.type) {
  13. case 'MY_ACTION': {
  14. // ...
  15. }
  16. default:
  17. return formReducer(state,action);
  18. }
  19. }

我们把同样的配置传给bindRedux方法,并获得这个表单对应的reducer和初始状态formState,并将这些内容整合在reducer中。

完成createForm和bindRedux这两个函数后,一个基于Redux的表单应用就完成了。为了后续修改表单更加灵活,建议将配置文件单独保存,并分别在组件和reducer中引入对应的配置文件

使用redux-form完成表单异步验证

redux-form-utils为我们提供了实现表单最基本的功能,但是为了填写表单的体验更加友好,在把数据提交到服务端之前,我们应该做一些基本的表单校验,比如填写字段不能为空等。要实现校验等复杂的表单功能,需要用到redux-form。

在使用和配置方面,redux-form与redux-form-utils没有太多的差异,唯一不同的是redux-form需要在Redux应用的state树中挂载一个独立的节点。这意味着,所有使用redux-form创建的表单中的字段都会在一个固定的位置,如state.form.myForm或state.form.myOtherForm均挂载在state.form下:

  1. import { createStore,combineReducers } from 'redux';
  2. import { reducer as formReducer } from 'redux-form';
  3.  
  4. const reducers = {
  5. // 其他的reducer...
  6. // 所有表单相关的reducer挂载在form下
  7. form: formReducer
  8. };
  9.  
  10. const reducer = combineReducers(reducers);
  11. const store = createStore(reducer);

完成了基本的配置后,现在看看redux-form如何帮我们完成表单验证功能

  1. import React,{ Component } from 'react';
  2. import { reduxForm } from 'redux-form';
  3.  
  4. function validate(values) {
  5. if(values.name == null || values.name === '') {
  6. return {
  7. name: '请填写名称'
  8. };
  9. }
  10. }
  11.  
  12. @reduxForm({
  13. form: 'my-form',validate
  14. });
  15.  
  16. class Form extends Component {
  17. render(){
  18. const { name,gender } = this.props.fields;
  19. return (
  20. <form className="form">
  21. <input name="name" value={...name} />
  22. { name.error && <span>{name.error}</span> }
  23. <input name="address" value={...address} />
  24. <select {...gender}>
  25. <option value="male" />
  26. <option value="female" />
  27. </select>
  28. <button type="submit">提交</button>
  29. </form>
  30. );
  31. }
  32. }

在上面的表单中,我们在提交时对name字段做了非空验证,而在Form组件的render方法中,同时添加显示相应错误的逻辑。触发验证、重新渲染、表单纯洁性判断等过程,均被redux-form进行了封装,对使用者透明。

可以看到,使用redux-form校验表单十分简单易用,从很大程度上填补了Redux应用在框架层面处理表单应用的不足。


参考资料:《深入React技术栈》

猜你在找的React相关文章