ThaJay
ThaJay

Reputation: 1922

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

Upvotes: 2

Views: 5407

Answers (1)

ThaJay
ThaJay

Reputation: 1922

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"));

Upvotes: 3

Related Questions