Search code examples
javascriptvue.jsimmer.js

Immer data not updating in Vue


I'm trying to use Immer with Vue. It appears that the state is updating, but Vue isn't updating the UI

// immutable.js
import produce, { applyPatches } from "immer"

let undo_buffer = []
export var state = { items: [] }
const handle_add_patch = (patch, inverse_patches) => {
  console.log("Inverse Patches: ", inverse_patches)
  undo_buffer.push(inverse_patches)
}
export const add_item = (item_name) => {
  console.log("Starting add_item call")
  const next_state = produce(
    state,
    draft => {
      draft.items.push({ name: item_name })
    },
    handle_add_patch
  )
  console.log("next state: ", next_state)
  state = next_state
}
export const undo = () => {
  const undo_patch = undo_buffer.pop()
  if (!undo_patch) return
  let new_state = applyPatches(state, undo_patch)
  console.log("New State: ", new_state)
  state = new_state
}
<!-- item_list.Vue -->
<template>
  <div>
    <button @click.prevent="add_item()">Add Item</button>
      {{ items }}
    <button @click.prevent="undo()">Undo</button>
  </div>
</template>
<script>
import * as immutable from './immutable.js'
export default {
  computed: {
    items: function(){ return immutable.state.items }
  },
  methods: {
    add_item(){
      console.log("State Before: ", immutable.state)
      immutable.add_item("Hello")
      console.log("State After: ", immutable.state)
    },
    undo(){
      console.log("State Before: ", immutable.state)
      immutable.undo()
      console.log("State After: ", immutable.state)
    }
  }
}
</script>

The console.log shows that the items array is changing, but the items in the Vue template just shows an empty array. How can I make this visible within Vue?


Solution

  • Computed properties are cached and they aren't recomputed until associated component data is changed. Since Immer object isn't a part of the component, recomputation never occurs.

    Caching can be prevented by using getter method instead of computed property:

    {{ getItems() }}
    
    ...
    
    methods: {
      getItems: () => immutable.state.items
      addItem() {
        immutable.add_item("Hello");
        this.$forceUpdate();
      }
    }
    

    A more direct approach is to force a property to be recomputed:

    data() {
      return { _itemsDirtyFlag: 0 }
    },
    computed: {
      items: {
        get() {
          this._itemsDirtyFlag; // associate it with this computed property
          return immutable.state.items;
        }
    },
    methods: {
      updateItems() {
        this._itemsDirtyFlag++;
      },
      addItem() {
        immutable.add_item("Hello");
        this.updateItems();
      }
    }
    

    A similar approach that uses Vue.util.defineReactive internal is used by vue-recomputed.