在React应用中使用Immer

其实React中提供的Hook useState已经提供了不可变数据的基本操作了,使用useState构建的任何State,都必须通过成对返回的setXXX方法修改其中的内容。

结合使用useState和Immer,就可以大大简化组件中状态的深度更新操作。例如以下示例所示。

import produce from 'immer';
import { useCallback, useState } from 'react';

const TodoList = () => {
  const [todos, setTodos] = useState([]);

  const handleToggleAction = useCallback(id => {
    // 这里利用produce创建了todo列表的新实例,而无需手动克隆原有的todo列表。
    setTodos(
      produce(draft => {
        const todo = draft.find(t => t.id === id);
        todo.completed = !todo.completed;
      })
    );
  }, []);

  return <div>{/* 这里输出Todo列表以及配置Callback的调用 */}</div>;
};

当示例中的这种用法可以形成一种模式的时候,那么就存在将这种逻辑提升一下的必要了。所以Immer库通过名为use-immer的包提供了一个专用的Hook:useImmer

要使用useImmer,只需要在应用项目中安装use-immer即可。利用这个Hook重写上面的示例,就会让代码变得更加简单。

import { useCallback } from 'react';
import { useImmer } from 'use-immer';

const TodoList = () => {
  const [todos, setTodos] = useImmer([]);

  const handleToggleAction = useCallback(id => {
    // 这里不再需要调用produce了,useImmer已经在其内部完成了这件事情。
    // 此时的setTodos就不再是之前useState返回的直接接受一个新值的简单函数了,而是一个接受一个操作Draft的函数的函数。
    setTodos(draft => {
      const todo = draft.find(t => t.id === id);
      todo.completed = !todo.completed;
    });
  }, []);

  return <div>{/* 这里输出Todo列表以及配置Callback的调用 */}</div>;
};

与此同理,useReducer也可以与Immer结合使用,例如以下示例。

import produce from 'immer';
import { useCallback, useReducer } from 'react';

const TodoList = () => {
  // 这里将柯里化的produce函数作为reduce函数传给了useReducer。
  const [todos, dispatch] = useReducer(
    produce((draft, action) => {
      switch (action.type) {
        case 'add':
          draft.push({ id: action.id, title: 'new todo', complete: false });
          break;
        case 'toggle':
          const todo = draft.find(t => t.id === action.id);
          todo.complete = !todo.complete;
          break;
        default:
          break;
      }
    }),
    []
  );

  const handleToggleAction = useCallback(id => {
    dispatch({ type: 'toggle', id });
  });

  return <div>{/* 这里输出Todo列表以及配置Callback的调用 */}</div>;
};

同样的,use-immer包中也提供了一个Hook:useImmerReducer来简化这种模式的代码。那么使用这个Hook重写上面的示例,也可以让括号减少一层。

import { useCallback } from 'react';
import { useImmerReducer } from 'use-immer';

const TodoList = () => {
  // 采用useImmerReducer以后,就省去了调用produce的过程。
  const [todos, dispatch] = useImmerReducer((draft, action) => {
    switch (action.type) {
      case 'add':
        draft.push({ id: action.id, title: 'new todo', complete: false });
        break;
      case 'toggle':
        const todo = draft.find(t => t.id === action.id);
        todo.complete = !todo.complete;
        break;
      default:
        break;
    }
  }, []);

  const handleToggleAction = useCallback(id => {
    dispatch({ type: 'toggle', id });
  });

  return <div>{/* 这里输出Todo列表以及配置Callback的调用 */}</div>;
};