mobxjs / mobx-state-tree

Full-featured reactive state management without the boilerplate
https://mobx-state-tree.js.org/
MIT License
6.92k stars 640 forks source link

Model type that can be a string or an object #1063

Closed rudijs closed 5 years ago

rudijs commented 5 years ago

Question

Hi,

I'd like to create a model with a property that can be a type string or a type object.

I'm using xstate and the currentState could be:

  1. "loading" (string)
  2. "active" (string)
  3. "inactive" (string)
  4. {"inactive": "noConnection"} (object)

The object form represents nested state in xstate.

In order to transition from one state to the next I need to pass in either the string or the object.

MST blows up when I try to pass in a object when a string has already been inferred.

I was looking at union types, but I'm a bit stuck and can't seem to figure it out.

Is it possible to model this type of MST model property?

Thanks.

xaviergonz commented 5 years ago

What code were you trying to use?

rudijs commented 5 years ago

@xaviergonz This code is still experimental for me I'm new to both xstate and MST.

Here's the model I have, it's pretty simple with just two model properties - name and currentState.

The currentState is derived from the xstate State Chart, I would like this property to be observable.

The state is transitioning from one to the other in an MST action, which is called by the application using the MST.

import { types } from "mobx-state-tree";
import { Machine } from "xstate";

// visualizer: https://musing-rosalind-2ce8e7.netlify.com/
const stateChart = {
  initial: "idle",
  states: {
    idle: {
      on: {
        LOADING: "loading"
      }
    },
    loading: {
      on: {
        SUCCESS: "active",
        INACTIVE: "inactive",
        REJECTED: [
          {
            target: "inactive.rejected"
            // target: "inactive"
          }
        ],
        ERROR: [
          {
            target: "inactive.noConnection"
            // target: "inactive"
          }
        ]
      }
    },
    inactive: {
      on: {
        LOADING: "loading"
      },
      states: {
        rejected: {
          data: {
            details: "Identity request rejected"
          }
        },
        noConnection: {
          data: {
            details: "Cannot connect to Scatter"
          }
        }
      }
    },
    active: {
      on: {
        FORGET_IDENTITY: "loading"
      }
    }
  }
};

const identityMachine = Machine(stateChart);

export const Identity = types
  .model({
    name: types.string,
    currentState: identityMachine.initialState.value
  })
  .views(self => ({
    get isAuthenticated() {
      return self.name === "Get Scatter" ? false : true;
    }
  }))
  .actions(self => ({
    stateTransition(state) {
      self.currentState = identityMachine.transition(
        self.currentState,
        state
      ).value;
    },
    setSession(identity) {
      if (!identity) {
        self.name = "Get Scatter";
        return;
      }
      self.name = identity.name;
    }
  }))
  .extend(self => {
    let scatter = null;

    return {
      views: {
        get scatter() {
          return scatter;
        }
      },
      actions: {
        setScatter(value) {
          scatter = value;
        }
      }
    };
  });

When the model property currentState changes from string to object, MST throws.

xaviergonz commented 5 years ago
const TInactive = types.model({
  inactive: types.string
})

const CurrentState = types.union("loading", "active", "inactive", TInactive)

export const Identity = types
  .model({
    name: types.string,
    currentState: types.optional(CurrentState, identityMachine.initialState.value)
  })

However I'd move indentityMatchine to the env of the node at creation time, or to a volatile, but up to you

rudijs commented 5 years ago

@xaviergonz Sweet! Thank you very much.

I was very stuck, your reply nails it plus offers up some flexibility on the modeling approachs I can now take.

rudijs commented 5 years ago

@xaviergonz Of the serveral approaches possible and following your suggested solution, I'm currently happy with this working solution:

There are states and nested states. Nested states allow more fine tuned and informative app/component states.

So I've got these two types separate for code clarity:

export const identityMachine = Machine(stateChart);

// states
const Tstates = types.enumeration(["idle", "loading", "active", "inactive"]);

// nested states
const TnestedStates = types.model({
  inactive: types.string
});

export const Identity = types
  .model({
    name: types.string,
    currentState: types.union(Tstates, TnestedStates)
  })

the identityMachine is exported so with the MST is instantiated in index.js (main) it's done like so:

import { Identity, identityMachine } from "./models/Identity";

const identityState = Identity.create({
  name: "Get Scatter",
  currentState: identityMachine.initialState.value
});

identityState is now the MST that is used in the React app.

For that I'm using mobx-react's Provider and inject for different components as required.