GithubHelp home page GithubHelp logo

martyjs / marty Goto Github PK

View Code? Open in Web Editor NEW
1.1K 1.1K 77.0 10.39 MB

A Javascript library for state management in React applications

Home Page: http://martyjs.org

License: MIT License

Makefile 0.10% JavaScript 99.79% Shell 0.11%

marty's People

Contributors

8th713 avatar aidos avatar ajanuary avatar ali avatar banderson avatar bigardone avatar blakeembrey avatar chollier avatar deepanchor avatar friss avatar imjared avatar jeroencoumans avatar jhollingworth avatar jonhester avatar jstrutz avatar moretti avatar nematode avatar nilliams avatar oliverwoodings avatar ordinathorreur avatar pbomb avatar pd avatar quazzie avatar randymorris avatar spicydonuts avatar surreal9 avatar taion avatar therealcisse avatar trainiac avatar winkler1 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

marty's Issues

Rollbacks

We want to have a mechanism to allow you to rollback optimistic actions (e.g. inserting an entity into a store, sending an HTTP request which then fails)

var UserActionCreators = Marty.createActionCreators({
  saveUser: function (user) {
    var action = this.dispatch(Constants.Users.ADD_USER, user);

    action.rollback(); // dispatch returns an action object that has a rollback function

    UserAPI.saveUser(user, action.rollback) // You could then pass that rollback function to API
  }
});

Within the store handler, if you wanted to rollback, you return a function. All state is within the closure and don't need to do much.

var UserStore = Marty.createStore({
  handlers: {
    addUser: Constants.Users.ADD_USER
  },
  addUser: function (user) {
    this.state = this.state.concat([foo]);

    return function rollback() {
      this.removeUser(user);
    }
  },
  removeUser: function (user) {
    ...
  }
});

Creating an action creator called 'dispatch' causes infinite loop

I realise this is obvious but took me a little time to realise my stupidity.

var TestActionCreators = Marty.createActionCreators({
  dispatch: TestConstants.DISPATCH(function (foo) {
    this.dispatch(foo);
  })
})

dispatch should be a reserved keyword, we should throw an error if you try to create an action creator called dispatch

Routing

We need a router that can be run the client and server so we can start building isomorphic applications. You should be able to

  • Define routes (regex/string) and what element should be rendered
  • Call the dispatcher when the router starts, before the router is about to change and after the route has changed
  • Have an action creator for navigating to routes

Rather than building our own we should use one of the many react-routers out there. react-router offers server side rendering and all the APIs we need. To avoid too many dependencies on the core module lets have this as its own package

var MartyRouter = require('marty-react-router');

var RoutesStore = MartyRouter.createRoutesStore(routes);
var NavigitationActionCreators = MartyRouter.createNavigationActionCreators(router);

Marty Developer Tools

A chrome developer tools tab that allows you to visualise the state of the application and how data is flowing through

Should stores work directly with WebAPI?

Stores are responsible for fetching their own data so getInitialState is a good place to make any API calls.

Came across this in the docs and I'm wondering if it would be better practice to call UsersAPI.getAll indirectly through an action creator instead.

var UsersStore = Marty.createStore({
  getInitialState: function () {
    UsersAPI.getAll();
    return [];
  }
});

// vs

var UsersStore = Marty.createStore({
  getInitialState: function () {
    UserActionCreators.getAll();
    return [];
  }
});

image

The more I think about it the less I think it's the store's responsibility at all.

Make sure ActionCreators will work with es6 arrow syntax

๐Ÿ’„

In some es6 transpilers, ES6 arrow functions are bound to the current scope making it impossible to call this.dispatch(...) or other methods of the ActionCreators inside the callback.

By passing context as the last argument to the ActionCreator, one can use context.dispatch(...) or other methods of the ActionCreator.

Since context is passed as the last argument, all existing code work as is and if you don't use es6 arrow syntax, you're not affected.

Handle ES6 classes

Now that React v0.13 is supporting ES6 classes its only a matter of time before the whole community goes that way. We should support ES6 classes out of the box. Fortunately the concepts in Marty largely map well to classes:

class UserStore extends Store {
  handlers: {
    addUser: UserConstants.ADD_USER
  },
  constructor() {
    this.state = {};
  },
  addUser: function (user) {
    this.state[user.id] = user;
    this.hasChanged();
  }
}


class UserComponentState extends StatefulComponent {
  listenTo: UserStore,
  getState() {
    return {
      users: UserStore.getAll()
    }
  }
}

class User : UserComponentState {
  render() {
    console.log(this.state.users);
  }
}

My major concern is how to map constants to action creators. I believe that the entire action function should have a type associated with it, not just when it dispatches. This was why we went with constants being functions. There have been some issues with this approach (#83) so would like to explore alternative solutions.

One idea I had is to use field annotations. I realise this isn't in a spec but neither is JSX. This would work out the box with Traceur however we need to support 6to5 and people using ES5. My suggestion is writing a annotation specific transformation (e.g. annotationify for browserify, equivalent for webpack and require.js). We would keep the original syntax for those who'd prefer it

class UserActionCreators extends ActionCreators {
  @ActionType(UserConstants.ADD_USER)
  addUser(user) {
    this.dispatch(user);
  }
}

Update

@KidkArolis has half convinced me its a bad idea to introduce annotations (although he agrees it solves the problem well. His suggestion was to call constants from inside the action function. This has the benefit that we don't have to mess with function contexts or introduce new tooling.

class UserActionCreators extends ActionCreators {
  addUser(name) {
    return Constants.ADD_USER((dispatch) => {
      var user = UserUtils.createUser(name);

      dispatch(user);

      return UserHttpAPI.createUser(user);
    });
  }
}

The other possible solution would be to define the types in a hash. On the upside its simpler and follows the same syntax as Stores, on the downside it does require a little mental work to work out which type is associated with an action creator.

class UserActionCreators extends ActionCreators {
  types: {
    addUser: Constants.ADD_USER
  },
  addUser(user) {
    this.dispatch(user);
  }
}

Add aliases

It should be easier to access all the types within Marty, e.g. require('marty/actions'). I think we can support this using aliasify or remapify.

Aliases I want

  • marty/dispatcher => ./lib/dispatcher.js
  • marty/store => ./lib/store.js
  • marty/constants => ./lib/constants.js
  • marty/actionCreators => ./lib/actionCreators.js
  • marty/repository => ./lib/repository.js
  • marty/actions => ./lib/stores/actions.js
  • marty/stateMixin => ./lib/mixins/stateMixin.js
  • marty/errors/* => ./lib/errors/*.js

Force fetch remotely

Hi!
I'm starting to use marty and I have a problem. My application has a PeopleStore and a PeopleAPI which returns some paginated list of json results.
So in my main component view I display those first results and a pagination component which just handles the on click event on any of it's page links, so when they are clicked it tries to fetch again the data from the server for the selected page results...
Te problem is that in my store it always returns the already loaded results from the first time as it always fetches locally first... is there a way of forcing it to fetch again remotely from the server?

Here's my code:

@PeopleStore = Marty.createStore
  displayName: 'PeopleStore'

  getInitialState: ->
    meta:
      totalPages: 0
      currentPage: 1

  handlers:
    receivePeople: PeopleConstants.RECEIVE_PEOPLE
    findPeople: PeopleConstants.FIND_PEOPLE

  findPeople: (params) ->
    @.fetch
      id: 'all-people'
      locally: () ->
        @state.people
      remotely: () ->
        PeopleAPI.findPeople(params)

  paginationMeta: ->
    @state.meta

  receivePeople: (response) ->
    @setState
      people: response.people
      meta: response.meta

@PeopleAPI = Marty.createStateSource
  type: 'http'

  findPeople: (params) ->
    searchText = if params and params.searchText then params.searchText else ''
    pageNumber = if params and params.pageNumber then params.pageNumber else 1

    req =
      url: Routes.people_path(
        search: searchText
        page: pageNumber
      )

    @get(req).then (res) ->
      PeopleSourceActionCreators.receivePeople res.body

I have a full demo, only using React components, of what I'm trying to achieve with using marty.js here's the link http://rails-and-react-iii.herokuapp.com/

Thank you very much in advance!

Add a yeoman template

To make it easier for people to get started with Marty, we should create a yeoman template. It will be a simple express app using react-router, browserify & grunt

Folder structure will be

/app
  /actions
  /components
  /constants
  /stores
  /apis
  /server
    /handlers
    /views
/config
/build
  /tasks
Gruntfile.js
Makefile
package.json
README.md

Allow you to specify when function context

Often you want to call another component function from within a when handler. Right now there's no way to set the function context so you end up having to assigning functions to local variables.

Ideally you should be able to optionally pass in a function context as the second argument (when(handlers, context)). The problem is we already set the function context to the handlers so that you can call other handlers.

Not sure how to solve this, needs some thought.

Fetch never calls done handler

Maybe I'm misunderstanding what should be happening, but it doesn't look to me like the done handler is getting called when the data is fetched asynchronously in remotely. Here's a Plunker that reproduces it:

http://plnkr.co/edit/DKQ6nriYimZ4KeYJCrLc?p=preview

It seems to be an issue when remotely returns the http promise, which then resolves and sets the state (simulated with a setTimeout). The only handler that gets called is pending.

Unclear on constants example in the docs

http://martyjs.org/guides/constants/index.html contains the following code snippet regarding the second argument to Constants functions:

var UserActionCreators = Marty.createActionCreators({
  deleteUser: UserConstants.DELETE_USER(function (user) {
    this.dispatch(user);
  }, { foo: true })
});
var FooStore = Marty.createStore({
  handlers: {
    fooAction: { foo: true }
  },
  fooAction: function () {
    ...
  }
})

In the example it looks like by suppling a 2nd arg you're changing the signature used to identify the action in the handler registration later. So you can write { foo: true } instead of UserConstants.DELETE_USER.

Is that what's going on and if so, what's the purpose of it?

Introduce repositories, get rid of HttpApi

The HttpApi feature is not scalable enough for all scenarios, so instead Marty should have the concept of generic repositories with mixins that extend their functionality. A repository is an abstracted method of reading and writing data to and from a source (regardless of whether it is remote or local). A repository has a definition and implementations. It can be implemented in multiple types, allowing for an application to choose what type it makes use of (e.g. in a test environment you might want to use localStorage as the repository type instead of an HTTP API).

Repositories are be defined like so:

var UserRepository = Marty.defineRepository({
  name: "user",
  methods: [
    "getUser",
    "addUser"
  ],
  implementedBy: [UserRepositoryLocal, UserRepositoryHTTP]
});

Repository implementations look like this:

var UserRepositoryLocal = Marty.implementRepository({
  type: "localStorage",
  prefix: "users",
  get: function (key) {
    return localStorage.getItem(this.prefix + key);
  },
  set: function (key, value) {
    return localStorage.setItem(this.prefix + key, value);
  },
  getUser: function (id) {
    return this.get(id);
  }
});

With mixins implementations can be simplified:

var UserRepositoryLocal = Marty.implementRepository({
  mixins: [LocalStorage({ prefix: "users" })]
  getUser: function (id) {
    return this.get(id);
  },
  addUser: function (id, user) {
    return this.set(id, user);
  }
});

Repositories are accessed by requiring in the definition, not the implementation:

var UserRepository = require("repositories/user");
UserRepository.getUser(1234);

Note: If this concept of defining and then implementing a repository multiple is too niche then we can just skip the definition step and simply create repositories.

Automatically create action creator if you don't pass one in

It seems pretty common that you create an action creator that just dispatches whatever arguments you pass in. We could automatically create this function for them

var ActionActionCreators = Marty.createActionCreators({
  addFoo: FooConstants.ADD_FOO(),
  // same as 
  addFoo: FooConstants.ADD_FOO(function (foo, bar) {
    this.dispatch(foo, bar);
  })
});

Make constants functions

Want to support a more succinct way of defining constants for action creators

Marty.createActionCreator({
   createUser: Constants.CREATE_USER(function (user) {
      this.dispatch(user);  
   })
})

Constants are actually functions

function CREATE_USER(creator) {
   creator.type = "CREATE_USER";
   return creator;
}

CREATE_USER.toString = function () {
   return "CREATE_USER";
}

Drop "action creator" terminology for "service"

With flux in general, having "actions" and "action creators" is not only confusing but also misleading. The name implies their role is simply to create actions, when really this is where the meat of your async and validation code should go.

@jhollingworth what do you think about calling "action creators" services instead?

var Service = Marty.createService({
  createUser: function(name, email) { ... }
});

Service.createUser(...);

"Failed to load response data" in chrome

I narrowed it down to a line in the fetch library, but in case this isn't an issue with fetch, I'm copying here. See JakeChampion/fetch#83

Following the getting started guide here: http://martyjs.org/guides/getting-started/

npm install -g yo generator-marty
mkdir example && cd example
yo marty
yo marty:domain user
npm start
open http://localhost:5000/user/134 in chrome

Inspecting the api call to http://localhost:5000/api/users/134 yields Failed to load response data in the preview tab. When I console.log the api response, it shows {id: "4"}, which makes it seem like response body is being returned somewhere, so this might not break anything, just make debugging responses difficult.

The [Exception: DOMException: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was 'blob').] error described in the fetch issue is still happening, despite the response apparently coming back.

In my original issue on my own implementation, returning when returns a 404 error, but when I right click > open in new tab on the API request from the chome debugger, the request works just fine (in a new tab, by its self) and I receive the response back with no issues. Something in marty/fetch is corrupting/not returning the response.

Let me know if this is reproducible. I've tried this with node v0.10.33 and v0.11.14

Simplify releasing

  • Have to update bower.json, index.js and docs/_config.yml manually
  • Have to do major/minor releases manually
  • make release fails a lot

Debugging

  • Have a logger instance inbuilt so that its easier for people to log things out
  • Log what marty is doing inside (e.g. entry and exit of each function)
    • Do so in a format that can also be used by Marty Developer Tools

Store API improvements

Batch operations
This will only cause a single change event

this.batch(function () {
   this.state = {};
   this.state = { a: 1 }
});

Dont cause change event

this.quietly(function () {
   this.state = {};
});

Mixins

var LocalProfileStore = Marty.createStore({
  mixins: [FooMixin],
  ...
});

module.exports = LocalProfileStore;

Isomorphic application

So that I can have better SEO and initial render time I want to be able to render the representation of the application on the server. i.e. isomorphism.

Concurrency issues

One of the biggest problems right now is that everything in Marty is a singleton. However you could also think of createStore, createActionCreators, etc as registration functions. If we have an internal registry of all types, action creators, etc we could then create new instances of them. The interface you return from createStore would just be a proxy to whatever instance of the store you specify

var FooStore = Marty.createStore({
  id: "foo",
  displayName: "Foo"
});

var FooActionCreators = Marty.createActionCreators({
  id: "foo",
  bar: function () { ... }
});

var instance = Marty.createInstance();

instance.getActionCreators("id").bar(123);

var html = Marty.renderToString(<Foo/>, instance);

(De)Hydration

Stores have the getInitialState() function which is where you get the initial state of the store and, if required, make any HTTP calls to hydrate the store.

var Store = Marty.createStore({
  getInitialState: function () {
    SomeAPI.getSomeState();

    return [];
  }
});

I propose we allow you to specify the initial state on the server which, when the store is loaded, is passed to the stores getInitialState

var Store = Marty.createStore({
  getInitialState: function (serverState) {
    if (serverState) {
      return serverState;
    } else {
      return [];
    }
  }
});

This way you can do any transformations (e.g. turning the object into a immutable object).

Marty.renderToString(component, instance) would a string that should be injected into the page that would include the html as well as the initial dehydrated state.

<div class="foo">...</div>
<script type="text/javascript">
   (window.__marty||(window.__marty={})).state={someStore:{foo:'bar'}}
</script>

Broken `this` when disconnecting function from it's calling parens

The problem is here: state.js:41

When store.serialize is undefined, store.getState is called with a broken this reference.

Here's a simplified recreation of the problem:

var x = {
  fn: function () { return this; }
};

x.fn() // => returns `x`

(x.fn)() // => returns `x`

(undefined || x.fn)() // => returns `window` ... wat

I think we can all conclude that JavaScript is retarded. Probably still needs to be fixed though.. : ]

jsonStorage will throw exception if key not found

This is the code for the 'get' method of jsonStorage:

    get: function (key) {
      var raw = getStorage().getItem(getNamespacedKey(key));
      try {
        var payload = JSON.parse(raw);
        return payload.value;
      } catch (e) {
        throw new Error('Unable to parse JSON from storage');
      }
    }

If a key is not defined, this will throw an exception saying cannot parse, even though the issue is actually that it will throw a 'cannot read property value of payload'.

Really it should just return null if raw is null. I'll do a PR.

Storing all actions causes memory issues

If you are dispatching lots of actions (e.g. mouse events) then memory usage skyrockets since we store all actions. Being able to get the status of an action is still a valid use case but its the exception rather than the norm. Instead we should have a way to tell Marty to remember (Is that the right terminology?) an action for later querying.

Some ideas about how to implement

var remember = require('marty/remember');

var UserActionCreators = Marty.createActionCreator({
  // action creators could have a fluent interface
  createUser: Constant.CREATE_USER(function () {
    this.dispatch()
  }).memorable(),

  //pass an object literal as a second object
  createUser: Constant.CREATE_USER(function () {
    this.dispatch()
  }, { remember: true })

  // compose functions
  createUser: remember(Constant.CREATE_USER, function () {
    this.dispatch()
  }))
})

Also, we should not listen to actions by default. Lets make it explicit instead but make it easy to find the action store

var Actions = require('marty').Actions;

var UserMixin = Marty.createStateMixin({
  listenTo: [Actions],
  getState: function () {
    return {
      status: Actions.getAction(this.state.token)
    }
  }
})

Handing asynchronous requests from stores

When you are getting something from a store, you must assume it will require an asynchronous operation to fetch the data (right now we are only concerned with HTTP requests). Currently if we don't have the data yet we send off an HTTP request and return null/undefined. This approach leaves a number of unanswered questions

  • How do you differentiate from the entity not being in the local cache and it not existing at all (e.g. 404/410)
  • How you know if there is already an HTTP request in progress which is trying to get the data?
  • How do you know if an HTTP request has failed?
  • How do you ensure all the stores are synchronised before returning the result of a asynchronous result?
  • If a views initial state is not available before the component is mounted, what should we render?

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.