useSelector优化

在react函数组件中如果要访问state上的值应该用useSelector,这个函数本身使用很简单,但要注意优化,避免产生性能问题。

基本使用方法

const selectTodos = state => state.todos

const todos = useSelector(selectTodos)

文档上给出的注意要点:

注意:useSelector的结果会用 === 对比,所以如果返回值不同就会导致组件重新渲染,如果不小心在不必要返回新对象的时候返回了新的对象,你的组件会在每次action被触发的时候都重新刷新。

错误做法

// 错误做法
const selectTodoDescriptions = state => {
  // 每次返回一个新的对象
  return state.todos.map(todo => todo.text)
}

因为array.map方法返回的是新的对象,因而虽然每次的内容相同,但是返回的结果用===比较时是不同的,因此每次都会重新渲染。

正确做法

// 正确做法
const todos = useSelector(state => state.todos)

仍然存在问题

这样虽然能解决一点问题,但是对于需要过滤的selector却是无效,因为不论使用什么方法,filter过后的值总是会返回一个新的对象

const filteredTodos = (state, completed) => 
  state.todos.filter(todo => todo.completed === completed)

并且这种情况下使用useMemo也是比较麻烦的。

所以我们要解决的问题是,当选择的条件不变,并且相关数据也没有变化的时候,返回相同的数据。

所幸已经有人解决了这个问题,就是使用reselect库,@reduxjs/toolkit 也已经集成了这个库。

使用createSelector

假设有这样结构的state

{
  users: {ids: ['id1', 'id2'], entities: {id1: user1, id2: user2}}
}

如果使用普通的selector取得用户列表,在render方法中使用useSelector:

const users = useSelector((state) => {
  // run every times when rerender
  console.log("users selector called");
  return state.users.ids.map((id) => state.users.entities[id]);
});

这样虽然能达到效果,但是如果父组件刷新,不但每次都会执行selector,并且还会每次返回一个新的数组,导致列表重新渲染(虽然子组件如果用memo处理仍然是同一个user对象,props不变的情况下不会重新渲染)。

如果用了createSelector,可以解决这个问题。createSelector的调用方式如下:

createSelector(
  depends1, 
  depends2,
  ...,
  selector(depends1result, depends2result, ...) {
    return 结果
  }
)

depends的结构为:

(state, ...args) => dependsResult

args 就是传给selector的额外参数

如果没有任何depends,则selector的形式就是上面的depends函数形式。

之所以需要上面的depends,是因为如果每个依赖项的返回值跟上一次都一样的话,selector就直接返回缓存后的结果,避免重新运算。所以用createSelector版本写出来就是这样:

import { createSelector } from "@reduxjs/toolkit";

export const usersSelector = createSelector(
  (state) => state.users.ids,
  (state) => state.users.entities,
  (_, filter) => filter,
  (ids, users, filter) => {
    // when any of ids, users, filter arguments changes, this selector will run
    console.log("users selector called");
    switch (filter) {
      case "male":
      case "female":
        return ids
          .map((id) => users[id])
          .filter((user) => user.gender === filter);
      default:
        return ids.map((id) => users[id]);
    }
  }
);

这里还加入了filter参数,只要ids/entities/filter参数都没变化,则selector返回的结果不会重新计算,达到了优化效果。

更复杂的完整例子见链接:
https://codesandbox.io/s/useselector-demo-1q18xg?file=/src/App.js

发表回复