Search code examples
reactjsecmascript-6es6-classeasy-peasy

easy-peasy: useStoreState not working with ES6 class instances


In easy-peasy, the useStoreState() hook does not cause a re-render when we use the hook to access the store's field that stores an ES6 class instance. For example:

store.js

import { action, createStore } from "easy-peasy";

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const store = createStore({
  person: new Person("Tom", "20"),  // <-- Stores ES6 class instance
  updatePersonName: action((state, payload) => {
    state.person.name = payload;
  })
});

export default store;

app.jsx:

import "./styles.css";
import { useStoreActions, useStoreState } from "easy-peasy";
import React from "react";

export default function App() {
  const person = useStoreState((state) => state.person);
  return (
    <div className="App">
      <p>{JSON.stringify(person)}</p>
      <EditPerson />
    </div>
  );
}

function EditPerson() {
  const person = useStoreState((state) => state.person);
  const updatePersonName = useStoreActions(
    (actions) => actions.updatePersonName
  );
  return (
    <input
      value={person.name}
      onChange={(e) => updatePersonName(e.target.value)}
    />
  );
}

Edit Easy Peasy w/ ES6 classes

If we try to type in the input box, even though the updatePersonName action is successfully dispatched (see the screenshot below), the value of the input box remains unchanged. The person store state is not successfully updated and the useStoreState() hook does not cause a re-render.

enter image description here


Solution

  • Reason

    easy-peasy uses the Immer library to convert the mutations into the equivalent immutable updates against the state. However, Immer does not implicitly support custom ES6 classes.

    According to the Immer's docs:

    Plain objects (objects without a prototype), arrays, Maps and Sets are always drafted by Immer. Every other object must use the immerable symbol to mark itself as compatible with Immer

    Solution 1

    We can make our custom ES6 class compatible with Immer according to the docs:

    store.js

    import { action, createStore } from "easy-peasy";
    import { immerable } from "immer";  // 👈 Add this (1)
    
    class Person {
      [immerable] = true;  // 👈 Add this (2)
    
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    }
    
    const store = createStore({
      person: new Person("Tom", "20"),
      updatePersonName: action((state, payload) => {
        state.person.name = payload;
      })
    });
    
    export default store;
    

    Edit Easy Peasy w/ ES6 classes (Solution 1 - Immer)

    Solution 2

    Alternatively, we can convert the ES6 class into a plain JavaScript object if your ES6 class is simple enough:

    store.js

    import { action, createStore } from "easy-peasy";
    
    const store = createStore({
      // Use a plain JS object here 👇
      person: {
        name: "Tom",
        age: "20",
      },
      updatePersonName: action((state, payload) => {
        state.person.name = payload;
      })
    });
    
    export default store;
    

    Edit Easy Peasy w/ ES6 classes (Solution 2 - Plain Object)

    Note on easy-peasy-decorators

    According to Easy Peasy docs, there's a community extension called easy-peasy-decorators that provides the ability to generate stores via classes and decorators.

    Unfortunately as at 2022 Nov, that library does not support Easy Peasy v4 or above (as hinted in this issue)

    Reference