Search code examples
javascriptreactjsmobxmobx-reactmobx-state-tree

mobx-state-tree error while converting to anonymousModel


What should happen - succesfully create RootStore from defaultSnapshot and reset it when needed, successful backcuping in localStorage. What happens - getting an error while trying to apply a snapshot, when attempting to open page, just by running code even without interacting with it.

When checking types manually I do not see problems with type mistakes, so can not understand why it throws error.

Codesandox live minimum code

Error

Error: [mobx-state-tree] Error while converting `{"token":"","myInnerInfo":{"login":"","type":""},"myDisplayInfo":{"login":"","type":""},"loginInfo":{"login":"","type":""},"loginList":[],"loading":false,"logined":false}` to `AnonymousModel`:

    at path "/myInnerInfo/login" value `""` is not assignable to type: `AnonymousModel` (Value is not a plain object).
    at path "/myInnerInfo/type" value `""` is not assignable to type: `AnonymousModel` (Value is not a plain object).
    at path "/myDisplayInfo/login" value `""` is not assignable to type: `AnonymousModel` (Value is not a plain object).
    at path "/myDisplayInfo/type" value `""` is not assignable to type: `AnonymousModel` (Value is not a plain object).
    at path "/loginInfo/login" value `""` is not assignable to type: `AnonymousModel` (Value is not a plain object).
    at path "/loginInfo/type" value `""` is not assignable to type: `AnonymousModel` (Value is not a plain object).

File structure

enter image description here

store.js (imported in index.js)

import { types, flow, onSnapshot, applySnapshot } from 'mobx-state-tree';
import { values } from 'mobx';
import axios from 'axios';

const defaultSnapshot = {
  token: '',
  myInnerInfo: { login: '', type: '' },
  myDisplayInfo: { login: '', type: '' },
  loginInfo: { login: '', type: '' },
  loginList: [],
  loading: false,
  logined: false,
}

const User = types
  .model({
    login: '',
    type: '',
  }).actions(self => ({
    setUserInfo({ login, type }) {
      self.login = login;
      self.type = type;
    }
  }))

const RootStore = types
  .model({
    token: '',
    myInnerInfo: types.map(User),
    myDisplayInfo: types.map(User),
    loginInfo: types.map(User),
    loginList: types.array(types.string),
    loading: false,
    logined: false,
  }).views(self => ({
    get loginListLength() {
      return values(self.loginList).length;
    },
  })).actions(self => ({
    // setToken (token) {
    //   self.token = token;
    // },
    // setMyInnerInfo (userInfo) {
    //   self.myInnerInfo.setUserInfo(userInfo);
    // },
    // setMyDisplayInfo (userInfo) {
    //   self.myDisplayInfo.setUserInfo(userInfo);
    // },
    // setLoginInfo (userInfo) {
    //   self.loginInfo.setUserInfo(userInfo);
    // },
    // setLoginList (loginList) {
    //   self.loginList = loginList;
    // },
    // setLoading (loading) {
    //   self.loading = loading;
    // },
    // setLogined (logined) {
    //   self.logined = logined;
    // },
    // reset() {
    //   self.token = '';
    //   self.myInnerInfo = User.create({});
    //   self.myDisplayInfo = User.create({});
    //   self.loginInfo = User.create({});
    //   self.loginList = [];
    //   self.loading = false;
    //   self.logined = false;
    // },
    register: flow(function* register(login, password) {
      self.loading = true;
      try {
        const res = yield axios({
          method: 'POST',
          url: `${process.env.REACT_APP_HOST}/users/register`,
          data: { login, password },
        });
        alert('Registered');
        self.loading=false;
      } catch (e) {
        console.error(e);
        alert(`Error registering! Please retry!`);
        resetStore();
      }
    }),
    login: flow(function* login(login, password) {
      self.loading = true;
      try {
        const res = yield axios({
          method: 'POST',
          url: `${process.env.REACT_APP_HOST}/users/login`,
          data: { login, password },
        });
        self.token = res.data.token;
        self.myInnerInfo.setUserInfo(res.data.user);
        self.myDisplayInfo.setUserInfo({ login: '', type: '' });
        self.loginInfo.setUserInfo({ login: '', type: '' });
        self.loginList = [];
        alert('Logined');
        self.logined = true;
        self.loading=false;
      } catch (e) {
        console.error(e);
        alert(`Error logining! Please retry!`);
        resetStore();
      }
    }),
    unlogin() {
      self.loading = true;
      self.logined = false;
      self.token = '';
      self.myInnerInfo.setUserInfo({ login: '', type: '' });
      self.myDisplayInfo.setUserInfo({ login: '', type: '' });
      self.loginInfo.setUserInfo({ login: '', type: '' });
      self.loginList = [];
      alert('Unlogined');
      self.loading=false;
    },
    getMyInfo: flow(function* getMyInfo() {
      self.loading = true;
      try {
        const res = yield axios({
          method: 'GET',
          url: `${process.env.REACT_APP_HOST}/users/my-info`,
          headers: {'Authorization': self.token ? `Bearer ${self.token}` : ''},
        });
        // self.token = res.data.token;
        // self.myInnerInfo.setUserInfo(res.data.user);
        self.myDisplayInfo.setUserInfo(res.data);
        // self.loginInfo.setUserInfo({});
        // self.loginList = [];
        alert('Loaded information');
        // self.logined = true;
        self.loading=false;
      } catch (e) {
        console.error(e);
        alert(`Error loading information! Please retry!`);
        resetStore();
      }
    }),
    getLoginList: flow(function* getLoginList() {
      self.loading = true;
      try {
        const res = yield axios({
          method: 'GET',
          url: `${process.env.REACT_APP_HOST}/users/list-logins`,
          headers: {'Authorization': self.token ? `Bearer ${self.token}` : ''},
        });
        // self.token = res.data.token;
        // self.myInnerInfo.setUserInfo(res.data.user);
        // self.myDisplayInfo.setUserInfo(res.data);
        // self.loginInfo.setUserInfo({});
        self.loginList = res;
        alert('Loaded list');
        // self.logined = true;
        self.loading=false;
      } catch (e) {
        console.error(e);
        alert(`Error loading list! Please retry!`);
        resetStore();
      }
    }),
    getUserInfo: flow(function* getUserInfo(login) {
      self.loading = true;
      try {
        const res = yield axios({
          method: 'GET',
          url: `${process.env.REACT_APP_HOST}/users/my-info/${login}`,
          headers: {'Authorization': self.token ? `Bearer ${self.token}` : ''},
        });
        // self.token = res.data.token;
        // self.myInnerInfo.setUserInfo(res.data.user);
        // self.myDisplayInfo.setUserInfo(res.data);
        self.loginInfo.setUserInfo(res.data);
        // self.loginList = [];
        alert('Loaded information');
        // self.logined = true;
        self.loading=false;
      } catch (e) {
        console.error(e);
        alert(`Error loading information! Please retry!`);
        resetStore();
      }
    }),
  }));

const store = RootStore.create();

if(!(localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY] && JSON.parse(localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY]))) {
  localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY] = JSON.stringify(defaultSnapshot);
}
applySnapshot(store, JSON.parse(localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY]));

onSnapshot(store, snapshot => {
  localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY] = JSON.stringify(snapshot);
  console.info(snapshot);
});

export default store;
export function resetStore() {
  localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY] = JSON.stringify(defaultSnapshot);
  applySnapshot(store, JSON.parse(localStorage[process.env.REACT_APP_LOCALSTORAGE_KEY]));
}

package.json

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.9",
    "@testing-library/react": "^11.2.3",
    "@testing-library/user-event": "^12.6.0",
    "axios": "^0.21.1",
    "mobx": "^6.0.4",
    "mobx-react": "^7.0.5",
    "mobx-state-tree": "^5.0.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.1",
    "web-vitals": "^0.2.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Solution

  • It appears that your defaultSnapshot does not match your defined model structure. You define your default snapshot as follows:

    const defaultSnapshot = {
      token: '',
      myInnerInfo: { login: '', type: '' },
      myDisplayInfo: { login: '', type: '' },
      loginInfo: { login: '', type: '' },
      loginList: [],
      loading: false,
      logined: false,
    }
    

    However, if you getSnapshot of your store after you create it with no arguments, you get:

    {
     token: "",
     myInnerInfo: {},
     myDisplayInfo: {},
     loginInfo: {},
     loginList: [],
     loading: false,
     logined: false
    }
    

    Which would be a "default snapshot" in the sense that it is what happens when you create your store with no specific data.

    Now this looks like the two should be compatible, except that you defined the three Info fields as maps. Maps of models look like this:

    {
      "<id>": { <model snapshot> },
      …
    }
    

    Therefore, when loading your default snapshot, it causes an error because it tries to treat what you intended to be model data as map data - it thinks you have a collections of two Users with keys of login and type, and values of "", instead of objects compatible with User. For instance,

    …
    myInnerInfo: { 
      login: { login: 'some user data', type:'' }, 
      type: { login: 'another user data', type:'' }
    },
    …
    

    Would work, but doesn't seem like what you intended.

    What you probably intended to do was make the Info fields directly of the User type, not a map of the User type, or perhaps an optional or the User type, since then you don't need to specify a User when creating the store. So perhaps your store model should look like this:

    .model({
        token: '',
        myInnerInfo: types.optional(User, {}),
        myDisplayInfo: types.optional(User, {}),
        loginInfo: types.optional(User, {}),
        loginList: types.array(types.string),
        loading: false,
        logined: false,
      })
    

    This structure is compatible with your default snapshot, and does not require values when creating the store.

    Note that primitive values are automatically optional, but models are not (hence why the explicit optional call). An optional parameter has a default value, but will still exist. It just does not need to be explicitly defined at create time. Also, be sure to reset your localStorage when testing, or it may seem like it didn't work...