Search code examples
reactjsrestreact-nativemobxmobx-state-tree

Is there any way to not define a data model in mobx-state-tree beforehand? (to decide the type dynamically)


I would love to be able to not define a type for a data model but still be able to have the data observable once it's loaded in. I also have relations but they could be defined statically. The docs tell me about frozen but I need the entries to be observable. Without that I would be better off sticking to what I have now.

I read something about dynamic model types in the comments of this: https://codeburst.io/the-curious-case-of-mobx-state-tree-7b4e22d461f but as I haven't worked with mst yet and don't use ts there is not enough information for me to see what solution he means exactly.

What mst wants me to do:

import React from "react";
import { render } from "react-dom";
import { types } from "mobx-state-tree";
import { observer } from "mobx-react";

const Todo = types.model({
  name: types.optional(types.string, ""),
  done: types.optional(types.boolean, false)
});

const eat = Todo.create({ name: "eat" });
render(
  <div>
    Eat TODO: {JSON.stringify(eat)}
  </div>,
  document.getElementById("root")
);

What I want to do:

import React from "react";
import { render } from "react-dom";
import { types } from "mobx-state-tree";
import { observer } from "mobx-react";

const Todo = types.model({});

const eat = Todo.create({ name: "eat" });
render(
  <div>
    Eat TODO: {JSON.stringify(eat)}
  </div>,
  document.getElementById("root")
);

more info:

https://github.com/mobxjs/mobx-state-tree/issues/326#issuecomment-433906949

https://github.com/mobxjs/mobx-state-tree/pull/247


Solution

  • This is how it could work in the app. It works mostly, just adding and removing items does not update the component.

    I came up with this, it could work. It does in the sandbox. This is a work in progress. As is, this does not work because it's impossible to change types after initializing them.

    With help of https://egghead.io/lessons/react-create-dynamic-types-and-use-type-composition-to-extract-common-functionality

    https://codesandbox.io/s/m39mjomzwx

    import React, { Component } from "react";
    import { types } from "mobx-state-tree";
    import { observer } from "mobx-react";
    import { render } from "react-dom";
    
    const ModelActions = types.model({}).actions(self => ({
      addToName: function addToName(string) {
        self.name = `${self.name} ${string}`;
      }
    }));
    
    function createModel(instance) {
      return types.compose(
        types.model(instance),
        ModelActions
      );
    }
    
    const TodoActions = types.model({}).actions(self => ({
      toggle: function toggle() {
        self.done = !self.done;
      }
    }));
    
    function createTodoModel(todo) {
      return types.compose(
        TodoActions,
        createModel(todo)
      );
    }
    
    function fetchUsers() {
      return Promise.resolve([{ id: 1234, name: "Jan" }]);
    }
    
    function fetchTodos() {
      return Promise.resolve([
        { id: 5, name: "eat", done: false },
        { id: 1, name: "drink" }
      ]);
    }
    
    function createDataStore(userData, todoData) {
      const User = createModel(userData[0]);
      const Todo = createTodoModel(todoData[0]);
    
      return types
        .model({
          users: types.map(User),
          todos: types.map(Todo)
        })
        .actions(self => {
          return {
            addUser(user) {
              self.users[user.id] = User.create(user);
            },
            removeUser(id) {
              self.users.delete(id);
            },
            addTodo(todo) {
              self.todos[todo.id] = Todo.create(todo);
            }
          };
        });
    }
    
    function makeStore([userData, todoData]) {
      const store = createDataStore(userData, todoData).create();
    
      function addData(add, data) {
        for (let i in data) {
          add(data[i]);
        }
      }
    
      addData(store.addTodo, todoData);
      addData(store.addUser, userData);
    
      return Promise.resolve(store);
    }
    
    const AppState = types.model({ selected: false }).actions(self => {
      return {
        select() {
          self.selected = !self.selected;
        }
      };
    });
    
    const appState = AppState.create();
    
    let dataState = null;
    
    // works
    const ThingNode = observer(function ThingNode({ thing }) {
      return (
        <span>
          {JSON.stringify(thing)}
          <br />
        </span>
      );
    });
    
    function* getThingList(things) {
      for (let key in things) {
        if (Number(key)) {
          yield (
            <ThingNode key={key} thing={things[key]} />
          );
        }
      }
    }
    
    // does not add or remove items
    const Child = observer(function Child({ state, store, ready }) {
      return (
        <div>
          Users:
          <br />
          {store ? [...getThingList(store.users)] : null}
          <br />
          Todos:
          <br />
          {store ? [...getThingList(store.todos)] : null}
          <br />
          Selected:
          <br />
          {JSON.stringify(state.selected)}
          <br />
          Ready:
          <br />
          {JSON.stringify(ready)}
        </div>
      );
    });
    
    @observer
    class Parent extends Component {
      state = { ready: null };
    
      componentDidMount() {
        Promise.all([fetchUsers(), fetchTodos()])
          .then(makeStore)
          .then(state => {
            dataState = state;
            // this.setState({ ready: true });
            this.props.store.select();
          })
          .then(() => {
            // test some stuff
            dataState.addTodo({ id: 789, name: "eat a cake" });
    
            dataState.addUser({ id: 324, name: "Henk" });
            dataState.users[324].addToName("Klaassie");
    
            dataState.todos[1].addToName("haha");
            dataState.todos[5].toggle();
    
            setTimeout(() => {
              dataState.removeUser(1234);
              dataState.addTodo({ id: 90, name: "dinges" });
              dataState.todos[5].addToName("thing");
            }, 1000);
    
            setTimeout(() => {
              dataState.todos[789].addToName("hihi");
            }, 2000);
    
            setTimeout(() => {
              dataState.todos[90].addToName("twice");
            }, 4000);
          })
          .then(() => {
            this.setState({ ready: false });
          })
          .then(() => {
            // only now do the added / removed entries become visible
            setTimeout(() => this.setState({ ready: true }), 3000);
          });
      }
    
      render() {
        console.log("Parent", dataState);
    
        return (
          <Child
            store={dataState}
            state={this.props.store}
            ready={this.state.ready}
          />
        );
      }
    }
    
    render(<Parent store={appState} />, document.getElementById("root"));