【问题标题】:Mutable global state causing issues with array length可变全局状态导致数组长度问题
【发布时间】:2020-04-23 15:29:25
【问题描述】:

我已经在 SPA 上工作了一段时间,并使用自定义上下文 API 管理我的全局状态,但它一直在导致树下不受欢迎的重新渲染让人头疼,所以我想我应该试试 react-easy-state .到目前为止,一切都很好,但我开始遇到一些我认为与全局状态的可变性有关的问题,这很容易通过使用 immer 之类的库的自定义上下文 api 实现来解决。

这是我遇到的问题的简化版本:我有一个用于管理订单的全局状态。订单对象primaryOrder 有一个插件数组,其中添加了额外的项目 - 可用插件的列表存储在一个单独的存储中,该存储负责从我的 API 获取列表。 orderStore 看起来像这样:


const orderStore = store({
  initialized: false,
  isVisible: false,
  primaryOrder: {
    addons: [],
  }
})

当用户选择增加一个插件项目的数量时,如果它不存在,它会被添加到插件数组中,如果它是插件的qty 属性则增加。当数量减少时,同样的逻辑适用,除非它达到 0,然后从数组中删除插件。这是在 orderStore 上使用以下方法完成的:

const orderStore = store({
  initialized: false,
  isVisible: false,
  primaryOrder: {
    addons: [],
  },
 get orderAddons() {
    return orderStore.primaryOrder.addons;
  },
increaseAddonItemQty(item) {
    let index = orderStore.primaryOrder.addons.findIndex(
      (i) => i.id === item.id
    );

    if (index === -1) {
      let updatedItem = {
        ...item,
        qty: 1,
      };
      orderStore.primaryOrder.addons = [
        ...orderStore.primaryOrder.addons,
        updatedItem,
      ];
    } else {
      orderStore.primaryOrder.addons[index].qty += 1;
    }
    console.log(orderStore.primaryOrder.addons);
  },
  decreaseAddonItemQty(item) {
    let index = orderStore.primaryOrder.addons.findIndex(
      (i) => i.id === item.id
    );

    if (index === -1) {
      return;
    } else {
      // remove the item from the array if value goes 1->0
      if (orderStore.primaryOrder.addons[index].qty === 1) {
        console.log("removing item from array");
        orderStore.primaryOrder.addons = _remove(
          orderStore.primaryOrder.addons,
          (i) => i.id !== item.id
        );
        console.log(orderStore.primaryOrder.addons);
        return;
      }
      orderStore.primaryOrder.addons[index].qty -= 1;
    }
  }
})

我遇到的问题与我的一个视图使用orderStore.addons 的事实有关。在这种情况下,我的 Product 组件是消费者:

const Product = (item) => {
    const [qty, setQty] = useState(0);
    const { id, label, thumbnailUrl, unitCost } = item;

    autoEffect(() => {
        if (orderStore.orderAddons.length === 0) {
            setQty(0);
            return;
        }
        console.log({ addons: orderStore.orderAddons });
        let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
        console.log({ index });
        if (index !== -1) setQty(orderStore.findAddon(index).qty);
    });

    const Adder = () => {
        return (
            <div
                className="flex"
                style={{ flexDirection: "row", justifyContent: "space-between" }}
            >
                <div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
                <div>{qty}</div>
                <div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
            </div>
        );
    }

    return (
        <div>
            <div>{label} {unitCost}</div>
            <Adder />
        </div>
    )
}

export default view(Product)

当我调用 decreaseAddonItemQty 并且该项目从 addons 数组中删除时,会出现此问题。在Product 组件中抛出错误,指出Uncaught TypeError: Cannot read property 'id' of undefined 由于数组长度读取为2,尽管该项目已被删除(见下图)

我的假设是消费者Product 在完成更新之前正在读取全局存储,当然我可能是错的。

使用 react-easy-state 来避免这个问题的正确方法是什么?

【问题讨论】:

    标签: reactjs react-state-management react-easy-state


    【解决方案1】:

    您似乎发现了一个自动批处理错误。只需将错误的变异代码包装在 batch 中,直到它被修复以使其正常工作。

    import { batch, store } from '@risingstack/react-easy-state'
    
    const orderStore = store({
      decreaseAddonItemQty(item) {
        batch(() => {
          // put your code here ...
        })
      }
    })
    

    阅读 “响应式渲染是批处理的。多个同步存储突变不会导致同一组件的多次重新渲染。” repo 自述文件的部分以获取有关批处理的更多信息。 p>

    还有一些见解:

    React 更新是同步的(与 Angular 和 Vue 不同),Easy State(以及所有其他状态管理器)在幕后使用 React setState 来触发重新渲染。这意味着它们也是同步的。

    setState 通常会立即应用大更新,而 Easy State 会在您更改存储属性时调用虚拟 setState。这意味着 Easy State 会不必要地频繁地重新渲染。为了防止这种情况,我们有一个batch 方法来阻止重新渲染,直到整个包含的代码块被执行。这个batch 会自动应用于大多数任务源,因此您不必担心它,但如果您从一些奇异的任务源调用一些变异代码,它不会自动批处理。

    我们很少谈论 batch,因为一旦发布 Concurrent React,它(最终)就会过时。与此同时,我们正在尽可能多的地方添加自动批处理。在下一次更新(几天后)中,存储方法将获得自动批处理,这将解决您的问题。

    您可能想知道,没有批处理怎么会把事情搞得这么糟糕。较旧的透明反应系统(如 MobX 4)只会不必要地渲染组件几次,但它们可以正常工作。这是因为他们使用 getter 和 setter 来拦截 get 和 set 操作。然而,Easy State(和 MobX 5)使用“看到更多”的代理。在您的情况下,浏览器的 array.splice 实现的一部分是在 JS 中实现的,并且代理在 array.splice 中拦截 get/set 操作。可能array.splice 在运行array.length = 2 之前正在执行array[2] = undefined(这当然只是伪代码)。如果不进行批处理,则结果与您所看到的完全相同。

    我希望这有助于并解决您的问题,直到问题得到解决(:

    编辑:在短期内,我们计划添加一个严格模式,当存储数据在存储方法之外发生突变时将抛出该模式。这 - 结合自动存储方法批处理 - 将是这个问题的最完整的解决方案,直到 Concurrent React 到来。

    Edit2:我很想知道为什么自动批处理逻辑没有正确批处理这个问题,以便通过一些测试来涵盖这种情况。您是否有机会公开回购?

    【讨论】:

    • 感谢@Bertalan,我从来没有考虑在方法中使用批处理,因为据我阅读文档的理解,更新多个状态属性时会使用批处理。我很想帮助弄清楚为什么会发生这种情况。仅供参考,在看到您的响应之前,我通过将插件数组作为道具传递给容器并将其包装在 cloneDeep 中来解决了这个问题。我认为这意味着道具仅在内部更新完成后才更新,而不是从组件内部直接访问商店。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-04
    • 2020-12-10
    • 1970-01-01
    • 2014-06-17
    相关资源
    最近更新 更多