Giter VIP home page Giter VIP logo

satcheljs's Introduction

Satchel

Satchel is a dataflow framework based on the Flux architecture. It is characterized by exposing an observable state that makes view updates painless and efficient.

npm Build Status License: MIT

Influences

Satchel is an attempt to synthesize the best of several dataflow patterns typically used to drive a React-based UI. In particular:

  • Flux is not a library itself, but is a dataflow pattern conceived for use with React. In Flux, dataflow is unidirectional, and the only way to modify state is by dispatching actions through a central dispatcher.
  • Redux is an implementation of Flux that consolidates stores into a single state tree and attempts to simplify state changes by making all mutations via pure functions called reducers. Ultimately, however, we found reducers and immutable state cumbersome to deal with, particularly in a large, interconnected app.
  • MobX provides a seamless way to make state observable, and allows React to listen to state changes and rerender in a very performant way. Satchel uses MobX under the covers to allow React components to observe the data they depend on.

Advantages

There are a number of advantages to using Satchel to maintain your application state:

  • Satchel enables a very performant UI, only rerendering the minimal amount necessary. MobX makes UI updates very efficient by automatically detecting specifically what components need to rerender for a given state change.
  • Satchel's datastore allows for isomorphic JavaScript by making it feasible to render on the server and then serialize and pass the application state down to the client.
  • Satchel supports middleware that can act on each action that is dispatched. (For example, for tracing or performance instrumentation.)
  • Satchel is type-safe out of the box, without any extra effort on the consumer's part.

Installation

Install via NPM:

npm install satcheljs --save

In order to use Satchel with React, you'll also need MobX and the MobX React bindings:

npm install mobx --save

npm install mobx-react --save

Usage

The following examples assume you're developing in Typescript.

Create a store with some initial state

import { createStore } from 'satcheljs';

let getStore = createStore(
    'todoStore',
    { todos: [] }
);

Create a component that consumes your state

Notice the @observer decorator on the component—this is what tells MobX to rerender the component whenever the data it relies on changes.

import { observer } from 'mobx-react';

@observer
class TodoListComponent extends React.Component<any, any> {
    render() {
        return (
            <div>
                {getStore().todos.map(todo => <div>{todo.text}</div>)}
            </div>
        );
    }
}

Implement an action creator

Note that, as a convenience, Satchel action creators created with the action API both create and dispatch the action. This is typically how you want to use action creators. If you want to create and dispatch the actions separately you can use the actionCreator and dispatch APIs.

import { action } from 'satcheljs';

let addTodo = action(
    'ADD_TODO',
    (text: string) => ({ text: text })
);

// This creates and dispatches an ADD_TODO action
addTodo('Take out trash');

Implement a mutator

You specify what action a mutator subscribes to by providing the corresponding action creator. If you're using TypeScript, the type of actionMessage is automatically inferred.

import { mutator } from 'satcheljs';

mutator(addTodo, (actionMessage) => {
    getStore().todos.push({
        id: Math.random(),
        text: actionMessage.text
    });
};

Orchestrators

Orchestrators are like mutators—they subscribe to actions—but they serve a different purpose. While mutators modify the store, orchestrators are responsible for side effects. Side effects might include making a server call or even dispatching further actions.

The following example shows how an orchestrator can persist a value to a server before updating the store.

import { action, orchestrator } from 'satcheljs';

let requestAddTodo = action(
    'REQUEST_ADD_TODO',
    (text: string) => ({ text: text })
);

orchestrator(requestAddTodo, async (actionMessage) => {
    await addTodoOnServer(actionMessage.text);
    addTodo(actionMessage.text);
});

mutatorAction

In many cases a given action only needs to be handled by one mutator. Satchel provides this utility API which encapsulates action creation, dispatch, and handling in one simple function call.

The addTodo mutator above could be implemented as follows:

let addTodo = mutatorAction(
    'ADD_TODO',
    function addTodo(text: string) {
        getStore().todos.push({
            id: Math.random(),
            text: actionMessage.text
        });
    });

This is a succinct and easy way to write mutators, but it comes with a restriction: the action creator is not exposed, so no other mutators or orchestrators can subscribe to it. If an action needs multiple handlers then it must use the full pattern with action creators and handlers implemented separately.

License - MIT

satcheljs's People

Contributors

apneer avatar dependabot[bot] avatar dev-tim avatar ericrallen avatar fpintos avatar jarmit avatar kenotron avatar lucasavila00 avatar microsoft-github-policy-service[bot] avatar mloughry avatar msftenhanceprovenance avatar shoaibbhimani avatar smikula 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  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

satcheljs's Issues

Simplify syntax for using the satcheljs-stitch raise API

Currently raising an action via satcheljs-stitch has some awkward syntax purely to maintain type safety:

raise<ActionType>("ActionName", (actionToExecute) => {
    actionToExecute(param1, param2);
});

We should be able to simplify it to something a little bit better:

raise<ActionType>("ActionName")(param1, param2);

React Hooks

Hello! I am starting a new project and I am a fan of Satchel Flux architecture, any ideas how we invision Satchel to work with Hooks?

We could create a satchel-lite forked repository so that it interfaces well with mobx-react-lite

I was thinking of something along these lines:


const onIncrement = mutatorAction('onIncrement', () =>{
   const store = useRootStore()
   store.count++
})

function useRootStore() {
   const store = useStore('RootStore', {
      count: 0
   })
   return store
}

const App = observer(() {
   const state = useRootStore()
   return <button onClick={() => onIncrement()}>Clicked {state.count}</div>
})

Right now Satchel doesn't work with React Hooks. @kenotron @smikula

It's too easy use select() incorrectly

Consider the following code:

interface DoSomethingState {
    someProperty: string;
}

export default select({
    someProperty: store.someValue
})(
    function doSomething(state?: DoSomethingState ) {
    });

This will cause a runtime error because the select should be:

select({
    someProperty: () => store.someValue
})

However, this won't be caught at compile time because the select doesn't actually know what type it is expecting; it just infers the generic type from what it is passed in. It does cause a compile error if you explicitly define the expected type:

select<DoSomethingState>(...)

We should figure out how to improve this. Ideally we would give the user a compile error if select is being used incorrectly. Or at a minimum we should throw a runtime error if the user is not passing a getter function to the selector.

select should give a better error if you select something that isn't observable

Currently the following code will give some hard to understand error:

let store = { prop: "value" };

select({
    prop: () => store.prop;
})(
    function(state?) {
        state.prop = "newValue";
    });

The problem is that store isn't actually an observable store; it's just an object. In that case select will provide a getter for state.prop but not a setter. We should still provide a setter and have it throw a more informative error message about why the value can't be set.

Is it inaccurate to describe satchel as "based on Flux"?

At best, SatchelJS seems to be a simplification of the Flux architecture (and could at the same time be losing significant architectural benefits).

In Flux, Stores are responsible for interpreting the effect actions have on their data (in the prototypical Flux implementation, this is the ReduceStore, in Redux, this can be with things like Reducers). In Satchel, Actions seem to have implementation that directly reaches into stores and modifies their internal state, which actually goes against the list Best Practices for Actions on Flux's page. This actually has very significant implications, as Actions being simple, serializable "instructions" (but not imperative state changes) opens up the doors to features like replayability in a much more predictable manner.

To what extent has the design of Satchel been verified with the creators of the Flux architecture as being a potential improvement (or regression) of the architecture?

Use named Mobx actions in Mutators

Hello folks!

I tried to use satchel with mobx dev tools and I found out actions are recorded inside of mobx as 'unnamed actions'. This makes debugging using dev tools harder.

image

Suggestion I have is extending mutator API and adding option to use named actions from mobx or using action.type as mobx action name.
https://mobx.js.org/refguide/action.html

WDYT about this approach?

Deprecate orchestratorAction

The orchestratorAction API was intended to be a convenience function to make it easier to implement simple orchestrators, but after seeing it in use for a while it's apparent that it causes more harm than good. For all the reasons below, we've decided to deprecate it and no longer recommend using it.

  1. The signature of an orchestratorAction looks like it returns a Promise, which leads people to think they can await it—but it can't be awaited. The problem here is a limitation of TypeScript; when decorating a function as an orchestratorAction the returned function has to have the same signature as the function passed in. We want it to have the same parameters, but the return type should actually be void. This can lead to all sorts of subtle bugs.
  2. Because an orchestratorAction blurs the line between "orchestrator" and "action", people have a tendency to lump them in with all the other actions. One goal of v3 is to promote a clear distinction between the data layer (which contains stores and their associated mutators) and the orchestration layer.
  3. The other goal of v3 is to promote the Flux mental model of dispatching and handling actions rather than just calling imperative functions. When you call a function imperatively you have certain expectations—like being able to return a value or await an async function. By forcing the use of the full action-creator-dispatch-listener pattern it's clear why these things don't make sense.
  4. Moreover, using the normal orchestrator pattern is more flexible for when requirements inevitably change. While it's not that hard to convert an orchestratorAction to the full orchestrator pattern, when the "simple" imperative pattern is already in place the inclination is retrofit new requirements into it, at the expense of veering the design farther and farther from Flux.

Best Practices for Resolvers

Components like Picker which require a "Resolver" that returns a Promise. What is the best practice for that?

If we call the API directly there, that goes against the Satchel/State Management reason. Because there are side-effect within the View.

For example, we are implementing a ListPicker where when you type a popup appears like an auto suggest. One prop has:

  <ListPicker
      onResolveSuggestions: (filter, selectedItems): Promise<ISuggestions[]> => {
         // BEST Practice getting data from API ? If we call an action, orchestrator cannot communicate
         // here unless the state has a promise type? But that becomes messy?
       
          return somePromise;
     },
  />

Docs: Consider moving Docs into /docs directory instead of gh-pages branch

It's much easier to keep Documentation and Source Code in sync when they are easily viewable at the same time and PRs that address changes in one can update the other.

GitHub supports using the /docs folder in your master branch as the source for GitHub Pages and I'm not very familiar with gitbook, but it seems like there might be a configuration tweak to support the same-branch setup.

This would mean that new work that isn't quite ready to be part of the live documentation would need to be merged into a develop branch first or that a release/ branch would need to be used until the new changes are ready to be published as a release.

mobx / mobx-react should be peerDependencies?

So I followed the README and installed mobx and mobx-react as directed and wired up my components as shown in the examples, however, any attempt at updating my store seemed to do nothing and my components failed to re-render... Took me a while to figure out but satcheljs has mobx and mobx-react as dependencies with versions set at ~2.6.5 and ~4.0.3 respectively. The disconnect occurred because when installing mobx with npm or yarn it installs version 3.x.x and thus my code and satcheljs's code were using different versions of mobx. Seems like they should be peerDependencies instead to avoid this problem, no? Looks like this may have already come up before as noted by @kenotron in issue #31.

Cannot use satcheljs along with regular mobx observable/actions

I've a few components using Mobx observables as local state rather than using setState as per this post.

I started moving app state to a satchel store but now mobx "regular" actions that are not going through satcheljs dispatcher are throwing because of the following code since spy is listening for any mobx action not just satcheljs actions.

spy((change) => {
    if (!getGlobalContext().inDispatch && change.type == "action") {
        throw new Error('The state may only be changed by a SatchelJS action.');
    }
});

it seems change.type == "action" is not enough to differentiate between mobx vs satchel actions

Prohibit calling legacy satchel actions from mutators

Mutators are meant to respond to actions, but not to dispatch further actions. Within modern Satchel this is enforced, but it's currently possible to call a legacy Satchel action from a mutator. This is explicitly against the intended pattern and should be similarly prohibited.

Switch to new version of mobx

Now with release 3.1.0 we can install latest mobx, however some warnings occurred like

[mobx] Deprecated: `mobx.map` is deprecated, use `new ObservableMap` or 
`mobx.observable.map` instead

I will try my best to create MR for this

Proposal for improved mutator API

Preface

Satchel exists because, while we liked the Flux pattern and Redux in particular, there were a few things we wanted that Redux couldn't give us.

  • Strong typing on store and actions. There have since emerged a variety of patterns to accomplish this in TypeScript.
  • Avoid immutable semantics. The immutable pattern makes for harder to comprehend code and can be more prone to bugs by inexperienced devs. Today immer could solve this for us, but we get it for free with...
  • The power and simplicity of MobX. MobX allows components to be reactive with a simple @observer decorator and is highly performant by default. Whatever the front-end of our dataflow looks like, we know we want the store itself to be observable.

Beyond those things, we really like Redux, and much of Satchel is influenced by it. These goals for this new mutator API aim to bring us closer to Redux while keeping the benefits above:

  • Mutators should define the shape of the state tree. Currently the store schema is defined separately from mutators, but we want mutators to mirror the shape of the store. If the store gets it's shape from the mutators then this will necessarily be true.
  • State should be passed into the mutators. Right now mutators access the the state by importing the store and/or one or more selectors. By injecting a subtree of the state into the mutator it's clear what the scope of the mutator is. Plus it will make the mutators easier to test by obviating the need for mocking selectors.
  • Super-strict mode. We should provide a new level of strict mode that (for debug builds only, to save on perf) enforces some best practices:
    • State cannot be modified except by the mutator that defines it.
    • References to state cannot be passed as part of an action message. If necessary, action messages should contain IDs that refer to state rather than the state itself.
  • This should be a non-breaking change. A lower priority, but it should be possible to implement this without breaking the existing Satchel APIs.

API

createMutator

The first challenge with mutators is that—because they act on observable objects—there needs to be a parent object on whose properties to act. Because reducers return a state object they can literally replace the entire state. With a little support from Satchel, we can have the best of both worlds: if a mutator returns a value then that value replaces the previous state object; if it does not return a value then we keep the same state object (which presumably has had some of its properties modified).

Creating a mutator for a simple state would look like the following. The state is simply a string, and the entire value of the state gets replaced when the mutator runs.

const mutator1 = createMutator('initial value')
    .handles(actionA, (state, action) => {
        return 'a';
    })
    .handles(actionB, (state, action) => {
        return 'b';
    });

Creating a mutator that mutates an object would look like the following. Note that nothing is returned, so the reference to the state object itself remains the same.

const mutator2 = createMutator({ someProperty: 'some value' })
    .handles(actionA, (state, action) => {
        state.someProperty = 'A';
    })
    .handles(actionB, (state, action) => {
        state.someProperty = 'B';
    });

Inferring whether to replace or mutate based on the return value feels a little loose. As an alternative, instead of handles we could have two separate APIs, e.g. mutateOn and replaceOn. I'm open to ideas for better names.

combineMutators

Mutators can be combined to build up the state of the store. (TypeScript can derive the shape of the combined mutators from the child mutators.)

const rootMutator = combineMutators({
    property1: mutator1,
    property2: mutator2
});

Effectively this creates a parent node in our state tree, so that our subtree looks like:

{
    property1: 'initial value',
    property2: {
        someProperty: 'some value'
    }
}

The combined reducer shouldn't expose handles because all the handling is done in the child reducers—except for the special case where we want the subtree itself to be null. We need a few new APIs for that.

const rootMutator = combineMutators({
        property1: mutator1,
        property2: mutator2
    })
    .nullOn(actionX)
    .nullOn(actionY)
    .definedOn(actionZ);

Satchel will make sure mutators are applied top-down, so that if actionZ is dispatched we will first define the root object and then run the child mutators which may set some properties on it.

createStore

We will have to extend createStore to create a store from a mutator. Functionally this store would be just like any current Satchel store, except that it could only be modified by one of its mutators.

const getStore = createStore('store name', rootMutator);

Testing

To test a mutator, you would call applyAction on it and pass in some fake state. (This is the same API that Satchel will use internally to dispatch actions into the mutator.)

const returnValue = mutator1.applyAction(fakeAction, fakeState);

Faking state is easy—just create a plain object in the shape that the mutator handles. Because the mutator is targetted to a very small portion of the state tree, the mock data should be trivial.

We also need a way to fake an action. This is harder since (by design) only Satchel can construct actions. We'll need to provide some sort of test utils APIs to do this.

const fakeAction = createFakeAction(actionA, { someProperty: 'X' });

Code organization

Now that mutators are tightly coupled to the state of the store, it makes sense to locate them with the store, preferably following the shape of the store. (Because mutators carry the schema there is no need to define the schema separately.)

store/
    property1/
        mutator1.ts
    property2/
        mutator2.ts
    rootMutator.ts
    getStore.ts

Actions should return Promise<void> rather than Promise<any>

The intent of actions is that they do not return anything, but rather they modify the store. They are allowed to return promises mostly for the sake of any middleware that might want to know when the action is truly done.

Currently actions are allowed to return void | Promise<any>. We're allowing return values if the action is async, but not if it is sync? To be consistent it should be void | Promise<void>.

Question: How can i wait promise from action()?

How can i achieve this behaviour? (check onClick method). Maybe i can achive this with middleware somehow ?

import * as React from "react";
import { action, orchestrator } from "satcheljs";

import { render } from 'react-dom';

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const someAction = action("some", () => ({}));

orchestrator(someAction, async () => {
  await sleep(2000);
});

class App extends React.Component {
  state = { loading: false };

  onClick = async () => {
    this.setState({ loading: true });
    await someAction();
    this.setState({ loading: false });
  };
  render() {
    return <button onClick={this.onClick}>{this.state.loading.toString()}</button>;
  }
}

render(<App />, document.getElementById('root'));

Live demo: https://codesandbox.io/s/jl5k1y0033

Proposal: Move orchestration to a middleware

The primary purpose of Satchel is as a state management library: actions are dispatched and handled by mutators in order to modify the store. Once we have this publish-subscribe pattern established, it's convenient to be able to reuse it for side effects and orchestrating between disparate components, hence the orchestrator API. But it's perfectly reasonable to use Satchel without orchestrators (in many cases a simple function can accomplish the same thing).

Also, as they exist today, there is no ordering guarantee among subscribers to an action. There are cases where it would be convenient for an orchestrator to deterministically execute before or after a given action has been handled; for example, if you specifically want to do perform side effect given the new state of the store. A middleware provides the opportunity to do this.

I propose the following;

  • Deprecate the orchestrator API.

  • Create a new package, e.g. satcheljs-orchestration which exports the orchestrator middleware. (Or the middleware could live in satcheljs, but the point is that this is separate functionality that consumers can choose to opt into.)

  • Instead of orchestrator, expose two ways to subscribe: before and after. Usage:

    import { before, after } from 'satcheljs-orchestration';
    import { someAction } from './actions';
    
    before(someAction, actionMessage => {
        // Do something before someAction has mutated the store
    });
    
    after(someAction, actionMessage => {
        // Do something after someAction has mutated the store
    });

select injects the parameters at the wrong place

Hello,
if I have a function like foo(a?, state?) and I invoke it as foo(), even if select should supply the information for state, it will end up setting the value of a.

Thanks! Awesome library :)

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.