gpbl/isomorphic500

Isomorphic example using flux

gpbl opened this issue · 36 comments

gpbl commented
Isomorphic example using flux
gpbl commented

Some notes after a day trying with server-side rendering and flux.

The main issue is that a store needs to have an initial state before the rendering-to-string call. On the client the application's state can be updated after having rendered the components, by calling the proper actions (This case applies to flux-react-router-example). The server instead allows just one rendering.

Other developers are trying to mix the router with flux, by creating actions or stores connected with it, as with fluxible-app. It is explained here: a singleton Dispatcher doesn't fit well on the server, and we need a different dispatcher for different contexts (e.g. a request, or a browser session). I believe this goes too far away since flexible-app is becoming a complex approach (and it is at an early stage).

Then, there's the data dehydration/rehydration: the server will output the state of the components in a variable so that when the component is mounted it has already the same data. This seems a simpler issue and express-state has solved it nicely.

Next thing I'll try is refluxjs, which simplify a lot the flux architecture and it could enlighten the server-side way. A developer solved the issue using react-async with refluxjs and react-router-component.

Hi @gpbl Nice trying with the flux approach! Any reason the code is removed in the current master branch code?

gpbl commented

Thanks, @fraserxu – yes it's not in the branch since i'm not quite ready with it :-)

There are yet some challenges to solve. I believe the simplicity of reflux.js would help, so that's my direction, but it's not yet defined how.

gpbl commented

The current work in progress with flux is in the reflux branch.

hi @gpbl I think you can take reference from https://github.com/chadpaulson/react-isomorphic-video-game-search. He uses reflux as well but not using react-router.

It uses a unified way of requesting for data between the server & client, using 'superagent' which exposes the same set of API for the server and client.

https://github.com/chadpaulson/react-isomorphic-video-game-search/blob/master/react/src/stores/searchStore.js#L17

As for server side routing using react-router, maybe this will help https://github.com/rackt/react-router/blob/master/docs/guides/server-rendering.md

gpbl commented

Thanks @geekyme for the insight, that repo has been already source of inspiration for my project. Yet the react-async dependency does not fit well with my ideal architecture :(

I've been trying reflux.js as well, but to make it isomorphic friendly it should be instantiable. Also in this case, reflux would need to know about each store it uses, or we need a better support of React contexts which are still not officially supported.

Now, there is this helpful comment on the react repo by @JSFB:

On the server-side, I see no reason that everyone can't share a single flux store. Each request to the store would also need the userid for whom you're fetching the information, and apps running client-side would get a 403 error when trying to access data for another user. Your flux store may act as a cache, or may query the database directly, but either way, by making store requests that include the user, you can now share a single store across the app again.

This is definitely a thing to investigate.

Ah thats an interesting comment. I'm fairly new to react and I haven't really delve into using stores on the server side. What's the benefit?

gpbl commented

Well @geekyme the goal is to have a server-side rendering page (mainly for SEO & performance) without having to rewrite code doing the same stuff on the client :-)

Ah i see... and so every connected client interacting with this singleton store is bad?

gpbl commented

yeah on the server a store doesn't distinguish between different clients. The same component could receive the wrong state being triggered by another request requesting the same store.

I am also investigating on this, and find an example from the isomorphic-chat example, they use singleton pattern to initialize a store on each request https://github.com/mridgway/isomorphic-chat/blob/master/stores/MessageStore.js

gpbl commented

@fraserxu thank you for the tip. That isomorphic-chat is very similar to the Yahoo implementation they explained here. In the last slides, they show the weak part of this approach: the initialized dispatcher needs to be passed to each component using it. Having few component it is not an issue, but as you application grows, it could become cumbersome. That's why they came to the fluxible-app wrapper.

Maybe we can try something similar for reflux with react-router? These days I'm busy on another project so I can't play with the idea right now.

Yes. I was trying to use it with react-router+ flux and set up this rough idea thing https://github.com/fraserxu/isomorphic-github/blob/master/route.js

I like their idea of share the store(or state) with express-state, but as you mentioned that passing dispatcher around is not a good idea. But I don't have any better idea yet.

The problem I have is that react-router seems not have the concept for context, and I do not know when to load the data in my component(for now is hard coded).

@gpbl I built a proof of concept of isomorphic app (https://github.com/edjafarov/fluxnot) with React router and ontop of something like flux.
I stumbled on problem of context - it was pretty complicated to solve it for reflux/flux so I built it on simplified concepts.
I came up to Idea that we need a context. Within the context stores should be initialized. Thus those should be created per request on a server side and once on client side. On server side though the rendering should be performed after stores will be filled with data on a client side they could be rendered before. Then actions on a server should also be a property of context and stores should listen to their actions as well as components should listen to their stores and trigger actions within context. Otherwise it won't work properly on server.
Instead of flux actions I am using a chain of promises that emit events for stores. At the end of a chain I trigger rendering. Actions emit events for stores thus I definitely know that all stores are done at the end of chain.
Actions, stores and components have same context. Thus they will be rendered properly on server and client.

I find here is the best way to handle context so far: https://github.com/insin/isomorphic-lab/blob/master/src/utils/env.js

@fraserxu I meant app context. But I like this browser/node context differintiator

gpbl commented

@edjafarov you couldn't explain the problem better. I'm still not convinced by one request = one contexts solution.

The key concepts are

– stores are pre-filled with data on the server,
– nothing should listen to stores on the server
– some store should trigger relevant data for the current context.
– stores may cache data

But what is a context? As first I thought a request, but in many cases, stores would contain the very same data for each context. We would end using too much memory holding the same things.

Some examples:

  • A context may be defined according to the user's locale (one language = one store). All the server requests asking for data in french would use the same store.
  • A context may be defined according to the logged-in user (one user = one store). All the server requests coming from the same user would use the same store.
  • ... other examples?

We would need more granularity on defining what is a context and which content is triggered by a store. Since it's just about plain data, we could make (some) actions aware of contexts, so that when I run an action, the store would trigger only the data for the given context.

– stores are pre-filled with data on the server,

question here is how will you get the data to fill them?
My answer is to make all code isomorphic and use superagent to get data on server as well using server API. Thus how to get the data and fill the stores? My answer - at the end of actions stores should be filled with data.

– nothing should listen to stores on the server

you are absolutely right. There need to be a way to get initial state for components right from the component. Like this: https://github.com/edjafarov/fluxnot/blob/master/components/UsersList.js#L12

– stores may cache data

I am not sure that is a great idea, especially on server, but that is discussible.

We would end using too much memory holding the same things.

context should die when request is ended, no sticky sessions or stuff like that. Node way without keepein state.

Keeping stores on server for reuse is not a good idea - because we would get tons of problems with that starting with scalability, memory problems, syncronization problems, leaks etc.

gpbl commented

yeah @edjafarov definitely the cache thing doesn't make sense :) That's because I'm trying to apply Reflux to a server side rendering, skipping the need of initializing multiple stores.

My answer is to make all code isomorphic and use superagent to get data on server as well using server API. Thus how to get the data and fill the stores? My answer - at the end of actions stores should be filled with data.

Yes this feels the right approach. In my view, the component's getInitialState() is basically the only part we should rely on when rendering server side. As you made in your 'getInitialState' in UserList, you pass a pre-filled, context-aware store for getting the initial state. Here is why you can't use Reflux, since it does not allow to initialize a store per context.

Still I believe it is utterly complicated and I'm open to review the basics.

Considering that server side components don't listen to stores and that client side we don't need contexts, isn't that a flux approach on the server is just the wrong thing? All this is needed once per server request, with flux stores being rehydrated client-side at the first call. I'd prefer then to write specific server-side code, without even knowing about flux on the server.

isn't that a flux approach on the server is just the wrong thing?

That is why I am not using flux/reflux.

But lets keep in mind that flux is not just framework - it is an architecture that is rising pretty awesome concepts we can use to get to our goal. That is why I had to build other framework. And so far it is working pretty well and I have promising results without side effects. With what I came up now I am almost not thinking about context.

gpbl commented

Yeah i understand your point – before the arrive of flux, I was also running isomorphic apps without so much hassle :-) We are all confronting the same problems, using our self developed solution: instead, I'd love to find a unique approach where people could contribute. Do you aim to make your fluxnot approach a bit more documented / reusable?

Yes that is my goal - to build a kind of framework out of it. So far It is not documented because it was just built - still working on better API. It works and I want to build one simple real app to check if there are no showstoppers for such architecture.
Also I really want to steal some of your solutions :) like how to render index.html - it is really awesome!

gpbl commented

thanks! good luck with that 👍

gpbl commented

I've made some progress! Far for being the hoped solution, it is yet the simplest one I could find.

The server side app won't even see the flux pattern.
I recall that the route's handlers are the only components having a state coming from a dehydrated store – while their children make a spare use of states and API requests, since they would be mainly use props.

In such configuration, all the logic can go in the getInitialState() of the route handler (the "component"):

  • on the server, the initial states of the stores are passed via props to the component's getInitialState.
  • on the client –having the store hydrated its data– the component's getInitialState will just use the stores' getInitialState.

The downside of this is that the developer must explicitly tell each route handler how to fill the store's initial data. With the use of statics and a server-only mixin, though, the thing doesn't look so horrible. I hope to make a demo soon...

getInitialState is not async. Thus stores should be somehow binded to routes - the route need to know what stores and how they should be filled and also when they are ready. Right?
It might make sense to have getAsyncInitialState :)

gpbl commented

right @edjafarov , that's why the renderToString must happen only when the api requests are complete. as in the good old times:

Router.run(routes, (Handler, state) => {
const route = _.last(state.routes); // get the active route handler
route.statics.getStoresStates(params).then(states) {
   // handler will use storesStates
   var markup = renderToString(<Handler storesStates={states} />)
   // dehydrate states and add them to markup ...
   res.status(200).send(markup);
})

hey!! thanks for this awesome template 👍

I sow the "help wanted", so here's our first try of help #10.

I'm going to do some work on top of the "reflux" branch, because we are needing a template to start developing an isomorphic app which use React, React-router with server rendering, Reflux, Webpack, Express, and other tools like Gulp.
At the end it will use Drupal 8 (or 7) as REST API with HAL format to populate the stores.

Getting the stores populated, we could put them in the context using React.withContext(). The tricky part is definitively get the right Stores populated for the specific Route that is being rendered.

@gpbl I liked your solution putting statics methods in the Router, this is working already? there's an example? I would love to put that in the "reflux" branch.

Other thing is that I'm learning a lot (because I'm newbie in React) with the React Starter template from the Webpack project.
Here are some interested things I'm looking at:

gpbl commented

Hi @sebas5384 thank you for the pull request. I will look into it these days!

I didn't update the project during the last weeks because I decided to try fluxible, using its flux, router, services, and stores implementation. I'd prefer to adopt an existing isomorphic implementation than writing one on my own. I had to get rid of reflux and react-router 😢, but I am pretty satisfied with the result and I hope to provide in another repo an example of a full stack app.

The tricky part is definitively get the right Stores populated for the specific Route that is being rendered.

Here I went giving the stores a unique name and rehydrating them with a reflux store mixin. Each route would tell how to preload the stores. Fluxible implements this part by registering the stores in the app and using "fluxible contexts" (not to be confused with React contexts).

Also, in my fluxible app as well, i couldn't escape a server-side only part where I had to specify for each route which stores had to be dehydrated. The problem with reflux was that (without contexts, at least) I couldn't use the reflux actions (those calling the APIs) for pre-fetching data: I had to call the REST api directly and that was a too weak point.

Another big win in my last setup was to get rid of gulp – you can do all with webpack 👍

@gpbl I am waiting your Fluxible example impatiently :-)

@gpbl thanks for the explanation.

I give it a try yesterday at the fluxible branch and for the first time I could understand the app with no major code learning.
But! I really don't like the react router component from Yahoo, using directly action to transition from one route to another, and it seems that the React Router project is being adopted very well by the community.
Other thing is that for some reason the router can't modify the current URL when navigating to other route, so the URL get stuck with the first requested URL, example:

const LocaleSwitcher = React.createClass({
  mixins: [IntlMixin, StoreMixin],
  handlePhotoChange(e) {
    // Navigate to other page.
    this.props.context.executeAction(navigateAction, {
      url: '/photo/' + e.target.value
    });
  },

Do you know how to solve this?

Well, I was thinking, what if we could change only the router part?

Other thoughts I had is, Reflux with Fluxible plugins, like the fetchr to solve the dehydrated/rehydrated issue, could be a good one. Lately I'm going to send a pull request to show the example of context.executeAction(navigateAction, {...}).

Cheers!

@gpbl would you welcome a PR that adds in isomorphic flux? I've got an implementation that'll probably do what you want: alt

gpbl commented

@goatslacker oh alt looks promising. Thanks a lot for sharing. TBH the isomorphic flux branch is a bit outdated so I'd not waste time on it.

At this point, I'm not sure about the next step for this project. I think to a new fluxible version, because I've got some solid experience with it (at this point a choice has to be made :-). The other parts of this project (webpack, build, ES6 etc.) needs to be updated as well...

Yeah I see that fluxible is 100 commits ahead of reflux. I can track the fluxible branch.

If you need any help with the Fluxible implementation, let me know. I'm usually available on the Fluxible gitter for live help.

Have you looked at https://github.com/acdlite/flummox? It has an interesting approach to support isomorphism.

gpbl commented

So... things need to move on: during the last months I could work with fluxible a lot and I'm ready to update this project. I've written my plans in a separate issue.

Frankly, I don't believe the choice of the flux library matters so much. I adopted fluxible because I liked its approach and its plugins. Also, I sympathize with yahoo's devs because the YUI library :-)

thank you all for taking part in this discussion!