转换为匿名模型时出现 mobx-state-tree 错误

问题描述

应该发生什么 - 从 defaultSnapshot 成功创建 RootStore 并在需要时重置它,在 localStorage 中成功备份。 发生了什么 - 在尝试应用快照时、在尝试打开页面时出现错误,即使没有与之交互也只是通过运行代码

手动检查类型时,我没有看到类型错误的问题,所以无法理解为什么会抛出错误

Codesandox live minimum code

错误

Error: [mobx-state-tree] Error while converting `{"token":"","myInnerInfo":{"login":"","type":""},"mydisplayInfo":{"login":"","loginInfo":{"login":"","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).

文件结构

enter image description here

store.js(在 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: '',loginInfo: { login: '',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),}).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,url: `${process.env.REACT_APP_HOST}/users/login`,});
        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`,});
        // 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}`,});
        // 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"
    ]
  }
}

解决方法

您的 defaultSnapshot 似乎与您定义的模型结构不匹配。 您可以按如下方式定义默认快照:

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

但是,如果您在没有参数的情况下创建 getSnapshot 之后的 store,您会得到:

{
 token: "",myInnerInfo: {},myDisplayInfo: {},loginInfo: {},logined: false
}

这将是“默认快照”,因为它是当您create 没有特定数据的商店时会发生的情况。

现在看起来两者应该是兼容的,只是您将三个 Info 字段定义为 map。模型图如下所示:

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

因此,在加载您的默认快照时,它会导致错误,因为它试图将您打算作为模型数据的内容视为地图数据 - 它认为您有两个 Users 的集合,键为 {{1 }} 和 login,以及 type 的值,而不是与 "" 兼容的对象。例如,

User

可以,但似乎不是您想要的。

您可能打算做的是将 … myInnerInfo: { login: { login: 'some user data',type:'' },type: { login: 'another user data',type:'' } },… 字段直接设为 Info 类型,而不是 User 类型的 map,或者可能是 {{1} }} 或 User 类型,此后您无需在创建商店时指定 optional。所以也许你的商店模型应该是这样的:

User

此结构与您的默认快照兼容,并且在创建商店时不需要值。

请注意,原始值是自动可选的,但模型不是(因此显式调用 User 的原因)。 .model({ token: '',myInnerInfo: types.optional(User,{}),myDisplayInfo: types.optional(User,loginInfo: types.optional(User,loginList: types.array(types.string),}) 参数具有默认值,但仍将存在。它只是不需要在 optional 时间明确定义。另外,请务必在测试时重置您的 optional,否则它可能看起来不起作用...