在React中Immutable概念及ImmerJS应用

91 阅读4分钟

最近又看了React官网,对于React视图更新的处理又有了一些理解,在此记录分享一下

Immutable 不可变数据

这是JavaScript中的一个概念,简单地说就是: 如果不创建一个全新的值替换它,它的内容是无法更改的。

在 JavaScript 中,原始值是不可变的——一旦创建了原始值,它就不能被改变,尽管持有它的变量可以被重新分配另一个值。相比之下,对象和数组默认是可变的——它们的属性和元素可以在不重新分配新值的情况下更改。 所以我们可以总结出来:不可变数据类型和可变数据类型的区别在于其内容是否可以被更改

代码解释:

let str = "hello"; // 这里我们声明了一个基础数据类型的值
str = "world"; // 当我们给这个变量赋予新的值的时候,实则是创建了一个新字符串,而不是改变原有字符串,原值就随即被释放了
let arr = [1, 2, 3]; // 声明一个引用数据类型的值
arr[0] = 4; // 当我们对这个值进行更新时,并没有改变它在堆内存中的引用地址即没有重新分配新值给到这个arr变量

React中应用 Immer.js

react的追求的是数据不可变(react内部比较的是数据的引用地址),操作数据太过繁琐,但JS更加习惯用可变的方式对数据进行操作

在React中,组件的渲染是基于其状态数据以及从父组件传递下来的属性进行的。当状态发生变化时,React会调用组件的 render() 方法重新渲染视图。因此,在开发过程中,我们需要考虑如何有效地更新组件状态。

React通过比较数据的引用地址来检查是否需要重新渲染。如果前后两次渲染、同一块数据的引用地址相同,则认为它们是相等的,不需要重新渲染;否则认为数据已经发生了变化,需要重新渲染。因此,React追求的是数据不可变,也即它们的值不能被直接更改,而是新建一个新的对象或者数组。

举个例子,假设一个组件有一个名为 data 的状态:

class MyComponent extends React.Component {
  state = { data: { foo: 'bar' } };
  render() {
    ...
  }
}

当你想要更新状态中的 data 属性时,你必须创建一个全新的对象来代替旧的对象,而不能直接修改它的属性:

this.setState({ data: { foo: 'baz' } }); // 创建了一个新对象

如果你尝试直接修改原有对象的属性,则React可能无法察觉到状态的变化,造成渲染的错误。

Immer.js

React官网给我们推荐了这个库用于更新state,它可以让你用更符合js思维逻辑的方式在React应用中操作数据,下面我们来看示例。

在类组件中使用

 class ClassDemo extends Component<any, IState> {
  constructor(props: any) {
    super(props);
    this.state = {
      obj: {
        date: new Date(),
        obj2: {
          a: 'a',
          b: 2,
        },
      },
      
    };
    this.upDataForImmer = this.upDataForImmer.bind(this);
  }

  upDatatoImmer = (data) => {
    return produce(data, (draft) => {
    draft.obj2.a = 'a-new';
  });
}

  upDataForImmer = () => {
    const newState = produce(this.state.obj, (draft) => {
      draft.obj2.a = 'a-new';
    });
    
   // 你也可以使用函数柯里化的思想进行优化处理 
   // const newState = this.upDatatoImmer(this.state.obj);
   
    this.setState({
      obj: newState,
    });
  };

  render() {
    return (
      <div>
        <button onClick={this.upDataForImmer}>更新数据</button>
      </div>
    );
  }
}

在函数组件中,如果要使用复杂的state,可以使用 produce(currentState, recipe: (draftState) => void): nextState这个api进行更新,用法参考下方示例

function useReducerDemo() {
  const [todos, setTodoList] = useReducer(
    produce(
      (
        draft: Todo[],
        action: { type: string; id: string }
      ): Todo[] | undefined => {
        switch (action.type) {
          case 'toggle':
            // eslint-disable-next-line no-case-declarations
            const todo = draft.find((todo) => todo.id === action.id);
            (todo as unknown as Todo).done = !todo?.done;
            break;
          case 'add':
            draft.push({
              id: action.id,
              title: 'A new todo',
              done: false,
            });
            break;
          default:
            return [
              {
                id: 'JavaScript',
                title: 'JavaScript',
                done: false,
              },
            ];
        }
      }
    ),
    [
      {
        id: 'React',
        title: 'React',
        done: true,
      },
      {
        id: 'Vue',
        title: 'Vue',
        done: false,
      },
    ]
  );

  const handleToggle = (id:string) => {
    setTodoList({
      type: "toggle",
      id
    });
  };

  const handleAdd = () => {
    setTodoList({
      type: "add",
      id: "todo_" + Math.random()
    });

  };

  return (
    <div>
      <button onClick={handleAdd}>新增一条</button>
      <ul>
        {todos.map((todo, index: number) => (
          <li key={index}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => {
                handleToggle(todo.id)
              }}
            />
            {todo.title}
          </li>
        ))}
      </ul>

    </div>
  )
}

使用 use-immer

immer.js还提供了另一个简略的hooks库以更好的支持在函数式组件中的使用

通过npm install use-immer安装后使用,语法简单易学,可参考useState和useReducer这两个hooks

npm地址:www.npmjs.com/package/use…

使用案例:

function ImmerDom() {

  const [imState, setImState] = useImmer<Option[][]>([[], [], [], []]);

  useEffect(() => {
    (function queryRegional() {
      fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
        .then((response) => response.json())
        .then((data) => {
          data.forEach((item: Option) => {
            const firstSpellNum: number = pinyin(item.label)[0].codePointAt(0)!;
            setRegionalSpell(firstSpellNum, item);
          });
        })
        .catch((e) => console.log(e));
    })();
  }, []);

  const setRegionalSpell = (spellNum: number, currentItem: Option) => {
    const describeForSpellMap: DescribeForSpellMap[] = [
      [
        (spellNum: number) => spellNum >= 97 && spellNum <= 103,
        (currentItem: Option) => {
          setImState((draft: Option[][]) => {
            draft[0].push(currentItem);
          });
        },
      ],
      [
        (spellNum: number) => spellNum >= 104 && spellNum <= 110,
        (currentItem: Option) => {
          setImState((draft: Option[][]) => {
            draft[1].push(currentItem);
          });
        },
      ],
      [
        (spellNum: number) => spellNum >= 111 && spellNum <= 116,
        (currentItem: Option) => {
          setImState((draft: Option[][]) => {
            draft[2].push(currentItem);
          });
        },
      ],
      [
        (spellNum: number) => spellNum >= 117 && spellNum <= 122,
        (currentItem: Option) => {
          setImState((draft: Option[][]) => {
            draft[3].push(currentItem);
          });
        },
      ],
    ];

    const getDescribe = describeForSpellMap.find((m) => m[0](spellNum));
    getDescribe ? getDescribe[1](currentItem) : null;
  };


  return (
    <div>
     ......
    </div>
  );
}

另外如果你需要使用Map或Set这种结构的数据,还需要在你项目的入口文件处显式的调用官方提供的api以启动支持 示例:App.jsx

import { enableMapSet } from 'immer';
enableMapSet();

function App() {

  return (
    <>
      .......
    </>
  )
}

export default App

关于其他配置,感兴趣的可以参考Immer.js 的官网:immerjs.github.io/immer/zh-CN…