npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

dsabanin-classy-mst

v3.8.1

Published

ES6-like syntax for mobx-state-tree

Downloads

2

Readme

classy-mst

build status npm version

classy-mst is the ultimate state management solution for TypeScript and JavaScript apps: a light wrapper around the amazing mobx-state-tree to allow standard ES6 syntax.

ES6 class methods become "views" or "actions" (when decorated with @action to indicate they have side effects). Then:

  • Changes automatically propagate through views.
  • State is protected from modification outside actions.
  • State and state diffs (patches) are serializable to JSON and replayable for undo / redo.
  • Redux DevTools are supported for working with the state.
    • The underlying technology is still MobX.

mobx-state-tree provides the state management, classy-mst adds the benefits of standard ES6 syntax:

  • Less boilerplate.
  • this, super and inheritance work as you would expect.
  • No lock-in, easier to switch to other technology if needed.

Note: Old versions 1.x work with mobx-state-tree 2.x. Now the major versions are kept in sync.

Contents

Usage

Install:

npm install --save mobx mobx-state-tree classy-mst

Use in your code:

import { types } from 'mobx-state-tree';
import { mst, shim, action } from 'classy-mst';

const TodoData = types.model({

	title: types.string,
	done: false

});

class TodoCode extends shim(TodoData) {

	@action
	toggle() {
		this.done = !this.done;
	}

}

const Todo = mst(TodoCode, TodoData, 'Todo');

ES6 methods become views (assumed to have no side-effects) unless decorated with @action, which turns them into actions.

Afterwards, Todo is a regular MST type. Here, TodoData is an MST type holding the properties with MobX state tracking magic, and TodoCode is only a block of code with methods (views and actions) to use with the type.

The mst function binds the two together (producing a new type "inheriting" TodoData), and the TodoCode class should not be used directly. A third, optional parameter gives the resulting model a name. Names are required for polymorphism to work correctly, when serializing models to JSON containing fields with types that have further subclasses.

The shim function is a tiny wrapper that makes TypeScript accept MST types as superclasses. It must be used in the extends clause of the ES6 class defining the views and actions.

The major differences compared to ordinary ES6 classes are:

  • this instanceof Class is false inside Class, because this refers to a MobX state tree node.
  • Class properties must be declared using MST type syntax in a separate block before the class.
  • MST has no static properties.

You can look at the tests for fully working examples, or run them like this:

git clone https://github.com/charto/classy-mst.git
cd classy-mst
npm install
npm test

Inheritance

You can inherit from and extend other classes wrapped in MST types as follows:

// Inherit Todo and add new count property.

const SpecialTodoData = Todo.props({
	count: 0
});

// Original MST type "Todo" containing the wrapped methods
// is needed by shim for binding references to "super".

class SpecialTodoCode extends shim(SpecialTodoData, Todo) {

	@action
	toggle() {
		console.log('Toggled ' + (++this.count) + ' times!');
		super.toggle();
	}

}

const SpecialTodo = mst(SpecialTodoCode, SpecialTodoData, 'SpecialTodo');

If adding new properties to the superclass, it's important to pass the unmodified superclass as the second parameter to shim so that super is initialized correctly.

Inheritance support combined with hot reload or late types can cause strange errors such as stack overflow. To fix them, a class can be sealed to disable inheritance entirely:

const Todo = mst(TodoCode, TodoData, 'Todo', { sealed: true });

Polymorphism

Instances of subclasses can be used in place of their parent classes inside models. Due to mobx-state-tree implementation internals, both classes must have been defined before the first parent class instance has been created anywhere in the program.

Snapshots containing polymorphic types require type names in the serialized JSON, to identify the correct subclass when applying the snapshot. A special key $ is automatically added in snapshots when an object in the tree belongs to a subclass of the class actually specified in the model.

The default key $ for types can be changed by passing a different string to the setTypeTag function before creating any model instances. For example:

import { getSnapshot } from 'mobx-state-tree';
import { setTypeTag } from 'classy-mst';

setTypeTag('type');

const Store = types.model({
	todos: types.array(Todo)
});

const store = Store.create({
	todos: [
		SpecialTodo.create({ title: 'Baz' })
	]
});

console.log(getSnapshot(store));

The above prints:

{ todos: [ { title: 'Baz', done: false, count: 0, type: 'SpecialTodo' } ] }

Getters and setters

Class members with getters become MobX computed properties. Setters are not considered actions themselves, so they're only allowed to modify internal state by calling other methods decorated with @action.

For example:

class TodoCode extends shim(TodoData) {

        @action
        toggle() {
                this.done = !this.done;
        }

        get pending() {
                return(!this.done);
        }

        set pending(flag: boolean) {
                if(this.done == flag) this.toggle();
        }

}

Volatile state

You can create a model with volatile state directly using mobx-state-tree syntax:

const VolatileData = types.model({}).volatile(
	(self) => ({ a: 1 })
);

Alternatively, for most types of volatile members (not functions, however) you can define and initialize them inside the ES6 class:

class VolatileCode extends shim(VolatileData) {

	b = 2;

}

Note that the member must be initialized, or it gets compiled away and classy-mst never sees it.

Asynchronous actions

Asynchronous actions return a promise. The actual method needs to define a generator, pass it to flow from mobx-state-tree, call the returned function and return its result, like this:

import { types, flow } from 'mobx-state-tree';
import { mst, shim, action } from 'classy-mst';

const AsyncData = types.model({});

class AsyncCode extends shim(AsyncData) {

	@action
	run() {
		function* generate() {
			yield Promise.resolve('This gets lost');
			return('Returned value');
		}

		return(flow(generate)());
	}

}

const Async = mst(AsyncCode, AsyncData);

Async.create().run().then(
	(result) => console.log(result)
);

Recursive types

Fully typed recursive types require some tricky syntax to avoid these TypeScript compiler errors:

  • error TS2456: Type alias 'Type' circularly references itself.
  • error TS2502: 'member' is referenced directly or indirectly in its own type annotation.
  • error TS2506: 'Type' is referenced directly or indirectly in its own base expression.
  • error TS7022: 'Type' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

If your model has a children property containing an array of the same model as their parent, the easiest solution is to add the children property only in the ES6 class and use mstWithChildren instead of mst when defining the model. It handles adding the property to the mobx-state-tree type.

The function mstWithChildren returns an object with the members:

  • Model, the model with views, actions and a children property attached.
  • Children, the correct mobx-state-tree type for the children property.

You should call it just after your class defining the views and actions (replacing Todo with your own class name) like this:

const { Model: Todo, Children } = mstWithChildren(TodoCode, TodoData, 'Todo');

You can use the Children type inside the class methods thanks to declaration hoisting. Without the type, it's difficult to initialize an unset children property correctly.

The children property should be declared in your class as (this | <class name>)[] to allow further inheritance, like this:

import { IObservableArray } from 'mobx';
import { types, isStateTreeNode, IModelType } from 'mobx-state-tree';
import { mst, mstWithChildren, shim, action, ModelInterface } from 'classy-mst';

export const NodeData = T.model({ value: 42 });
export class NodeCode extends shim(NodeData) {

	@action
	addChild(child: Node | typeof Node.SnapshotType) {
		if(!this.children) this.children = Children.create();
		this.children.push(isStateTreeNode(child) ? child : Node.create(child));

		return(this);
	}

	children?: (this | NodeCode)[];
}

const { Model: Node, Children } = mstWithChildren(NodeCode, NodeData, 'Node');
export type Node = typeof Node.Type;

If you want to use some other name than children for the property, easiest is to copy, paste and customize the mstWithChildren function from classy-mst.ts. Without macro support in the TypeScript compiler, the name cannot be parameterized while keeping the code fully typed.

License

The MIT License

Copyright (c) 2017- BusFaster Ltd