Emulating Flux/Redux "lite" (was: Anyone wanna help make a Meteor Redux package?)


#1

So, as I’ve mentioned before, I’m not a fan of using browserify in Meteor—and in fact, as of Meteor 1.2, it seems to no longer work (there’s no such package as npm-container, which apparently makes browserify break).

I really strongly believe Redux ought to be a Meteor package. Anyone want to contribute? I think redux and react-redux should be packaged together into a single package.


Possible to access subscription data without declaring collection?
#2

I personally don’t like the idea of Redux inside Meteor or any Flux library because it is designed to solve a problem Meteor already solved with subscriptions.

If you really want to use it or just play with it, I suggest that you get it through NPM and use webpack with it. Otherwise, you will lose all the awesomesauce of time traveling and a lot of cool stuff on the dev toolbar. Meteor will kill it every time it restart.

You can probably start with this kickstarter and add Redux very easily. Do we really want Atmosphere to clone libraries like Redux that are already isomorphic? It will end up being a copy of NPM (that will be pretty useless).


#3

But Meteor’s pub/sub system is not really similar to Flux’s/Redux’s store/action dispatch system. I can’t use Meteor’s pub/sub to have a modal component at the top level that I can invoke from any grandchild component.


#4

You can use a global ReactiveDict as a UI state store.


#5

actions - > Meteor method (argubaly more powerful)
store -> mini mongo
meteor doesnt need flux…

if you need states in top component to call from grandchild then that is a React vs Blaze problem.

You can also store states as a big state in Session/ReactiveDict.

All this is implemented with latency compensation and cashing.


#6

So torn. I like the idea of Redux, but getting it integrated into Meteor is just so clunky. Right now I’ve just constructed my own React mixin that stores the global state.

But if that action is to invoke something like a loading spinner or modal (React components), I don’t know how that would work with Meteor methods. You can’t just call up a modal and make it appear, since it’s a React component that would need to be rendered into a specified area.

I’ll be honest and say this could also be chalked up to my inexperience with React. :slight_smile: I decided to just dive in head first and learn as I go. But part of this, I think, is the growing pains of getting React to work within Meteor. I think we could use some more defined standards of how to get them to play nice. It would be cool if MDG developed their own Flux-like architecture to use with React!

Yep, I can see using that to store states.

This is an issue both engines have, IMO. React can pass props down which is nice, but going back upwards is tricky. Blaze doesn’t have a nice and neat way for templates to pass data to each other, so it becomes easy to rely on Session instead.


#7

Where is your spinner and modal in ? The top component or childrens?

If your modal is in near-top level and you need to trigger it in the children, you pass the function that calls setState({modalShow: true}) to the children that needs to invoke it. If it is too many levels down, you use react context and expose the function to all its children that needs it: https://blog.jscrambler.com/react-js-communication-between-components-with-contexts/

If your modal is in the children or sibling then you need to keep the state of the modal in the top level component and pass it down. This is standard react practice where you push the state up as much as possible. So if it is like this:

<Top>
  <Modal />
  <ComponentThatTriggersModal />
</Top>

You gotta keep the state of the modal in <Top /> and pass the state down to <Modal /> as a prop, and at the same time, pass the setState({modalShow: true}) down to <ComponentThatTriggersModal />. Now you can have sibling that communicate with one another AND parent setting the state(or props) of the children .


#8

What?! I’ve never even heard of this context thing… wow. Thanks, I’ll have to check this out. I wonder why it’s undocumented.

You also gave me another idea: I wonder if I can build an easy store/state manager with mini-mongo, and use observe() to watch for new additions to the dispatch queue, and carry out actions at the top-most component. Hmm…

To answer your question, yes, I keep modals, spinners, and other global things at the topmost component. So the issue I run into is, I don’t want to have to pass all the props needed for Modal 6 or 7 levels deep into the grandchildren. I’ll check out that context feature, and also toy with some ideas using mini-mongo and observe() to construct an easy communication channel to the top.


#9

@sikanx - you rock! context is totally awesome, it does exactly what I’ve been wanting to do. Thanks!


#10

Context is an undocumented feature of React that will be probably soon. However, they are like global variables. We shouldn’t use it unless it is REALLY necessary.

Just to let you know, you will hate context until React 0.14 is released ;). They switched to parent-based (based on the DOM architecture) rather than owner-based. When 0.13 will be the past, they will work like a charm.


#11

This sounds pretty cool :smiley: Just a warning, that projects changes a lot so you’d need a way to automate updates.


[quote="benoitt, post:2, topic:10360"] I personally don't like the idea of Redux inside Meteor or any Flux library because it is designed to solve a problem Meteor already solved with subscriptions. [/quote]

Those are completely orthogonal IMHO. This also seems to be a common assumption (that and Redux is the same as Meteor methods). Redux also works best when using it for non persistent state.

Redux does replace a Reactive-Dict when using Blaze (however this approach doesn’t solve cascading renders). You could store all transient state (local state in memory) in a Reactive-Dict and have the UI re-render on change.

If one would like a single very simple version of Redux for Blaze or React you could do this:


// app-state.js
appState = new ReactiveDict('appState');


// namespace to make it more clear what it's doing in UI
Actions = {
  // allows you to de-couple UI and data logic
  selectPlayer(docId) {
    // any logic needed can be done here, cleaning up UI
    appState.set("selectedPlayerId", "jd863sk64sd");
  }
}


// or this could be a Blaze UI
SelectPlayerButton = React.createClass({
  handleClick() {
    Actions.selectPlayer(this.props._id);
  },
  render() {
    return ( <div onClick={handleClick}>Select Me</div> );
  }
});


// view controller handles fetching appState or collections data
AppContainer = React.createClass({
  getMeteorData() {
    return {
      selectedPlayerId: appState.get('selectedPlayerId'),
      favoriteColor: appState.get('favoriteColor'),
    };
  },
  render() {
    // pass data down as props
    return ( <App {...this.data} /> );
  }
});

This gives you a single source of local state that is decoupled from the view but does not solve cascading renders (from what understand on how the mixin works). That shouldn’t be too big of an issue though.


#12

@SkinnyGeek1010 I was thinking about not using Flux in Meteor and actually what you are describing is what I have done in a test app. But this is a simple application and I’m wondering what problems are going to occur in a more complex app.
Beside possible problems with cascading renders, do you or anyone else see any other problems?
…Or any reasons to actually use a Flux library (like Redux) in Meteor?


#13

Gah! I already ran into my first problem with React context.


#14

You’re going to be very coupled to the Meteor mixin for better or worse (react-packages/issues. This isn’t nec. a bad thing but something to be aware of.

To get the full benefits you need to enforce a team style to use action creators so that you can test and maintain the UI easily. They also make larger apps much easier to go back to months later. In this example and Redux both, action creators are just regular functions.

In a large app I would also wrap the reactive-dict in an object with a getter and setter so you have the opportunity to do something with the data before it’s get/set (like logging in dev env).


> ...Or any reasons to actually use a Flux library (like Redux) in Meteor?

Using the simple reactive-dict above you’re not going to get the benefit of time travel debugging, community knowledge and tools that work with Redux, and the ability to send ‘actions’, objects with a type and payload. The latter means you can’t do things like sniff actions streaming by and triggering analytics events (in a middleware for example).

You also can’t easily store minimongo state in this store because at the end of the day both are using the mixin to update the UI. However, if you’re using Redux you have the option to listen to changes and emit collection change events, syncing a copy of the collection in Redux. The pro is that you’ll have all local state in one tree, not just transient state. This costs a bit of memory but can make it easier to fit the app program in your head.

That being said, if Redux seems too complex or doesn’t jive with your workflow I think the above is a good solution. I don’t foresee any bad effects happening but have never tried it.


#15

Whew. I feel like this has been wasting precious time I could be making progress on my app, but it’s also important, I think, for me to figure out this inter-component communication thing, come up with something that works (even if it’s not perfect). I appreciate all the input and ideas!

@SkinnyGeek1010, I like the idea of using ReactiveDict. So this is what I’ve got going on for this spinner component of mine. This version was just a rough test I threw together to see if it works. The spinner has a 1.5s delay before it shows up, so that it only displays if an operation is taking longer than expected (I personally don’t like seeing an overlay and spinner pop up for operations that take less than a second).

app-state.js

appState = new ReactiveDict('appState');
appState.set({
  spinnerVisible: false,
  spinnerMessage: ''
  // other global states here, for other components
});

Actions = {
  showSpinner(message) {
    appState.set({
      spinnerVisible: true,
      spinnerMessage: message
    })
  },

  hideSpinner() {
    appState.set({spinnerVisible: false});
  }
};

Spinner.jsx

Spinner = React.createClass({
  displayName: 'Spinner',
  mixins: [ReactMeteorData],

  getMeteorData() {
    return {
      message: appState.get('spinnerMessage'),
    }
  },

  getInitialState() {
    return {
      visible: false
    }
  },

  render() {
    if (!this.state.visible) {
      return false;
    }

    return (
      <div>
        [graphical spinner here] ({this.data.message})
      </div>
    )
  },

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({visible: true});
    }, 1500);
  }
});

Home.jsx

Home = React.createClass({
  displayName: 'Home',
  mixins: [ReactMeteorData],

  getMeteorData() {
    return {
      spinnerVisible: appState.get('spinnerVisible')
    }
  },

  render() {
    return (
      <div>
        <DummyWrapper />
        {this.data.spinnerVisible ? <Spinner /> : ''}
      </div>
    )
  }
});

So, anywhere in the app, if an operation could potentially take some time:

  handleClick() {
    Actions.showSpinner('loading all the things!');

    Meteor.call('potentiallyLongOperation', function () {
      Actions.hideSpinner();
    });
  },

The spinner waits 1.5s, then displays, and then is unmounted when Actions.hideSpinner() is called.

As you mentioned, Adam, the one caveat (but not necessarily a bad thing) is that you’re pretty tied to using the ReactMeteorData mixin on every component that needs to access the app state. As you can see, I’ve opted to not pass props simply because I don’t want to have to deal with that on a larger scale.

The only other slight issue is that if I were to use these spinner calls within a getMeteorData, then getMeteorData is fired off four times: once when the component mounts, another time when I call Actions.showSpinner (because it changes the reactive dict), another time when the sub is ready, and another time when Actions.hideSpinner is called.


#16

You might be able to wrap a facade around the appState.set that uses a setTimeout of 0 to try and mitigate cascading renders. For the most part it shouldn’t be terribly un-performant rendering 4 times (as its just a virtual diff).

You may also be able to gather all of the data calls into one component higher up, then pass down the data for the spinner as props.

Also you could create a higher order function that creates a component and wraps yours for components that are only reading state (note last line and spinner comp lines):

const Spinner = React.createClass({
  getInitialState() {
    return { visible: false }
  },

  render() {
    if (!this.state.visible) {
      return false;
    }

    return (
      <div> [graphical spinner here] ({this.props.message}) </div>
    )
  },

  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({visible: true});
    }, 1500);
  }
});

Spinner = connectAppState(Spinner, {message: 'spinnerMessage'});

MeteorFlux Flow
#17

I totally agree with you. Meteor solved a lot of things and it’s means they can duplicate.


#18

There are other forum posts where people are discussing similar issues.

@luisherranz is working on a global AppState package that seems awesome
I have though of a ComponentTree package that allows each component to know it’s parent and children and automatically subscribe to events dispatched only by its children

I think that if the redux /flux functionality would have to be built for meteor we would have to use the great stuff that Meteor already has. Publications and Subscriptions are already a part of Meteor.
If the Dispatcher or a global AppEventStore where actually a client only collection, we could get the time travel functionality too, because it would be stored in a collection.
We could even store it in a permanent collection on the server so that we could “spy” on how users actually use the app.
That is powerful.
But in order to do that we need 3 pieces of code

a Global AppEventStore > contains all events
a Global AppState > contains all the state
a ComponentTree > so that each component knows which AppStore updates to listen and which to ignore
maybe the AppStore and AppState can be two sides of the same global object: according to EventSourcing standards, the state is reconstructed from the history of small state changes that are stored in the AppStore


#19

Almost forgot:

I should point out meteorflux:dispatcher.


#20

I won’t bet on React’s context for production unless you know what you’re doing. I think FB sort of tried to hack around some issues and context is the outcome. It’s not documented because it’s not a solid feature and the core devs probably dislike it :wink: also how it’s used can change with every release… In any case, I try to avoid it, or only use it for something simple.