React新特性-Hooks之useReducer(九)

useReducer是React提供的一个高级Hook,它不像useEffectuseStateuseRef等必须的hook一样,没有useReducer我们也可以正常完成需求的开发,但useReducer可以使我们的代码具有更好的可读性、可维护性、可预测性。

系列目录

什么是reducer

用过redux的人来说,reducer对于大家来说一点都不陌生。简单来说reducer是一个函数(state, action) => newState:接收当前应用的state和触发的动作action,计算并返回最新的state

下面是一段伪代码:

// 举个栗子 计算器reducer,根据state(当前状态)和action(触发的动作加、减)参数,计算返回newState(新的状态)
function countReducer(state, action) {
    switch(action.type) {
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
        default:
            return state;
    }
}

state是一个number类型的数值,reducer根据action的类型(加、减)对应的修改state,并返回最终的新的state

从上面的示例可以看到reducer本质是一个纯函数,没有任何UI和副作用。这意味着相同的输入(stateaction),1函数无论执行多少遍始终会返回相同的输出(newState)。因此通过reducer函数很容易推测state的变化,并且也更加容易单元测试。

这里纯函数和副作用(React新特性-Hooks之useEffect和useLayoutEffect(五))这边文件有介绍到。

//单元测试 伪代码
expect(countReducer(1, { type: 'add' })).equal(2); // 成功
expect(countReducer(1, { type: 'add' })).equal(2); // 成功
expect(countReducer(1, { type: 'sub' })).equal(0); // 成功

reducer函数的返回值

在上面的例子中state是一个基础数据类型,但很多时候state可能会是一个复杂的JavaScript对象,如上例中count有可能只是state中的一个属性。针对这种场景我们可以使用ES6的结构赋值:

// 返回一个 newState (newObject)
function countReducer(state, action) {
    switch(action.type) {
        case 'add':
            return { ...state, count: state.count + 1; }
        case 'sub':
            return { ...state, count: state.count - 1; }
        default:
            return count;
    }
}

关于上面这段代码有两个重要的点需要我们记住:

  1. reducer处理的state对象必须是不可变得数据,这意味着永远不要直接修改参数中的state对象,reducer函数应该每次都返回一个新的state object。就和setState({state:newState})修改复杂的state数据每次生成一个新的state对象来进行变更旧的state
  2. 既然reducer要求每次都返回一个新的对象,我们可以使用ES6中的解构赋值方式去创建一个新对象,并复写我们需要改变的state属性,如上例。

这样做是为了更加可预测、可测试。如果每次返回同一个state,就无法保证无论执行多少次都是相同的结果

action理解

action:用来表示触发的行为。

  1. 用type来表示具体的行为类型(登录、登出、添加用户、删除用户等)
  2. 用payload携带的数据(如增加书籍,可以携带具体的book信息),下例:
const action = {
    type: 'addBook',
    payload: {
        book: {
            bookId,
            bookName,
            author,
        }
    }
}
function bookReducer(state, action) {
    switch(action.type) {
        // 添加一本书
        case 'addBook':
            const { book } = action.payload;
            const books:{
               ...state.books,
              [book.bookId]: book,
            }
            return {
                ...state,
                books,
            };
        case 'sub':
        default:
            return state;
    }
}

声明一个action动作,action的类型是添书籍(addBook),payload中存放新书籍的资料信息,在switch中根据actiontype来执行什么操作,在addBook动作中将新增加的书籍放入到整个书籍列表books中,然后返回新增过后的书籍。

如何使用useReducer

React Hook功能正式发布之后,允许在函数组件中拥有state和副作用(useEffect)。官方提供了两种state管理的hook:useStateuseReducer。下面我们会通过一系列Demo逐步说明如何使用useReducer管理state

useState版login

我们先看看登录页常规的使用useState的实现方式:

  function LoginPage() {
      const [name, setName] = useState(''); // 用户名
      const [pwd, setPwd] = useState(''); // 密码
      const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中
      const [error, setError] = useState(''); // 错误信息
      const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录

      const login = (event) => {
          event.preventDefault();
          setError('');
          setIsLoading(true);
          login({ name, pwd })
              .then(() => {
                  setIsLoggedIn(true);
                  setIsLoading(false);
              })
              .catch((error) => {
                  // 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
                  setError(error.message);
                  setName('');
                  setPwd('');
                  setIsLoading(false);
              });
      }
      return (
          //  返回页面JSX Element
      )
  }

上面Demo我们定义了5个state来描述页面的状态,在login函数中当登录成功、失败时进行了一系列复杂的state设置。可以想象随着需求越来越复杂更多的state加入到页面,更多的setState分散在各处,很容易设置错误或者遗漏,维护这样的老代码更是一个噩梦。

useReducer版login

下面看看如何使用useReducer改造这段代码,先简单介绍下useReducer。

const [state, dispatch] = useReducer(reducer, initState);

第一个参数:reducer函数,没错就是我们上面介绍的。

第二个参数:初始化的state。返回值为最新的statedispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer

// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {count: 0};

// 第二个参数:state的reducer处理函数
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

function Counter() {
    // 返回值:最新的state和dispatch函数
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
          // useReducer会根据dispatch的action,返回最终的state,并触发rerender
          Count: {state.count}
          // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );
}

了解了useReducer基本使用方法后,看看如何使用useReducer改造上面的login demo:


const initState = {
    name: '',
    pwd: '',
    isLoading: false,
    error: '',
    isLoggedIn: false,
}

function loginReducer(state, action) {
    switch(action.type) {
        case 'login':
            return {
                ...state,
                isLoading: true,
                error: '',
            }
        case 'success':
            return {
                ...state,
                isLoggedIn: true,
                isLoading: false,
            }
        case 'error':
            return {
                ...state,
                error: action.payload.error,
                name: '',
                pwd: '',
                isLoading: false,
            }
        default:
            return state;
    }
}
function LoginPage() {
    const [state, dispatch] = useReducer(loginReducer, initState);
    const { name, pwd, isLoading, error, isLoggedIn } = state;
    const login = (event) => {
        event.preventDefault();
        dispatch({ type: 'login' });
        login({ name, pwd })
            .then(() => {
                dispatch({ type: 'success' });
            })
            .catch((error) => {
                dispatch({
                    type: 'error'
                    payload: { error: error.message }
                });
            });
    }
    return (
        //  返回页面JSX Element
    )
}

乍一看useReducer改造后的代码反而更长了,但很明显第二版有更好的可读性,我们也能更清晰的了解state的变化逻辑。

可以看到login函数现在更清晰的表达了用户的意图,开始登录login、登录success、登录errorLoginPage不需要关心如何处理这几种行为,那是loginReducer需要关心的,表现和业务分离。

另一个好处是所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码,比如在其他函数中也需要触发登录error状态,只需要dispatch({ type: 'error' })

useReducer可以让我们将what(在哪里做)和how(怎么做)分开。比如点击了登录按钮,我们要做的就是发起登陆操作dispatch({ type: 'login' }),点击退出按钮就发起退出操作dispatch({ type: 'logout' }),所有和how相关的代码都在reducer中维护,组件中只需要思考What,让我们的代码可以像用户的行为一样,更加清晰。

3个W和1个H:why,where,what,how,做什么,为什么做,在哪里做,怎么做。

除此之外还有一个好处,我们在前文提过Reducer其实一个UI无关的纯函数,useReducer的方案是的我们更容易构建自动化测试用例。

小结

  • 如果你的state是一个数组或者对象
  • 如果你的state变化很复杂,经常一个操作需要修改很多state
  • 如果你希望构建自动化测试用例来保证程序的稳定性
  • 如果你需要在深层子组件里面去修改一些状态(关于这点我们下篇文章会详细介绍)
  • 如果你用应用程序比较大,希望UI和业务能够分开维护

配合useContext使用

上面使用useReducer,可以帮助我们集中式的处理复杂的state管理。但如果我们的页面很复杂,拆分成了多层多个组件,我们如何在子组件触发这些state变化呢,比如在LoginButton触发登录失败操作?

useContext第六章已经介绍过(React新特性-Hooks之useContext(六)),useContext能够允许数据跨域组件层级直接传递到任何的子组件身上。

下面结合useContext来优化login

// 定义初始化值
const initState = {
  name: '',
  pwd: '',
  isLoading: false,
  error: '',
  isLoggedIn: false,
}
// 定义state[业务]处理逻辑 reducer函数
function loginReducer(state, action) {
  switch(action.type) {
      case 'login':
          return {
              ...state,
              isLoading: true,
              error: '',
          }
      case 'success':
          return {
              ...state,
              isLoggedIn: true,
              isLoading: false,
          }
      case 'error':
          return {
              ...state,
              error: action.payload.error,
              name: '',
              pwd: '',
              isLoading: false,
          }
      default: 
          return state;
  }
}
// 定义 context函数
const LoginContext = React.createContext();

function LoginPage() {
  const [state, dispatch] = useReducer(loginReducer, initState);
  const { name, pwd, isLoading, error, isLoggedIn } = state;
  const login = (event) => {
      event.preventDefault();
      dispatch({ type: 'login' });
      login({ name, pwd })
          .then(() => {
              dispatch({ type: 'success' });
          })
          .catch((error) => {
              dispatch({
                  type: 'error'
                  payload: { error: error.message }
              });
          });
  }
  // 利用 context 共享dispatch
  return ( 
      <LoginContext.Provider value={dispatch}>
          <...>
          <LoginButton />
      </LoginContext.Provider>
  )
}
function LoginButton() {
  // 子组件中直接通过context拿到dispatch,出发reducer操作state
  const dispatch = useContext(LoginContext);
  const click = () => {
      if (error) {
          // 子组件可以直接 dispatch action
          dispatch({
              type: 'error'
              payload: { error: error.message }
          });
      }
  }
}

可以看到在useReducer结合useContext,通过contextdispatch函数提供给组件树中的所有组件使用 ,而不用通过props添加回调函数的方式一层层传递。

使用Context相比回调函数的优势:

  • 对比回调函数的自定义命名,Context的Api更加明确,我们可以更清晰的知道哪些组件使用了dispatch、应用中的数据流动和变化。这也是React一直以来单向数据流的优势。
  • 更好的性能:如果使用回调函数作为参数传递的话,因为每次render函数都会变化,也会导致子组件重新渲染。当然我们可以使用useCallback解决这个问题,但相比useCallback React官方更推荐使用useReducer,因为React会保证dispatch始终是不变的,不会引起consumer组件的重新渲染。

总结

  • 如果你的页面state很简单,可以直接使用useState
  • 如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
  • 如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext

文章作者: jackie chen
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jackie chen !
评论
 上一篇
React新特性-Hooks之自定义Hook(十完结) React新特性-Hooks之自定义Hook(十完结)
自定义Hook可以将一些逻辑抽离至可复用的函数里 系列目录 React新特性-Context(一) React新特性-Lazy和Suspense(二) React新特性-memo(三) React新特性-Hooks之useState(四
2019-07-29
下一篇 
React新特性-Hooks之useMemo和useCallback(八) React新特性-Hooks之useMemo和useCallback(八)
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。 系列目录 React新特性-Context(一) React新特性-L
2019-07-24
  目录