How to save Mobx state in sessionStorage

11,918

Solution 1

The easiest way to approach this would be to have a mobx "autorun" triggered whenever any observable property changes. To do that, you could follow my answer to this question.

I'll put some sample code here that should help you get started:

function autoSave(store, save) {
  let firstRun = true;
  mobx.autorun(() => {
    // This code will run every time any observable property
    // on the store is updated.
    const json = JSON.stringify(mobx.toJS(store));
    if (!firstRun) {
      save(json);
    }
    firstRun = false;
  });
}

class MyStore {
  @mobx.observable prop1 = 999;
  @mobx.observable prop2 = [100, 200];

  constructor() {
    this.load();
    autoSave(this, this.save.bind(this));
  }

  load() {
    if (/* there is data in sessionStorage */) {
      const data = /* somehow get the data from sessionStorage or anywhere else */;
      mobx.extendObservable(this, data);
    }
  }

  save(json) {
    // Now you can do whatever you want with `json`.
    // e.g. save it to session storage.
    alert(json);
  }
}

Solution 2

Posting the example from here: https://mobx.js.org/best/store.html

This shows a cleaner method of detecting value changes, though not necessarily local storage.

import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';

export class TodoStore {
    authorStore;
    transportLayer;
    @observable todos = [];
    @observable isLoading = true;

    constructor(transportLayer, authorStore) {
        this.authorStore = authorStore; // Store that can resolve authors for us
        this.transportLayer = transportLayer; // Thing that can make server requests for us
        this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
        this.loadTodos();
    }

    /**
     * Fetches all todo's from the server
     */
    loadTodos() {
        this.isLoading = true;
        this.transportLayer.fetchTodos().then(fetchedTodos => {
            fetchedTodos.forEach(json => this.updateTodoFromServer(json));
            this.isLoading = false;
        });
    }

    /**
     * Update a todo with information from the server. Guarantees a todo
     * only exists once. Might either construct a new todo, update an existing one,
     * or remove an todo if it has been deleted on the server.
     */
    updateTodoFromServer(json) {
        var todo = this.todos.find(todo => todo.id === json.id);
        if (!todo) {
            todo = new Todo(this, json.id);
            this.todos.push(todo);
        }
        if (json.isDeleted) {
            this.removeTodo(todo);
        } else {
            todo.updateFromJson(json);
        }
    }

    /**
     * Creates a fresh todo on the client and server
     */
    createTodo() {
        var todo = new Todo(this);
        this.todos.push(todo);
        return todo;
    }

    /**
     * A todo was somehow deleted, clean it from the client memory
     */
    removeTodo(todo) {
        this.todos.splice(this.todos.indexOf(todo), 1);
        todo.dispose();
    }
}

export class Todo {

    /**
     * unique id of this todo, immutable.
     */
    id = null;

    @observable completed = false;
    @observable task = "";

    /**
     * reference to an Author object (from the authorStore)
     */
    @observable author = null;

    store = null;

    /**
     * Indicates whether changes in this object
     * should be submitted to the server
     */
    autoSave = true;

    /**
     * Disposer for the side effect that automatically
     * stores this Todo, see @dispose.
     */
    saveHandler = null;

    constructor(store, id=uuid.v4()) {
        this.store = store;
        this.id = id;

        this.saveHandler = reaction(
            // observe everything that is used in the JSON:
            () => this.asJson,
            // if autoSave is on, send json to server
            (json) => {
                if (this.autoSave) {
                    this.store.transportLayer.saveTodo(json);
                }
            }
        );
    }

    /**
     * Remove this todo from the client and server
     */
    delete() {
        this.store.transportLayer.deleteTodo(this.id);
        this.store.removeTodo(this);
    }

    @computed get asJson() {
        return {
            id: this.id,
            completed: this.completed,
            task: this.task,
            authorId: this.author ? this.author.id : null
        };
    }

    /**
     * Update this todo with information from the server
     */
    updateFromJson(json) {
        // make sure our changes aren't send back to the server
        this.autoSave = false;
        this.completed = json.completed;
        this.task = json.task;
        this.author = this.store.authorStore.resolveAuthor(json.authorId);
        this.autoSave = true;
    }

    dispose() {
        // clean up the observer
        this.saveHandler();
    }
}

Solution 3

Turns out you can do this in just a few lines of code:

const store = observable({
    players: [
        "Player 1",
        "Player 2",
    ],
    // ...
})

reaction(() => JSON.stringify(store), json => {
    localStorage.setItem('store',json);
}, {
    delay: 500,
});

let json = localStorage.getItem('store');
if(json) {
    Object.assign(store, JSON.parse(json));
}

Boom. No state lost when I refresh the page. Saves every 500ms if there was a change.

Solution 4

Here, you can use my code, although it only supports localStorage you should be able to modify it quite easily.

https://github.com/nightwolfz/mobx-storage

Share:
11,918
anthony-dandrea
Author by

anthony-dandrea

Web developer

Updated on June 16, 2022

Comments

  • anthony-dandrea
    anthony-dandrea almost 2 years

    Trying to essentially accomplish this https://github.com/elgerlambert/redux-localstorage which is for Redux but do it for Mobx. And preferably would like to use sessionStorage. Is there an easy way to accomplish this with minimal boilerplate?

  • anthony-dandrea
    anthony-dandrea over 7 years
    Thanks! How would it reload the state on page reload?
  • Mouad Debbar
    Mouad Debbar over 7 years
    In the constructor, before you call autoSave(), you could read from sessionStorage and set the values on the store instance.
  • Mouad Debbar
    Mouad Debbar over 7 years
    I just edited my answer to provide a more complete solution. Of course you still need to make some changes to get it working for your case, but I hope this helps you get started.
  • karianpour
    karianpour almost 5 years
    If the strict-mode is enabled, it gives an error, changing observed observable values outside actions is not allowed
  • karianpour
    karianpour almost 5 years
    While loading I encounter Error: [mobx] 'extendObservable' can only be used to introduce new properties. Use 'set' or 'decorate' instead
  • mpen
    mpen almost 5 years
    @karianpour I think you just have to wrap it in action(() => Object.assign(...))) then