Mobx: How do you organize state-related code?

Created on 13 Feb 2017  路  9Comments  路  Source: mobxjs/mobx

I'd like to discuss a little about how best to organize state management code, for example in React application, with good scalability in terms of number of files and functions-per-file, and with clear import chain.

My concerns are:

  1. If I put all in a single state object, it quickly becomes unmanageable as the app grows.
  2. I can't subdivide the state in actions vs computations (Redux style) in a clean way.
  3. If I subdivide the state in a modular way with separation of concerns (every state has its values, computed values and actions) I eventually need to have one value in another state.
  4. I could create a different state whenever I need a value computed from two different states' values, but seems like overkill?
    This is what I'm currently experimenting with.

What are your thoughts about this, and how do you solve the problem?

(Or: is there an easy solution I'm entirely missing? 馃槃)

Most helpful comment

I've shared some insights at a recent meetup as well: https://www.youtube.com/watch?v=uWIT9M95mqQ, http://mobx-patterns.surge.sh/#44 (needs keyboard keys for navigation)

All 9 comments

'It depends' of course, but generally my state is a tree which mirrors the natural hierarchy of the application. I tend to have a root state object whose properties are other state objects.

class ItemModel {
     @observable public name: string;
}

class FilterViewState {
    @observable public text: string = '';
    @action public setText(text) {
        this.text = text;
    }
}

class AppState {
    public readonly filter = new FilterViewState();
    @observable.shallow public items: Array<ItemModel> = [];
    @computed public get filteredItems() {
        return this.items.filter((item) => item.name.includes(this.filter.text));
    }
}

const appState = new AppState();

More generally, I seem to end up having 'view state' observable classes (which define the state of the application - generally tied to a set of React components) and 'model' classes which contain business data. This is the directory structure this leads to:

- components
- state
  - models
    - itemmodel.ts
  - viewstate
    - filterviewstate.ts
  - appstate.ts

Hello @jamiewinder, thank you for your response, it's quite useful.

Have you ever had the problem of needing some observable value in a computed getter, but it resides in a different unreachable-from-there branch of the state?

I guess it means something is wrong in my state hierarchy, but I can't figure it out in some repeating situations, such as the state of the UI.

What I end up with (using your example as a base) is an AppState cluttered with "join" substates, such as filteredItems. They need both the substates, so I can't move them anywhere else. What would you do in this case?

This sounds a bit like when you have a circular dependency in OOP. There, and here, the solution is to create another object/class/whatever that makes use of the two other ones.

@hccampos good parallelism, that would be point 4 in my first post.

But these join-states would/could be a lot in a big application, and what about join-join-states? Do you think it's scalable?

@caesarsol

It usually fits together naturally, but there are occasions where I've had to pass a reference around to let parts of the state which access other parts. For example, passing the AppState instance to FilterViewState in my example above, similar to how a tree node might have a reference to its 'parent' or 'root'.

@jamiewinder thanks for posting your example code. You aren't using AppState as the only observable class that can be accessed by outside components, correct? FilterViewState's action will be directly called from the component. So is it only the computed values that require the combining of multiple stores that are exposed directly through AppState?

@superplussed - Yes, I think so. The relationship between the state objects is largely hidden away from the components. For example, expanding on the above:

class ItemModel {
    constructor(appState: AppState, name: string) {
        this._appState = appState;
        this.name = name;
    }
    private readonly _appState;
    @observable public name: string;
    @computed public get isSelected() {
        return this._appState.selectedItem === this;
    }
}

class FilterViewState {
    @observable public text: string = '';
    @action public setText(text) {
        this.text = text;
    }
}

class AppState {
    public readonly filter = new FilterViewState();
    @observable.shallow public items: Array<ItemModel> = [];
    @observable.ref public selectedItem: ItemModel | null = null;
    @computed public get filteredItems() {
        return this.items.filter((item) => item.name.includes(this.filter.text));
    }
    public createItem(name: string) {
        return new ItemModel(this, name);
    }
}

const appState = new AppState();

Now my ItemModel has an isSelected computed property which checks the root of the state to see if its selected. A component would still just need an ItemModel instance part of the state, despite it interrogating other parts of the state.

You could go further and have a setSelectedItem(item: ItemModel) action method on the AppState, and a select action method on the ItemModel (which would call this._appState.setSelectedItem) for example. Sometimes it makes sense to do so, but others not; for example, you mightn't necessarily want all ItemModels to be 'owned' by one AppState.

This seems to also have the advantage to forbid <Item> to know anything about the state, but only things about ItemModel, is that right?

This won't need to be touched, whatever happens to AppState:

<div>
  {appState.items.map(item => 
    <Item model={item} />
  )}
</div>

I've shared some insights at a recent meetup as well: https://www.youtube.com/watch?v=uWIT9M95mqQ, http://mobx-patterns.surge.sh/#44 (needs keyboard keys for navigation)

Was this page helpful?
0 / 5 - 0 ratings