ds300 / derivablejs

Functional Reactive State for JavaScript and TypeScript
Apache License 2.0
515 stars 23 forks source link

Question about the project #35

Open qur2 opened 8 years ago

qur2 commented 8 years ago

Hi,

I am following your project from the distance (its previous name, along with the patrician quote, catched my eye :) and I'm wondering about it's relation to mobX (previously mobservable). I understand they have the same goals and mobX is much more mature (battle-tested as they say). Do you have any thoughts on that?

Thanks, Aurélien.

ds300 commented 8 years ago

its previous name, along with the patrician quote

Ha! Thanks. I'm glad at least one person picked up on that :)

[...] mobX is much more mature [...]. Do you have any thoughts on that?

Hmm. MobX is a little older and vastly more popular thanks to the 2.0 release, so make of that what you will. I wouldn't personally classify it as being 'much more mature' however. If anything DerivableJS is more mature in a couple of important ways:

In fact I think API design is the most important difference between the two libraries. MobX optimises for concision at the cost of of clarity and simplicity, and DJS is the exact opposite. This results in DJS being less than half the size (4.3k vs 9.1k .min.gz), but slightly more verbose to use and with a higher learning curve.

qur2 commented 8 years ago

Thanks a lot for this overview, that is helpful. I think mobX popularity is also due to the examples using react and this nice debugging information about how many times a component is rendered. For both libraries, example architectures for such reactive apps would be nice to explore (even though totally out of scope, I agree). For example, how to handle state updates? Something like redux? Any pointer for exploration would be welcome.

Thanks for answering quickly. As far as I'm concerned, this issue can be closed. I leave that up to you (in case you want to leave it open for better visibility).

Cheers.

PS: Yesterday, it's been one year since Terry left us. Here is a nice read: https://archiveofourown.org/works/6225052

ds300 commented 8 years ago

I think mobX popularity is also due to the examples using react and this nice debugging information about how many times a component is rendered.

Indeed. MobX has great docs and the React integration is a boon.

how to handle state updates? Something like redux?

I think so, yes. I'm totally convinced by event sourcing as a paradigm for updating system state. The simplicity and amenability to dev tooling is unsurpassed. I don't think it should be at all connected to effects, however. IMO, effects should always be a reaction to state changing and nothing else. People have realised this for DOM updates, but not yet for other kinds of effects like network requests and so forth. The Redux community has started talking about doing this so hopefully it will be more common soon.

ggoodman commented 8 years ago

Effects should always be a reaction to state changing and nothing else. People have realised this for DOM updates, but not yet for other kinds of effects like network requests and so forth.

Have you come up with any patterns that you like for putting this into practice? What does the action -> state change -> XHR chain look like in more concrete terms?

liron00 commented 8 years ago
init() {
  this.$avatarId = atom('batman')

  this.$avatarId.react(aid => {
    ajaxPost('setAvatarId', {avatarId: aid})
  }, {skipFirst: true})
},

render() {
  return <div>
    <img src="/images/avatars/{this.$avatarId.get()}.png" />
    <button onClick={this.$avatarId.swap(aid => aid == 'batman'? 'superman' : 'batman')}>
      Toggle avatar
    </button>
  </div>
}
ggoodman commented 8 years ago

@liron00 I didn't ask the right question ;-)

How would you extend your example to add 'handling' to the response from that POST. Specifically, that request ('effect') will produce subsequent effects that are not naturally captured by DerivableJS but should result in a state change or some sort.

One of the canonical examples for Observables is handling the example of a live ajax search widget where keypresses will be throttled and when they get through, they will cancel in-flight requests and issue a new one. The results of a completed request will populate the widget.

I'm having a tough time visualizing how we could use the Derivable primitives to put something like this in a clear, concise bit of code.

Here is a quick dump of how I might imagine this working in terms of bits of state:

{
  input$, // Atom representing content of input
  throttleTimer$, // State set by changes to input$ that cancels previous timer if set
  throttledInput$, // State set when the throttle timeout fires without being first cancelled. Triggers XHR
  outstandingRequestHandle$, // Handle to the outstanding request. When changed, system cancels previous value
  searchResults$, // MaybeError boxed value of the XHR response
}

Seems to get complicated quickly (IMHO). Though to be fair, this whole lib really means embracing a paradigm I'm not used to.

liron00 commented 8 years ago

@ggoodman Yeah if you want to implement throttling yourself, you first want to have a data model of the state of your requests, and represent it as state transitions. Then whatever imperative network commands you have to issue should be a reaction to the state transitions.

Another option is to just implement something like a smartAjaxPost() method outside the Derivable paradigm, which knows to throttle requests and abort previous requests. Then your component can have the guarantee that any network response it gets back will be connected to the state that it's currently in, and your component doesn't have to model any low-level state of in-flight vs. cancelled network requests.

ds300 commented 8 years ago

@ggoodman I did the flux challenge using the effects paradigm described earlier:

https://github.com/staltz/flux-challenge/blob/master/submissions/ds300/src/request.ts

Basically the idea is that you know the set of things you have and the set of things you need, so you can discover the set of things you don't have but need by doing a diff derivation. Then you react to that set of things while independently keeping track of the things you are currently fetching. To make this a generic pattern in the context of event sourcing, the output of your diff derivation could be a set of of thing-you-need request maps (think ring style) which include a response event which can be embellished with the response data and submitted to the event sourcing hub.


const $thingsToFetch = $thingsINeed.derive(set.difference, $thingsIHave);
const $requests = $thingsToFetch.derive(things =>
  things.map(thing =>
    ({
      method: 'GET',
      url: `things/${thing.id}`,
      onComplete: {
        type: 'THING_FETCHED',
        thingID: thing.id,
      },
      onFail: {
        type: 'THING_FETCH_FAILED',
        thingID: thing.id,
      },
    })
  );
);

function thingsReducer (state, action) {
  switch (action.type) {
  case 'THING_FETCHED':
    console.log('the thing is', action.data);
    return addNewthing(state, action.thingID, action.data);
  case 'THING_FETCH_FAILED':
    console.log('could not fetch thing because', action.data);
  default:
    return state;
  }
}

const store = createStore(thingsReducer);

$requests.react(createRequestHandler(store));

// this would be provided by a framework.
function createRequestHandler(store) {
  let activeRequests = Set();
  return requests => {
    let newRequests = requests.subtract(activeRequests);
    newRequests.forEach(req => {
      // ... make new request
      // when completed submit req.onComplete to store
      // with response data attached
      // similarly for req.onFail
    });
    let oldRequests = activeRequests.subtract(requests);
    oldRequests.forEach(req => {
      // ... cancel old request
    });
    activeRequests = requests;
  }
}

As for throttling, you can just use lodash.throttle on your reactor functions. If you want to do it in a 'derivable' way, however, yeah its harder because you need to reimplement that logic in an unfamiliar way and it requires an intermediate time step between two state systems (unless your user event handlers become aware of the throttling, but that's very undesirable because you end up reinventing the wheel every time you need to do throttling). I'll try to get around to writing up a demo of that tomorrow or later this week.