Giter Site home page Giter Site logo

xaviergonz / mobx-keystone Goto Github PK

View Code? Open in Web Editor NEW
520.0 10.0 23.0 23.64 MB

A MobX powered state management solution based on data trees with first class support for Typescript, support for snapshots, patches and much more

Home Page: https://mobx-keystone.js.org

License: MIT License

TypeScript 87.40% JavaScript 0.44% CSS 0.10% MDX 12.06%
mobx mobx-state-tree reactive frp functional-reactive-programming state-management mobx-keystone snapshots state-tree data-trees immutability mutability

mobx-keystone's Introduction

mobx-keystone

A MobX powered state management solution based on data trees with first-class support for TypeScript, snapshots, patches and much more


Full documentation can be found on the site:

New! Y.js bindings for mobx-keystone are now available in the mobx-keystone-yjs package as well as a working example in the examples section of the online docs.

Introduction

mobx-keystone is a state container that combines the simplicity and ease of mutable data with the traceability of immutable data and the reactiveness and performance of observable data, all with a fully compatible TypeScript syntax.

Simply put, it tries to combine the best features of both immutability (transactionality, traceability and composition) and mutability (discoverability, co-location and encapsulation) based approaches to state management; everything to provide the best developer experience possible. Unlike MobX itself, mobx-keystone is very opinionated about how data should be structured and updated. This makes it possible to solve many common problems out of the box.

Central in mobx-keystone is the concept of a living tree. The tree consists of mutable, but strictly protected objects (models, arrays and plain objects). From this living tree, immutable, structurally shared snapshots are automatically generated.

Another core design goal of mobx-keystone is to offer a great TypeScript syntax out of the box, be it for models (and other kinds of data such as plain objects and arrays) or for its generated snapshots.

To see some code and get a glimpse of how it works check the Todo List Example.

Because state trees are living, mutable models, actions are straightforward to write; just modify local instance properties where appropriate. It is not necessary to produce a new state tree yourself, mobx-keystone's snapshot functionality will derive one for you automatically.

Although mutable sounds scary to some, fear not, actions have many interesting properties. By default trees can only be modified by using an action that belongs to the same subtree. Furthermore, actions are replayable and can be used to distribute changes.

Moreover, because changes can be detected on a fine-grained level, JSON patches are supported out of the box. Simply subscribing to the patch stream of a tree is another way to sync diffs with, for example, back-end servers or other clients.

Since mobx-keystone uses MobX behind the scenes, it integrates seamlessly with mobx and mobx-react. Even cooler, because it supports snapshots, action middlewares and replayable actions out of the box, it is possible to replace a Redux store and reducer with a MobX data model. This makes it possible to connect the Redux devtools to mobx-keystone.

Like React, mobx-keystone consists of composable components, called models, which capture small pieces of state. They are instantiated from props and after that manage and protect their own internal state (using actions). Moreover, when applying snapshots, tree nodes are reconciled as much as possible.

Requirements

This library requires a more or less modern JavaScript environment to work, namely one with support for:

  • MobX 6, 5, or 4 (with its gotchas)
  • Proxies
  • Symbols
  • WeakMap/WeakSet

In other words, it should work on mostly anything except it won't work in Internet Explorer.

If you are using TypeScript, then version >= 4.2.0 is recommended, though it might work with older versions.

Installation

npm install mobx-keystone

yarn add mobx-keystone

mobx-keystone's People

Contributors

airhorns avatar dangoodman avatar dependabot[bot] avatar dodas avatar gbcreation avatar hcschuetz avatar ivandotv avatar jeloagnasin avatar justin-hackin avatar kcoop avatar polcats avatar sisp avatar xaviergonz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mobx-keystone's Issues

Sandbox "tried to invoke action '...' over a readonly node" when original tree contains reaction

I've discovered a problem when the sandbox copy of a node contains a reaction. When the reaction in the original node fires and triggers an action, the sandbox attempts to apply the changes made by the action to the sandbox copy, but it appears the patches are not applied inside allowWrite(() => { ... }):

[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[Reaction@15]' MobxKeystoneError: tried to invoke action 'inc' over a readonly node

I believe the problem is caused by this onPatches listener:

this.allowWrite(() => {
applyPatches(this.subtreeRootClone, patches)
})

This is a test case (to be added to packages/lib/test/treeUtils/sandbox.test.ts) that reproduces the problem:

test("sandbox is patched when original tree with reaction changes", () => {
  @model("R")
  class R extends Model({ a: prop<A>(), n: prop<number>(0) }) {
    onInit() {
      autoDispose(
        reaction(
          () => this.a.b.value,
          () => {
            this.inc()
          }
        )
      )
    }

    @modelAction
    inc() {
      this.n++
    }
  }

  const r = new R({ a: new A({ b: new B({ value: 0 }) }) })
  const manager = sandbox(r)
  autoDispose(() => manager.dispose())

  r.a.b.setValue(10)

  expect(r.a.b.value).toBe(10)
  expect(r.n).toBe(1)

  manager.withSandbox(r, rcopy => {
    expect(rcopy.a.b.value).toBe(10)
    expect(rcopy.n).toBe(1)
    return false
  })
})

I suspect that the problem is related to the fact that a reaction is executed after an action has finished and the onPatches listener is wrapped as an action. In the code snippet I referenced above, this.allowWrite is called inside the onPatches listener, so if the reaction is executed after the listener has finished, it means the reaction is executed outside this.allowWrite. However, I tried removing

if (!isAction(listener)) {
listener = action(listener.name || "onPatchesListener", listener)
}

to prevent the listener from becoming an action and the problem was still present. At this point, I'm not sure what exactly is causing this error.

[Typescript][Babel] decorators and fields issue

Hi, thanks for the awesome work on mobx-keystone
I am really excited to move from MST for TS compatibility reasons

However, I've ran into an issue with decorators and class fields
The condensed example is: (sandbox link with full example)

@model("model")
class ExampleModel extends Model({
  /* ... */
}) {
  @modelFlow // this line throws at runtime
  asyncAction = _async(function*() { /* ... */ });
}

At the time modelFlow runs asyncAction field hasn't been defined yet
I believe the root of the problem is Babel's handling of decorators and properties (more info here)
However in TS case the above doesn't work (probably because of this babel issue)

In this case we can use mobx's decorate helper and plain assignment call of model() decorator (see commented out code in the example) but it's a little verbose

Considering that a lot of people (and CRA) nowadays use Babel to transpile their Typescript maybe decorators are not the way to go
Other concern is that decorators proposal has been pretty much redone from scratch and would not be compatible with "legacy" Typescript one

My suggestion would be getting rid of @model in favour of "name" argument of Model({ }) and shipping function alternatives of modelFlow and modelAction

Overall I'm not that sold on having decorators in mobx-keystone which is the only downside in comparison with mobx-state-tree I've experienced so far
decorate approach is alright but for me DX takes a big hit in that case

Factory pattern is hard to achieve when using 'declaration: true'

I was trying to implement a factory pattern as described here with Typescript 3.7.4.

export function createEntityStore<TEntity>(modelName: string) {
    @model(`EntityStore/${modelName}`)
    class EntityStore extends Model({
        entities: prop(() => new Map<string, TEntity>())
    }) {
    }
    return EntityStore;
}

However, when I try to export this factory function, Typescript returns the error
Return type of exported function has or is using private name 'EntityStore'. ts(4060)

Could it be that this pattern does not work with latest Typescript any more?

Common design patterns

I think it would be nice to have a section about common design patterns in the documentation. A few patterns that have come to my mind and which I'd like to discuss/verify in the process are:

  • Testable design of models, especially for models that require access to other parts of the tree using findParent etc. (#8)
  • References across arbitrary tree branches using a "registry" (similar to MST's IdentifierCache) at the root of the tree (#7)
  • Backreferences that give a referenced object access to its referencers/consumers (#6)
  • Polymorphic collections and abstract base classes/models (e.g. adding a symbol to the base class when instanceof is not possible because the base class is created in a factory)
  • Model versioning (#52)
  • ...

@xaviergonz What do you think?

Map support

Are there any plans to support Map as a valid prop type?
I could imagine something like entities: prop(() => new Map<string, MyEntity>, e => e.id) where the second argument would define a function to get the key of the item in the map.

If there are no plans to support Map or it's not possible, what would you recommend as alternative?

PS: Also the same could be said about Set.

Reactions are executed after onPatches listener returns

Related to #96, I'm wondering whether there is any argument against executing reactions inside the onPatches listener, i.e. when an action inside an onPatches listener triggers a reaction the reaction would be executed right after the action call and before the listener returns. The onPatches listener seems to be wrapped as an action

if (!isAction(listener)) {
listener = action(listener.name || "onPatchesListener", listener)
}

but even when I remove this part, reactions are still executed after the listener returns, so something else must be causing this behavior and I can't seem to find it.

This is a test case meant to be included in test/patch/patch.test.ts for the behavior I'd like to have:

test("reaction is executed inside onPatches listener", () => {
  let reactionCalls = 0

  @model("test/reactionOnPatchesListener/M")
  class M extends Model({ a: prop<number>(10), b: prop<number>(20) }) {
    onInit() {
      autoDispose(
        reaction(
          () => this.b,
          () => {
            reactionCalls++
          }
        )
      )
    }
  }

  const m = new M({})

  autoDispose(
    onPatches(m, () => {
      expect(reactionCalls).toBe(0)

      runUnprotected(() => {
        m.b = 21
      })

      expect(reactionCalls).toBe(1)
    })
  )

  runUnprotected(() => {
    m.a = 11
  })
})

This is the behavior I would have expected, but in the context of #96 I discovered that a reaction is executed after the onPatches listener returns.

Ref::id vs. Ref::$modelId

Since #50, I think Ref::id and Ref::$modelId are redundant. Should Ref::id be made a computed property that returns this.$modelId? Or should Ref::id perhaps be removed entirely?

Observables in a sandbox are not tracked outside the sandbox

A computed property which uses a sandbox to determine its return value is not tracked. The test is meant to be included in packages/lib/tests/treeUtils/sandbox.test.ts:

test("sandbox props are tracked", () => {
  const sandboxContext = createContext<SandboxManager>()

  @model("R")
  class R extends Model({ a: prop<A>() }) {
    @computed
    get times2(): number {
      return sandboxContext.get(this)!.withSandbox(this.a.b, b => {
        b.setValue(b.value * 2)
        return { commit: false, return: b.value }
      })
    }
  }

  const r = new R({ a: new A({ b: new B({ value: 0 }) }) })
  const manager = sandbox(r)
  autoDispose(() => manager.dispose())
  sandboxContext.setComputed(r, () => manager)

  const values: number[] = []
  autoDispose(
    reaction(
      () => r.times2,
      times2 => values.push(times2)
    )
  )

  r.a.b.setValue(1)
  expect(values).toEqual([2])

  r.a.b.setValue(2)
  expect(values).toEqual([2, 4])
})

This test results in the following error:

[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[Reaction@222]' Error: [mobx] Computed values are not allowed to cause side effects by changing observables that are already being observed. Tried to modify: [email protected]
    at invariant (mobx-keystone/node_modules/mobx/lib/mobx.js:94:15)
    at fail (mobx-keystone/node_modules/mobx/lib/mobx.js:89:5)
    at checkIfStateModificationsAreAllowed (mobx-keystone/node_modules/mobx/lib/mobx.js:726:9)
    at ObservableValue.Object.<anonymous>.ObservableValue.prepareNewValue (mobx-keystone/node_modules/mobx/lib/mobx.js:1044:9)
    at ObservableObjectAdministration.Object.<anonymous>.ObservableObjectAdministration.write (mobx-keystone/node_modules/mobx/lib/mobx.js:4012:31)
    at set (mobx-keystone/node_modules/mobx/lib/mobx.js:2615:17)
    at Object.set (mobx-keystone/node_modules/mobx/lib/mobx.js:2944:9)
    at BaseModel.set (mobx-keystone/packages/lib/src/model/Model.ts:250:28)
    at BaseModel.setValue (mobx-keystone/packages/lib/test/treeUtils/sandbox.test.ts:30:15)
    at BaseModel.<anonymous> (mobx-keystone/packages/lib/src/action/wrapInAction.ts:77:19)
    at executeAction (mobx-keystone/node_modules/mobx/lib/mobx.js:910:19)
    at BaseModel.res (mobx-keystone/node_modules/mobx/lib/mobx.js:902:16)
    at mobx-keystone/packages/lib/test/treeUtils/sandbox.test.ts:356:11
    at mobx-keystone/packages/lib/src/treeUtils/sandbox.ts:123:9
    at SandboxManager.allowWrite (mobx-keystone/packages/lib/src/actionMiddlewares/readonlyMiddleware.ts:94:16)
    at SandboxManager.withSandbox (mobx-keystone/packages/lib/src/treeUtils/sandbox.ts:122:32)
    at BaseModel.get times2 (mobx-keystone/packages/lib/test/treeUtils/sandbox.test.ts:355:40)
    at trackDerivedFunction (mobx-keystone/node_modules/mobx/lib/mobx.js:764:24)
    at ComputedValue.Object.<anonymous>.ComputedValue.computeValue (mobx-keystone/node_modules/mobx/lib/mobx.js:1254:19)
    at ComputedValue.Object.<anonymous>.ComputedValue.trackAndCompute (mobx-keystone/node_modules/mobx/lib/mobx.js:1239:29)
    at ComputedValue.Object.<anonymous>.ComputedValue.get (mobx-keystone/node_modules/mobx/lib/mobx.js:1199:26)
    at shouldCompute (mobx-keystone/node_modules/mobx/lib/mobx.js:684:33)
    at Reaction.Object.<anonymous>.Reaction.runReaction (mobx-keystone/node_modules/mobx/lib/mobx.js:1754:17)
    at runReactionsHelper (mobx-keystone/node_modules/mobx/lib/mobx.js:1894:35)
    at reactionScheduler (mobx-keystone/node_modules/mobx/lib/mobx.js:1872:47)
    at runReactions (mobx-keystone/node_modules/mobx/lib/mobx.js:1877:5)
    at endBatch (mobx-keystone/node_modules/mobx/lib/mobx.js:1577:9)
    at _endAction (mobx-keystone/node_modules/mobx/lib/mobx.js:963:5)
    at executeAction (mobx-keystone/node_modules/mobx/lib/mobx.js:917:9)
    at BaseModel.res (mobx-keystone/node_modules/mobx/lib/mobx.js:902:16)
    at Object.<anonymous> (mobx-keystone/packages/lib/test/treeUtils/sandbox.test.ts:375:9)
    at Object.asyncJestTest (mobx-keystone/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
    at mobx-keystone/node_modules/jest-jasmine2/build/queueRunner.js:43:12
    at new Promise (<anonymous>)
    at mapper (mobx-keystone/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
    at mobx-keystone/node_modules/jest-jasmine2/build/queueRunner.js:73:41
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

getRefsResolvingTo after loading from snapshot

There seems to be a problem with getRefsResolvingTo after loading the state from a snapshot. Specifically, I'm retrieving backreferences in a computed property which uses getRefsResolvingTo and all works fine with the "original" state, but when I load the state from a snapshot, the array of backreferences returned by the computed property is empty.

I've created a test case that demonstrates the problem, but I haven't been able to find its cause.

import { computed } from 'mobx';
import {
  fromSnapshot,
  getParent,
  getRefsResolvingTo,
  getSnapshot,
  model,
  Model,
  prop,
  Ref,
  registerRootStore,
  rootRef,
} from 'mobx-keystone';

test('getRefsResolvingTo after loading from snapshot', () => {
  @model('#56/Root')
  class Root extends Model({
    a: prop<A>(),
    b: prop<B>(),
  }) {}

  @model('#56/A')
  class A extends Model({}) {
    @computed
    public get bs(): B[] {
      return Array.from(getRefsResolvingTo(this), ref => getParent<B>(ref)!);
    }
  }

  @model('#56/B')
  class B extends Model({
    a: prop<Ref<A>>(),
  }) {}

  const aRef = rootRef<A>('aRef');

  const a = new A({});
  const b = new B({ a: aRef(a) });
  const root = registerRootStore(new Root({ a, b }));
  expect(root.a.bs).toHaveLength(1);
  expect(root.a.bs[0]).toBe(b);

  const newRoot = registerRootStore(fromSnapshot<Root>(getSnapshot(root)));
  expect(newRoot.a.bs).toHaveLength(1); // ERROR: length is 0
});

Reset function on hot-reloads using filenames

I had a mistake where I accidentally gave the same model name to two classes which worked basically but resulted in a strange error in combination with fromSnapshot that loaded the type definition of the wrong/unexpected model. Now, that error is fixed I come to think that it showed as the same error message that occurs naturally when using hot-reload in my react-native context: #65 .

What class is used in a hot-reload? Say, some changes happen and I try to restore a Snapshot: will it use the old or new class? What if the signature changed?

My thinking is that mobx-keystone could provide a feature like:

import { file } from 'mobx-keystone'

const finish = file(__filename)

@model('MyModel')
class MyModel extends Model({}) {}

finish()

If a hot-reload is triggered the all models that were previously declared within __filename are dropped and the instances of those models receive new "prototype's" - hot-reloading.

Design pattern: testable models

I've been thinking how to design model classes that are easily testable, especially unit-testable. In my experience, models easily become coupled because of parent-child relationships, so testing a model in isolation is not straightforward. For instance, I have a model with a computed property that depends on some other model instance in the tree, e.g. a parent node.

@model('myApp/Child')
class Child extends Model({ /* ... */ }) {
  @computed
  public get value(): number {
    const parent = findParent<Parent>(this, n => n instanceof Parent)
    if (!parent) {
      throw new Error('Child must have a parent of type Parent')
    }
    return parent.value * 2 // or whatever
  }
}

@model('myApp/Parent')
class Parent extends Model({
  value: prop<number>()
}) {}

Now, if I want to unit-test Child::value, I would need to mock findParent (e.g. using Jest), but since findParent is not dependency-injected, it's not that easy, is it?

@xaviergonz How would you do it? Would it help to retrieve the parent in a separate method (or getter)? E.g.:

@model('myApp/Child')
class Child extends Model({ /* ... */ }) {
  @computed
  public get value(): number {
    const parent = this.parent
    if (!parent) {
      throw new Error('Child must have a parent of type Parent')
    }
    return parent.value * 2 // or whatever
  }

  public get parent(): Parent | undefined {
    return findParent<Parent>(this, n => n instanceof Parent)
  }
}

Perhaps then Child::parent could be mocked more easily?

Optional fields inside of types.object

I have such model in MST:

const M = types.model({
    id: types.string,
    timestamp: types.string,
    user: types.model({
        is_my: false, // <--- !!!!!!!
        profile: types.model({
            first_name: types.maybeNull(types.string),
        }),
    }),
})

I tried to translate this to ks model, but there is compilation error:

class LinkMessage extends Model({
    id: tProp(types.string),
    timestamp: tProp(types.string),
    user: tProp(types.object(() => ({
        is_my: tProp(types.boolean, false), //compilation error
        profile: types.object(() => ({
            first_name: types.maybeNull(types.string),
        })),
    }))),
}) { }

Of course i can extract user type into another model class, but i think types.optional will be pretty useful in such situations.

Also, can types.object calls be nested?

Place to meet and chat

When starting with mobx-keystone I was looking for means to chat with people that already set it up before me. What about opening/joining a gitter channel?

Inheritance Issues

Hello, first, let me thank you for AWESOME package. Made my life so muche easier. I just have and issue with inheritance from Generic Models, it seems to short circuit something in Typescript. Example:

@model('my/BaseModel')
export class BaseModel extends Model({
  items: prop<string[]>(() => []),
}) {}

@model('my/ChildModel')
export class ChildModel extends ExtendedModel(BaseModel, {}) {
   foo() {
      this.items.push(); // <- LIFE IS GOOD, this.items is available
   }
}

This stops working, the only deifference is adding the generic parameter to base class name

@model('my/BaseModel')
export class BaseModel<T> extends Model({
  items: prop<string[]>(() => []),
}) {}

@model('my/ChildModel')
export class ChildModel extends ExtendedModel(BaseModel, {}) {
   foo() {
      this.items.push(); // <- LIFE IS BAD, this.items is NOT available
   }
}

Design pattern: backreference

Imagine the following case:

@model("myApp/A")
class A extends Model({
  b: prop<Ref<B> | undefined>()
}) {}

@model("myApp/B")
class B extends Model({}) {}

A.b is a reference of B, but B doesn't know which As reference it.

I see the following options now:

  1. Get all instances of A from its store(s) and filter those that have b.current === this:

    @model("myApp/B")
    class B extends Model({}) {
      @computed
      public get a(): A[] {
        return /* get all instances of A */.filter(a => a.b.current === this)
      }
    }

    Pros:

    • Computed property, so cannot be outdated

    Cons:

    • Recomputes every time any instance of A changes
    • Needs to iterate over all instances of A anywhere in the tree (could be many)
    • How to get all instances when there is not a single store for instances of A?
  2. Set the backreference "manually" from the reference callbacks:

    @model("myApp/B")
    class B extends Model({}) {
      @observable
      public readonly a: A[] = []
    
      @action
      public addA(a: A): void {
        this.a.push(a)
      }
    
      @action
      public removeA(a: A): void {
        const index = this.a.indexOf(a)
        if (index !== -1) {
          this.a.splice(index, 1)
        }
      }
    }
    
    const bRef = customRef<B>("myApp/BRef", {
      // ...
      
      onResolvedValueChange(ref, newB, oldB) {
        // Problem: `a` can be undefined, e.g., if `ref` has already been detached.
        const a = findParent(ref, parentNode => parentNode instanceof A)
        if (oldB) {
          oldB.removeA(a)
        }
        if (newB) {
          newB.addA(a)
        }
        if (oldB && !newB) {
          // If the value we were referencing disappeared then remove the reference
          // from its parent.
          detach(ref)
        }
      },
    })

    Pros:

    • No computed property that needs to iterate over possibly many instances of A to filter for the right ones

    Cons:

    • Is there a risk that the backreference gets out of sync because we're managing it manually?
    • The above example doesn't even work because when the reference in A.b gets detached, then a is undefined and we cannot remove it from B.a.

Are there other options and what do you think about the ones above?

[Poll] Time for v1.0.0?

After the latest PR to remove "$" from paths I don't foresee big changes to the API for the time being. Also most issues seem to have been resolved now. Do you think it is time to brand it as v1.0.0?

Retreiving the modelType by class

To avoid mistakes and provide better logging it would be nice if the class instance of Models would have a $modelType property and print the output beautifully:

@model('com/myModel')
class MyModel extends Model({
 name: prop(() => 'hello')
}) {
  x: number // not-stored-properties not rendered
}

console.log(MyModel.$modelType) // com/myModel
console.log(MyModel)
// [constructor MyModel#com/myModel]

const inst = new MyModel({})
console.log(inst)
// [MyModel#com/myModel$modelId { name: "hello" }]

Only use "prop" with primitive values and never with object values

The documentation of props states:

* You should only use this with primitive values and never with object values
* (array, model, object, etc).

But in the "Flows (async actions)" section of the docs, the book store example has a prop<Book[]>(() => []) prop:

@model("myApp/Auth")
class BookStore extends Model({
  books: prop<Book[]>(() => []),
}) {
  // ...
}

Are non-primitive types in prop in fact allowed and if not, how should a non-primitive prop be declared, e.g. a nested object prop for which I don't want to define a separate class/model?

Design pattern: versioning

I've been thinking how to deal with model versioning. The scenario I'm looking at is saving a snapshot of the current state in local storage, so when the browser is closed and re-opened (or the app is reloaded), the previous state is automatically loaded again. All is good as long as the state layout does not change. But what happens when a snapshot is saved, I change a model class (or multiple model classes, or the entire state layout), re-deploy the app, and then the snapshot, which has the old layout, is loaded by the new state implementation? A way to migrate from one/any (old) snapshot version to the current version is needed, I think.

A couple of questions I've been asking myself:

  • Should models have version information (e.g. semver-style versions)?
    • What is the version of a parent model class when the version of one of its child model classes changes?
    • Would each model class implement migration code in its fromSnapshot method?
  • Should the root store have a version?
  • How and where is migration code implemented for complex layout changes?

Error: assertion failed: value is not ready to take a parent

After upgrading to v0.17, I'm getting the following error:

mobx-keystone.esm.js:110 Uncaught Error: assertion failed: value is not ready to take a parent
    at failure (mobx-keystone.esm.js:187)
    at mobx-keystone.esm.js:2017
    at executeAction (mobx.module.js:713)
    at res (mobx.module.js:701)
    at tryUntweak (mobx-keystone.esm.js:3251)
    at Array.interceptObjectMutation (mobx-keystone.esm.js:3428)
    at interceptChange (mobx.module.js:3202)
    at ObservableObjectAdministration.write (mobx.module.js:4393)
    at _set (mobx.module.js:2833)
    at Object.set (mobx.module.js:3156)
    at BaseModel.set (mobx-keystone.esm.js:5166)
    at BaseModel.setProps (p.ts:29)
    at executeAction (mobx.module.js:713)
    at BaseModel.res (mobx.module.js:701)
    at BaseModel.wrappedAction (mobx-keystone.esm.js:1750)
    at BaseModel.update (o.ts:121)
    at update (n.ts:72)
    at BaseModel.onAttachedToRootStore (n.ts:89)
    at executeAction (mobx.module.js:713)
    at BaseModel.res (mobx.module.js:701)
    at BaseModel.wrappedAction (mobx-keystone.esm.js:1750)
    at mobx-keystone.esm.js:1894
    at walkTreeParentFirst (mobx-keystone.esm.js:1838)
    at walkTree (mobx-keystone.esm.js:1831)
    at attachToRootStore (mobx-keystone.esm.js:1891)
    at mobx-keystone.esm.js:2083
    at executeAction (mobx.module.js:713)
    at res (mobx.module.js:701)
    at internalTweak (mobx-keystone.esm.js:3184)
    at executeAction (mobx.module.js:713)
    at res (mobx.module.js:701)
    at interceptArrayMutation (mobx-keystone.esm.js:3095)
    at interceptChange (mobx.module.js:3202)
    at ObservableArrayAdministration.spliceWithArray (mobx.module.js:3390)
    at Proxy.push (mobx.module.js:3544)
    at BaseModel.add (g.ts:91)
    at executeAction (mobx.module.js:713)
    at BaseModel.res (mobx.module.js:701)
    at BaseModel.wrappedAction (mobx-keystone.esm.js:1750)

There's no problem with v0.16. I suspect the cause of this problem has been introduced in #24. I'm calling an action of a child model instance in some parent model's onAttachedToRootStore which replaces the current (empty) array of SomeModel instances (prop<SomeModel[]>()) with a new array of SomeModel instances. I haven't had time to come up with a minimal example for reproduction. If you can't guess what might be causing this problem, I'll try to create a test case for you.

Incorrect (inverse) patches when model action in onAttachedToRootStore

When a model action is performed in onAttachedToRootStore, the order in which onPatches emits patches is unexpected (to me) and the order in which it emits inverse patches is wrong, i.e. it is impossible to revert the changes by applying inverse patches (in reverse order - see #84).

I believe that model actions performed in onAttachedToRootStore should trigger a patch after, e.g., the patch for adding a model instance to a tree (with a root store) is triggered. This issue may be related to #76.

This is a test case that demonstrates the problem:

test("patches with action in onAttachedToRootStore", () => {
  @model("test/patchesWithActionInOnAttachedToRootStore/M")
  class M extends Model({ value: prop<number>(0) }) {
    onAttachedToRootStore() {
      this.setValue(1)
    }

    @modelAction
    setValue(value: number) {
      this.value = value
    }
  }

  @model("test/patchesWithActionInOnAttachedToRootStore/R")
  class R extends Model({ ms: prop<M[]>(() => []) }) {
    @modelAction
    addM(m: M) {
      this.ms.push(m)
    }
  }

  const r = new R({})
  autoDispose(() => unregisterRootStore(r))
  registerRootStore(r)

  const sn = getSnapshot(r)

  const rPatches: Patch[][] = []
  const rInvPatches: Patch[][] = []
  autoDispose(
    onPatches(r, (ptchs, iptchs) => {
      rPatches.push(ptchs)
      rInvPatches.push(iptchs)
    })
  )
  expect(rPatches).toMatchInlineSnapshot(`Array []`)
  expect(rInvPatches).toMatchInlineSnapshot(`Array []`)

  r.addM(new M({}))

  runUnprotected(() => {
    rInvPatches
      .slice()
      .reverse()
      .forEach(invpatches => applyPatches(r, invpatches, true))
  })
  expect(getSnapshot(r)).toStrictEqual(sn)
})

tcProp confusion

All examples in tProp.ts mention tcProp, which IIUIC doesn't exist (I guess, it's how tProp used to be called earlier).


Another thing: There's a tProp overload stating that "You should only use this with primitive values and never with object values". As they're just three types useful in this place, would it be possible to split the declaration to cover exactly these types, so that errors like

badArray: tProp(types.array(types.string), []), // must be () => []

could be avoided?

Better error messages on snapshot mismatch

When I try to apply a snapshot for some reason I get an error message stating:

snapshot model type 'consento/User' does not match target model type 'consento/User'

Which is fine as I probably have a mistake somewhere but it would be helpful to know where the mismatch happened as its probably a small issue in the case at hand.

Model types in properties

I was just pointed in the direction of this project because I mentioned wanting to build something similar myself. I took a quick look, read some docs and want to say you (singular? Plural?) did a good job!

For now I only have two questions:

  • Are you on slack/gitter somewhere?
  • Why aren't types specified as properties, with decorators instead of a function to extend from?

Thanks for building this, I'm looking forward to working with it.

Model data syntax

I'm a very new to this project and I love pretty everything, except for one thing: the two syntaxes for model data. I prefer to use the one providing optional type-checking (as it may get useful when things break and I have no clue what's going on), but it's pretty verbose. It's more verbose than with MST and that's IMHO a big drawback.

I guess, plain done: false can't work, but couldn't there be just done: tProp(false) instead of done: tProp(types.boolean, false)?

Am I right, you're using the simpler syntax? I guess, with lots of experience, relying on typescript is pretty safe...

Missing btoa when I'm trying to use this on react-native

I'm trying to use this on our monorepo which includes a react-native repo because I'm tired of trying to make mobx-state-tree to work with TS ๐Ÿ˜‚ but this library assumes that there's a btoa or Buffer defined in the global variable.

I've setup a simple repo where the issue exists.

In App.tsx in the repo above, importing createRootStore doesn't work because it will immediately initialize the localBaseId which requires the btoa or Buffer. So I have to polyfill it first before importing.

This doesn't work:

import { createRootStore } from "./src/store"
import { TodoListView } from './src/TodoListView';

global.Buffer = global.Buffer || require('buffer').Buffer;

This works:

import { TodoListView } from './src/TodoListView';

global.Buffer = global.Buffer || require('buffer').Buffer;
const { createRootStore } = require('./src/store');

Invalid snapshot when model prop changed in onAttachedToRootStore

I think a test case that reproduces this problem explains it best:

import {
  getSnapshot,
  Model,
  model,
  modelAction,
  prop,
  registerRootStore,
} from 'mobx-keystone';

test('snapshot', () => {
  @model('#74/A')
  class A extends Model({
    value: prop<number>(),
  }) {
    public onAttachedToRootStore() {
      this.value = 1; // but, e.g, this works: setTimeout(() => { this.value = 1; })
    }
  }

  @model('#74/Store')
  class Store extends Model({
    all: prop<A[]>(() => []),
  }) {
    @modelAction
    public add(a: A): void {
      this.all.push(a);
    }
  }

  const store = registerRootStore(new Store({}));
  store.add(new A({ value: 0 }));

  expect(getSnapshot(store).all).toHaveLength(store.all.length);
});

The state is correct, but the snapshot is wrong. Unfortunately, I haven't been able to determine the cause of this error.

[Question] Is there a way to get the model definition at runtime

This library looks really good! I'm currently building an application using mobx-state-tree, but running into a lot of trouble especially with Typescript. I was looking into switching to mobx-keystone, but I can't get one thing to work:

Is there a way to get the model definition for a ModelClass at runtime? I looked into the source code, but it seems that all the interesting stuff is not available outside the library itself, is that right?

So, basically what I'd like to be able to do is something like:

@model('Test')
class Test extends Model({
  name: tProp(types.string)
}) {}

Test.getProperties().forEach((prop, key) => {
   // [key] would be 'name'
   // [prop] would be the tProp instance
})

Is there any way to do this? Thanks!

Lookahead / Testing changes to a tree without actually committing them

I frequently need to test how a change somewhere in the tree affects the rest of the tree. For instance, while editing a text field in a form, I validate the entered value, but the validation routine has (generally speaking) arbitrary dependencies across the tree. In some cases, I don't want the changes to take effect immediately, but I'd rather like to test what the effect would be if the changes were actually applied to the tree.

To achieve this goal, I currently clone the tree, apply the changes to the cloned tree, and check the result. E.g.:

const rootPath = getRootPath(myModel)
const rootClone = registerRootStore(clone(rootPath.root, { generateNewIds: false }))
const myModelClone = resolvePath<typeof myModel>(rootClone, rootPath.path).value!

// Make changes to `myModelClone`.
// Check result of the changes.

unregisterRootStore(rootClone)

While this approach works fine in terms of correctness, I'm faced with performance issues (my tree is not even big at the moment). It seems that fromSnapshot is quite expensive which adds up when executing the above snippet many times per second.

I've been thinking about possible solutions:

  1. Debounce or throttle execution of the above snippet.
  2. Find a way to avoid cloning the entire tree, perhaps using proxies along the lines of Immer drafts. The idea is to make changes to the proxied tree which behaves as if it was the original tree, but the changes are not actually applied to the original tree. I'm not sure though whether/how this can be done with mobx/mobx-keystone because class instances may hold references to other class instances and also these references would need to be replaced by proxies, I guess.

@xaviergonz Do you have any experience with this kind of situation and advice how to deal with it?

Abstract class property and ExtendedModel in CRA app causes error: "Error: data changes must be performed inside model actions"

I'm afraid #17 didn't solve everything.

abstract class A extends Model({}) {
  // Comment out the line below and it works fine!
  public abstract value: number;
}

@model("B")
class B extends ExtendedModel(A, { value: prop<number>() }) {}

// Error: data changes must be performed inside model actions
new B({ value: 1 }); // Instantiating the class inside `runUnprotected` works.

Running the above snippet inside a CRA app results in the following error:

Error: data changes must be performed inside model actions

When running the snippet with ts-node directly, there is no problem.

How to implement optional values?

I have a following store, where message is only optional:

@model('geodas/Modal')
export class ModalStore extends Model({
  message: tProp(types.model<ModalMessage | null>(ModalMessage), null)
}) {}

when I initialise this store new ModalStore() I receive an error that the message is not of type ModalMessage.

Is there any way how to do this?

Bug: Preserve static properties when wrapping with model decorator

PR incoming in a sec here are the failing tests:

test("model decorator preserves static properties", () => {
  @model("BarSimple")
  class Bar extends Model({}) {
    static foo = "foo"
  }

  expect(Bar.foo).toBe("foo")
})

test("model decorator preserves static property getters", () => {
  @model("BarWithGetter")
  class Bar extends Model({}) {
    static sideEffectCount = 0
    static get foo() {
      return Bar.sideEffectCount++
    }
  }

  expect(Bar.foo).toBe(0)
  expect(Bar.foo).toBe(1)
})

test("model decorator works with static proxy gymnastics", () => {

  class Bar extends Model({}) { }

  // @ts-ignore
  Bar = new Proxy(Bar, {
    get: (target, key: keyof typeof Bar | "foo") => {
      if (key === "foo") return "oof";
      return target[key];
    }
  })

  // @ts-ignore
  Bar = model("BarWithProxyStuff")(Bar)

  // @ts-ignore
  expect(Bar.foo).toBe("oof")
})

Afaict there shouldn't any problem in preserving static properties, I'm using them to store meta data about the model. Also at the moment TypeScript doesn't give error when accessing static properties even though they don't exist in runtime.

Model factory pattern: ModelProps type issues

I'm trying to migrate my entities store pattern into mobx-keystone with typescript and it's pretty hard for me since my ts isn't really good.

Here is my entities store factory:
https://t.co/dKnklXwgnF

Here is usage:
https://t.co/4kpAzmAxCZ

It receives just a collection store I defined here: https://t.co/M0JNTErYQL

The problem with that part. It creates a map with all my collection stores and passes it as model props:
https://t.co/uSqmius1zF

But seems like that part isn't working since ts doesn't resolve todos prop of my entities store which is one of my collection stores:
https://t.co/rImQry1YES

Maybe you have some suggestions on how to trick it?

Design pattern: model registry

References in mobx-state-tree were designed to be transparent to the user. Maybe way too transparent, up to a level where thereโ€™s no way, for example, to get the parent of a reference node. In mobx-keystone references are explicit objects, thus making for example this use-case trivial.

Source: "mobx-keystone, an alternative to mobx-state-tree without some of its pains"

But how are model instance references resolved in mobx-keystone when the model store location is non-trivial to access, e.g. when model instances are not stored in a single model store with a well-known location? MST implements transparent reference resolution by means of the IdentifierCache which is a fully-managed collection of all model instances within the tree and lives at the root of a tree.

The "Reference to single selected Todo" example does not require a central model instance registry as selectedRef lives in TodoList, which is the store of all Todo items, so the location of Todo items for reference resolution is simple. But if Todo was referenced from an arbitrary location in the tree and/or there wasn't only a single Todo store, I think a Todo registry at the root of a tree could be a general pattern to addressing this problem.

@model('myApp/Todo')
class Todo extends Model({
  id: prop<string>(),
  // ...
}) {
  public onAttachedToRootStore() {
    // find the root model and throw an error if it does not have the expected type
    const root = getRoot(this)
    if (!(root instanceof Root)) {
      throw new Error('Todo expects the root of the tree to be of type Root')
    }
    // add this Todo instance to the registry
    root.todoRegistry.add(this)
    // remove this Todo instance from the registry when it is detached from the tree
    return () => void root.todoRegistry.remove(this)
  }

  // ...
}

const todoRef = customRef<Todo>("myApp/TodoRef", {
  // ...
  resolve(ref) {
    // get the root of the tree
    const root = getRoot(this)
    // if the ref is not yet attached to the root then it cannot be resolved
    if (!(root instanceof Root)) {
      return undefined
    }
    // but if it is attached then try to find it
    return root.todoRegistry.get(ref.id)
  },
  // ...
})

@model('myApp/Root')
class Root extends Model({}) {
  public readonly todoRegistry = new Registry<Todo>()
}

class Registry<T extends { id: string }> {
  private readonly byId = observable.map<string, T>()

  @computed
  public get items(): readonly T[] {
    return values(this.byId) // import { values } from 'mobx'
  }

  @action
  public add(item: T): void {
    this.byId.set(item.id, item)
  }

  @action
  public remove(item: T): void {
    this.byId.delete(item.id)
  }

  public get(id: string): T | undefined {
    return this.byId.get(id)
  }
}

@xaviergonz What do you think?

Dedicated Patch type for each operation

fast-json-patch has clear type definitions for Patches. Would it be a good idea to replace the current Patch interface to look like โ†“ ?

export declare type Patch = AddOperation<any> | RemoveOperation | ReplaceOperation<any>
export interface BaseOperation {
    path: string[]
}
export interface AddOperation<T> extends BaseOperation {
    op: 'add'
    value: T
}
export interface RemoveOperation extends BaseOperation {
    op: 'remove'
}
export interface ReplaceOperation<T> extends BaseOperation {
    op: 'replace'
    value: T
}

Batching actions

I'm not sure if I've just missed something in mobx docs.

I have several cases where I might do something like this:

@model('SomeModelCollection')
class SomeModelCollectionModel extends Model({
	collection: prop(() => objectMap<SomeModel>()),
}) {
	@modelAction
	add(id: string, model: SomeModel) {
		// some additional logic
		this.collection.set(id, model);
	}
}

// some reaction
reaction(() => rootStore.entities.someModel.collection.values(), () => console.log('Reaction');

// somewhere in my code
const arr: ISomeModel[] = ... // array with models
const collection = getRoot(this).entities[collectionName];

// will it be batched?
arr.forEach(modelPayload => {
	const model = new SomeModel(modelPayload);
	collection.add(model.id, model);
});

I'm curious will my forEach block trigger a single reaction call and/or snapshot? Since each collection.add call is the model action call and I'm changing the values of the map.

How to properly type model creation data?

UPDATE: For everyone looking for a specific type of model creation data โ€“ use ModelCreationData<T>

I'm still figuring out how to properly do some things in TS, so I'm stuck typing the server response which I'm going to pass as the model data. Just something like SnapshotIn<typeof TodoModel> in MST. SnapshotInOf from mobx-keystone isn't the right thing since it requires the modelTypeKey to be in the object itself, here is example:

@model('Todo')
export class TodoModel extends Model({
  id: prop<string>(() => uuid()),
  text: prop<string>(),
  completed: prop<boolean>(false),
}) {

}

// this throws an error the modelTypeKey is missing
const snapshot: SnapshotInOf<TodoModel> = {
  text: 'test',
}

const m = new TodoModel(snapshot);

add onJSONPatches

I am using onPatches to store the changes in a log. Doing that I need to process the patches to fit the json patches standard. Namely: change the path from array to / joined and prefixed string. I wonder if its a good idea to add a new "onJSONPatches" api that provides this out-of-the box or alternatively a .toJSON() method on Patch that would do the same thing?

Ability to create a reference automatically from snapshot like in MST?

I was playing with the mobx-keyston and trying different things I was able to do int MST a find a thing which forces me to rewrite a lot of my MST application code to handle it.

In short, I'm using normalizr a lot which allows me to normalize nested responses. It generates me nice normalized structure like this:

const response = [
  {
    id: '1',
    title: 'Buy',
	// normalizr leaves the id here
	// the object itself will be placed in entities
    todos: {
  		id: '1',
  		text: 'Apple',
	}, 
  },
];

// normalized using Group scheme
const normalized = {
	// the array of all groups ids
	result: ['1'],
	// all the objects
	entities: {
		groups: {
			'1': {
				id: '1',
				title: 'Buy',
				// normalizr leaves the id here
				// the object itself will be placed in entities
				todos: ['1']
			}
		},
		todos: {
	 		'1': {
	  			id: '1',
	  			text: 'Apple',
			}, 
		}
	},
}

And here is the main problem. Check out fetchGroups action:

@model('RootStore')
export class RootStore extends Model({
  todos: prop(() => objectMap<TodoModel>()),
  groups: prop<GroupModel[]>(() => []),
}) {
  @modelAction
  fetchGroups() {
    const todoNode = new TodoModel(normalized.entities.todos[1]);
    this.todos.set(todoNode.id, todoNode);

    // this won't work because we haven't created references
    this.groups = normalized.results.map((id) =>
      new GroupModel(normalized.entities.groups[id])
	);

	// but this one should work just fine
	this.groups = normalized.results.map((id) =>
      new GroupModel({
        ...normalized.entities.groups[id],
        todos: normalized.entities.groups[id].todos.map(todoRef),
      })
    );
  }
}

const rootStore = new RootStore({});

rootStore.fetchGroups();

Also, rest of the code here:

@model('TodoModel')
export class TodoModel extends Model({
  id: prop<string>(() => uuid()),
  text: prop<string>(''),
}) {}

const todoRef = customRef<TodoModel>('todo ref', {
  getId(ref) {
    return ref.id;
  },

  resolve(ref) {
    return getRoot<RootStore>(ref).todos.get(ref.id);
  },
});

@model('Group')
export class GroupModel extends Model({
  id: prop<string>(() => uuid()),
  title: prop<string>(''),
  todos: prop<Ref<TodoModel>[]>(() => []),
}) {}

Abstract generic base class and model

I'm trying to implement a generic base class with a generic model prop but some shared logic among all subclasses (here the error getter):

import { computed } from 'mobx';
import { model, Model, prop } from 'mobx-keystone';

abstract class A<P> extends Model({
  value: prop<P>(), // Base class expressions cannot reference class type parameters. ts(2562)
}) {
  public abstract validate(value: P): string | undefined;

  @computed
  public get error(): string | undefined {
    return this.validate(this.value);
  }
}

@model('B')
class B extends A<string> {
  public validate(value: string): string | undefined {
    return value.length < 3 ? 'too short' : undefined;
  }
}

How would you implement this instead? Using the new ExtendedModel factory doesn't seem to help because I wouldn't be able to declare the generic prop and abstract validate method in A, so the error getter couldn't be implemented already in A.

Any help is much appreciated. :-)

[Poll] should default values also be used when null is found?

Right now default values are only applied when undefined is found, e.g.

prop(42) will turn undefined to 42
tProp(types.number, 42) will turn undefined to 42

but there's no way to do it for null
tProp(types.maybeNull(types.number), 42) will turn undefined to 42, but will leave null as is
prop<number | null | undefined>(42) same

so the question is, should a default value transform both undefined AND null into the default value? e.g.

prop(42) will turn null and undefined into 42
tProp(types.number, 42) will do the same

it would still be ok to have no default value as usual
tProp(types.maybe(types.number)) both number and undefined are accepted and left as is, null is not valid
tProp(types.maybeNul(types.number)) both number and null are accepted and left as is, undefined is not valid

applySnapshot messes with onAttachedToRootStore

This might be related to #27.

/ Short

Last night I tried to figure out why I got following execution order:

.onAttachedToRootStore
.onAttachedToRootStore
(detachMethod)

and it seems that applySnapshot will mess with the onAttachedToRootStore hooks.
I worked around it by only counting how many calls to either are made and only execute onAttached... the first time and only execute detach once its called at an equal amount as the onAttached.. amount. But it would be nice if it would be executed in order.

/ Long

In my code I have a User Model that is restored/persisted in the .onAttachedToRootStore hook. When the app is started, the user is created and attached to the rootStore. Once all data is loaded .applySnapshot is called to restore the user. There is also a Vault child of User that will be restored with the user.
There is also a VaultData model that is to be restored/replicated from a different physical location when attached to the root store. The VaultData is conceptually linked Vault as Vault.dataKeyHex contains the storage location of the VaultData. So: once there is a new Vault, I need to also create/restore the VaultData with it. In a previous version I tried using onInit to restore the data. (more about the current version later). It worked somehow but I noticed that the VaultData.onAttachedToRootStore method was called more than once, screwing with my restore logic (the store was restored twice). I attempted to make sure that in it can be destructed at any time and still I ran into weird states.

... so i tried to debug mobx-keystone...

I figured out that both onAttachedToRootStore calls are triggered by this block:

enqueuePendingAction(() => {
detachFromRootStore(value)
})
}
if (newRootStore) {
enqueuePendingAction(() => {
attachToRootStore(newRootStore, value)
})

where the listener execution is added to a queue, which is filled twice for the same object before being executed with applySnapshot. I noticed that when executing on... here

const disposer = ch.onAttachedToRootStore!(rootStore)
if (disposer) {
onAttachedDisposers.set(ch, disposer)

before the execution onAttachedDisposers.get(ch) was already defined, causing in an overwrite (and loss) of the previous disposer without it ever being called.

I didn't find a good way for a test yet but I worked around this by storing the VaultData in a separate rootStore, counting how often .onAttachedToRootStore is called on the Vault and only running it the first time while registering the destruction to be called once the counter is back to 0.

Now this workaround somehow does the job, even if it feels fishy, but it would be good if .onAttachedToRootStore was called only once, or alternatively if the destruct method to be called before the next onAttachedToRootStore call.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.