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.
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
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"
]
}
}
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 map
s. 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...