转换为 anonymousModel 时出现 mobx-state-tree 错误

mobx-state-tree error while converting to anonymousModel

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

手动检查类型时,我没有看到类型错误的问题,所以不明白为什么会抛出错误。

Codesandox live minimum code

错误

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).

文件结构

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: '', 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"
    ]
  }
}

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

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

但是,如果您 getSnapshotstore 在创建后没有参数,您将得到:

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

从某种意义上说,这将是“默认快照”,因为当您 create 您的商店没有特定数据时会发生这种情况。

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

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

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

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

会起作用,但似乎不是您想要的。

您可能打算做的是直接将 Info 字段设为 User 类型,而不是 User 类型的 map,或者 optionalUser 类型,此后您在创建商店时无需指定 User。所以也许你的商店模型应该是这样的:

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

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

请注意,原始值是自动可选的,但模型不是(因此显式 optional 调用的原因)。 optional 参数有一个默认值,但仍然存在。它只是不需要在 create 时明确定义。此外,请务必在测试时重置您的 localStorage,否则它可能看起来不起作用...