arqex / fluxify

The simplest Flux implementation.
GNU General Public License v2.0
101 stars 5 forks source link

Server-side rendering via renderToString? #2

Open sahat opened 9 years ago

sahat commented 9 years ago

I was just looking through a few Flux implementations and stumbled upon fluxify. One major problem I have with Flux is the difficulty of building isomorphic apps. With React (no Flux) it's quite easy because you can pass the props to renderToString and set the state directly on the component.

But with examples like Chat app you don't store the data on components; it's delegated to the stores. As a result, I cannot generate a component's markup since I can't pass the data via props anymore.

Does fluxify have a way around this problem?

Thanks.

arqex commented 9 years ago

Hi Sahat,

In the projects I have played with server side rendering, I have used just React to achieve it. Apps usually have a main store, ( called appStore in most of examples ) that reflects the state of the application given an URL. If your application has an object similar to that, it is easy to render in the server giving it to the React app object and using renderToString.

Unless you have a common state for all requests in the server - see fluxible - I wouldn't use flux in the server, because flux is created to handle UIs changes and in the server a UI is useless.

Said so, there would be something that'd be great to have in fluxify in order to make it more isomorphic: a router. With a router that could be "shared" between client and server, isomorphic apps would be much easier to create. In the server that router could re-create states based on URLs. In the client that state would be fetched when the URL changes and merged with the current one to refresh the app.

I've been having the router idea in my mind for a while, but I haven't started to work on it. What do you think?

ArnoBuschmann commented 9 years ago

There are already routers aiming for isomorphic routing such as React Router or Monorouter for example. This project shows, how isomorphic rendering can get implemented nicely with react-router:

https://github.com/irvinebroque/isomorphic-hot-loader

This structure can be combined with Fluxible, any other flux based project or even immutable data structures such as Omniscient for example -> http://omniscientjs.github.io/

When it comes to do things isomorphically, what I am thinking about is something it seems no one has cared about yet too much, which is:

Imagine, a user has changed the state of a page client side and reloads it -> now the server renders the page again with "initial state", so the previous state got lost. If we want to get the last user state again, it leads either to the question "Should the local state get synced somehow with the server, so it can use that one for the render instead of the initial state?" or "Should we store the user state in local storage and create components, which are checking local storage for user state before mounting the component?"

arqex commented 9 years ago

Thanks for the links Arno. I have never really liked React Router because I think that markup should only be used for content, and I feel weird defining routes that way. But, monorouter seems to be close of what I was thinking, I need to test it!

The way of storing client-side state is another issue. The option of storing it in the client itself (cookies or local-storage) seems easier, since no communication (or automatic with cookies) with the server is needed. But then you need to merge initial and client-side in the browser, making the browser logic more complex the view different than the one created by the browser.

A classical session storage on server can be used and it would make it simpler from the rendering point of view, since the inital and client state are always merged before it is passed to the rendering system.

Both problems (routing and clientstate) I think they are outside of flux architecture scope, that defines how to update the views but says nothing about the source of those updates.

ArnoBuschmann commented 9 years ago

@arqex Yes, I guess when we are speaking about client-server-state-synchronisation, it is already about the fine tuned details and it's not needed too often anyway.

Not sure if you had a look into immutable data structures yet, which is a very fascinating topic and actually relates a lot to flux, leading to questions such as: Do they replace the flux pattern or is it even useful to combine them? After sticking my head into Omniscient (link mentioned before) but also Morearty

https://github.com/moreartyjs/moreartyjs

I actually think that yes, some sort of flux solution might play nicely together with them as we still need places to put methods which I would label as "stores" kinda.

Curious about your thoughts on this topic :)

Concerning React Router I have problems to combine it with Morearty as React Router requires the routes top level and Morearty sort of wraps the top level component for its bootrapping process. So I might have to try Monorouter as well maybe.

Cheers, Arno.

arqex commented 9 years ago

Ey Arno, you shared a great link again! I didn't know about morearty either. I have a look at their docs and they are also using a router in their todo example but they didn't say which one.

I like so much the idea of immutable objects for React, they makes you impossible to update the store inside your components and that makes all the apps much easier. I started to use immutable.js with the first versions of fluxify, but it was another dependency for the public release and I wanted it to have no dependencies. So I created store properties immutable in fluxify my own way, making the package smaller and self-contained.

In the case of morearty, it seems that is somehow a different pattern than flux, isn't it? There is no dispatcher, and it relies on a binding system to make update petition and listen to them. I need to use it to have a better understanding, but it seems to follow what fluxify enforces: All the updates must be made on the top of the application ( in this case the binding system ) and go down through the component tree.

As for the router, I am still using backbone router in my client side: evenfult and with history handling. It is really nice, but I need to require the full backbone ( and jQuery ) library to use it. That is why creating a router the next step for me.

Nice chat here! :D

Cheers

ArnoBuschmann commented 9 years ago

Hi arqex, no matter if Omniscient or Morearty, both share the same binding concepts of curors. Cursors, originating from immutable.js, are like pointers into a nested data structure. So let's say a React component just needs some deeply nested small part of the entire app data, it can register a cursor only for that part (leaf of the tree) and whenever that data changes, the component gets an automatic notification and can rerender with the new data.

The idea here is, that the entire data of the app gets stored in one immutable object, so there is no redundancy, no unnecessary duplication of values and so on. Aside of being less error-prone, there are two other nice advantages now:

Due to the fact, that the object is immutable, you can store "history" for free, so it enables you to implement undo/redo functionality very easily. Another very fascinating thing is, that you can use transit.js (implemented in Morearty), a library to serialize the entire app state (the immutable object) and transfer/save it to, for example, restart the app later again or on another computer with the last state while still! having the undo/redo data. Needs to be mentioned, that keeping history is not mandantory. Last but not least, immutable objects are able to not use a lot memory due to a smart vector referencing algorithm. Oh, and transit.js by the way is also used to transfer the data between different languages such as Javascript, C, Closure and others.

Routers... I had a short look at Monorouter yesterday and I didn't got friends with it. For isomorphic purposes it suggests to use Webpack and bundle all components together with the express server itself. At the end there are two big bundles, one for the server, one for the browser. Honestly I'm not sure, if that has advantages, it just did not feel "right" for me ;)

So if you are going to develop a neat router, it would be a pleasure for me to have a look at it. Indeed it feels a bit like there's no perfect router match on the scene yet or to put it differently, there's still space for further creative ideas.

Agreed, nice chat! :)

Cheers

ArnoBuschmann commented 9 years ago

Oh and just watch the video on this blog post to see transit.js in action (aside of the interesting contents of that post): http://blog.circleci.com/local-state-global-concerns/

arqex commented 9 years ago

Hehhe, too much work for me! I need some time to reply to your last message and now I need to read that too!

Thanks for your links they are useful, I will try to reply properly tomorrow :smile_cat:

ArnoBuschmann commented 9 years ago

Haha sure, take your time. I just sent you all these information as I'm (almost) sure, they fit to your project in one or another way and that you'll find them as interesting as I do :)

arqex commented 9 years ago

In the way I see the immutability, the main reason of using it is to enforce the developer not to modify a part of the application in a certain level that can affect the rest of the application, because doing so it is easy to lose the track of where and when the changes had happened. The cheap undo/redo feature it is great too, but it is something extra that don't make devs change to immutable objects by itself.

Have a look at this speech by David Nolen I assisted here in Barcelona, you will like it:

https://www.youtube.com/watch?v=mS264h8KGwk

So immutable stores makes easier to think about an app works, but I think it is not the important part of the application (well, in morearty.js it is :smiley: ) what I see really important it is having a global state for the main part of the application. Every article I read stands for one app state as the source of truth of the application, and that store should be modified only in one place ( dispatcher, binding system... ). I would be glad to use immutable.js with fluxify if it's size wasn't 55kb minified, but adding that weight to a 5KB library I think is too much.

I liked the article speaking about local and global state paradigm in react. Again, it reflects the need of storing all the data in the appStore. Even if it is easier to encapsulate the components by themselves and make them responsible of their local state, they are always trying to reflect the changes in the app state using the intercession.

It is an interesting point of view, intercession will be easy to implement for javascript developers (not just clojure ones) when proxies can be used.

But I am not sure if it is important to save absolutely all the state data into the appState, in fact, following react premises for states, local states should store data that changes independently from the global state, and they warn that local states should be as small as possible. If those changes updates the global state ( if you want to store everything in the global state ) it is much better to dispatch an action -> update global store -> re-render the current component using props.

If you want to recreate exactly the same app state from one session to another, you should store everything in the appState, but that is not always the desired behaviour. Imagine the chat window in facebook inside the facebook application, I don't want to save all the chat messages in the app state, probably I would create the chat as self-contained component, and store the chat id in my appState, so the chat would have its own store and even it can have its own dispatcher.

A simpler example would be a to do list where I can select multiple items to make an action on them. If I select some items, close the session, and days later I open the todo app again, for me it is not important to restore the items as selected. So the selected state could be local.

From my point of view, the concept of appState is tightly coupled to a own URL. The minimum data I need to render an app complete view. The rest of data is client specific state, the one you asked for in your first message that is not handled by any isomorphic library ( I think fluxible does ). That client state may be stored separately for every user as needed.

That's why I think that the concept of router is really important in the flux architecture and isomorphic apps, because a URL should be translated directly to an appState. An URL change in the browser would automatically fetch the state data from the server, and the server could render the pages based on an URL effortlessly.

Wow! Long speach :D And I still haven't spoken about how I imagine a great router for fluxify! That's for the next message.

ArnoBuschmann commented 9 years ago

@arqex interesting read! :)

Another advantage I see in immutability is, that it provides a precise way to get a snapshot at any given time, so if being recorded, it is possible inspect things, which were not exploreable before. That can help to find bugs for example.

Thanks for the link to David Nolens video, yeah, I actually knew it already and it reminded me of another one from Rich Hickey, where he speaks entirely about immutability, perception and lots of other interesting things in 2009 -> http://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey

I agree completely in that is it not always desired to recreated an entire state. What I find very useful though is, that it is possible in general and that it will be an exact snapshot. In detail I can imagine some sort of abstraction on top, that describes, what parts of the state will be used/recreated finally, but it can be good to have a complete snapshot recorded at least.

URL, coupled with minimal state and separate client state sounds like a nice construct! :)

Curious about your ideas on a great router for Fluxify! :) Might be a nice idea to create it in a way that it works also decoupled from Fluxify.

Cheers, Arno.

arqex commented 9 years ago

@ArnoBuschmann I think that we agree completely In the benefits of immutability :)

I will give a shot on how I imagine the router, including immutability. Of course, the router could be used besides fluxify, I would construct it as a separate module.

Here it is how I think the router should work.

Let's start from the association of a URL and state that I talked about in the last comment.

The application should have a main store where it keeps the current app state ( the snapshot! ), its source of truth.

That app state may have two different parts, a server state and a client state.

Server state

Server state should be created by the server, and every different server state must have their own unique URL inside the application, so there should be easy to make the translation serverState <-> URL.

The server state can be seen as the initial state of the application for the URL, then it can be updated by user or server actions, but basically it is the base data that is needed to render the URL.

An application may modify the serverState in the client side implying a different URL, so the router should update URL automatically to reflect that state. <-- I am not sure about this feature.

The URL may be updated in the client side, for example following a link or clicking on browser's history buttons, so the router should fetch the URL state automatically from the server ( or maybe history cache ) and update the app.

If immutable.js is used for the stores, it is easy to implement history handling without fetching from the server, but I think this feature can be optional. Create a way of tell fluxify-router that immutable is available to use with the stores, and then use it to handle back and fordward button click.

The way of updating the URL should follow the flux pattern, so maybe it is worthy create the router as a flux store, that responds to a 'navigate' action. That 'navigate' should update the url property of the routerStore, so other stores may have a 'navigate' action callbacks or listen to url changes in the routerStore.

That routerStore may also have the historic state changes as a property if the history handling is on.

I wouldn't create the server router as a express middleware link in monorouter. The server may have routes that are not expected to be handled by fluxirouter. I like the way that the server router is initialized in monorouter though, passing the client router as argument, but in this fluxirouter the server is the one which generates the states, so if we rely only in server states generated by URLs there is not the need of knowing the routes on the client side, just request a URL expecting the state data.

In the server the router should return the rendered view for non-ajax calls. I would ship react rendering using the root react component and its 'renderToString' method by default. That render method should be overridable by the developer, in case he/she doesn't want to use react for the views.

Ajax calls to the router should return only the state data letting the client app update by itself.

I am afraid that the router should know about making ajax calls, or using sockets if the app use them. What would be the tiniest way of support both without dependencies?

Client state

The client state would be the part of the app store that is not bound to an URL. Not all the user actions may change the url, but they may update the UI (selecting some element for example ) and the appStore.

This client state may have the same properties than the server state. Imagine an app that show a box that can be open or not. The server state would be:

// Let represent the store as a plain object
var appStore = {
    id: 'box1',
    open: false
};

Then the user clicks to open the box and the appStore turns into

{id: 'box1', open: true}

The open: true property is the client state.

It should be great to create a way of save it completelly or partially in the server to restore user "sessions".

I think that saving that data is beyond the scope of a router, but it would be great that the router could offer a method to communicate that state to the server or adapt it to be saved in the client itself.

Also the router should give a way of merging client an server states to create the app store.

And I think that's all at the moment. I am looking forward to reading your comments :)

ArnoBuschmann commented 9 years ago

@arqex Before you start to create a new router, we can publish a book about how it should look like ;) Here are my thoughts...

In general I would try to follow a certain isomorphic principle: define once, use twice. For example routes should be defined in one place for both, client and server.

I like your idea to separate two types of state, I just wouldn't call them server and client state though. Renaming them into "initial state" and "ongoing state" makes it possible to think in new directions such as... it doesn't matter, if we are on the server or the client, what does matter is, that apart from that initial state we had once, the ongoing state developed further to this exact point in time - we have a snapshot of it at any given moment, that we can render into a view.

Just as there is ongoing state on the client side, the server wouldn't know about it and not be able to render the ongoing state after a reload for example.

But what if... we have an immutable state object for the entire app on the client side, that is synchronizing itself with another immutable state object for the entire app on the server side? Same structure, mirrored on both sides. By opening a proxy connection between client-server, the client side navigation/redering could stay completely client side and the server-syncing can run easily in the background without any performance problem.

Now both sides know about the exact ongoing state and can always render the latest page.

This way I can imagine, there is a very robust state propagation while it seems easy to reason about the flow.

Did you got familiar with Immutables cursor concept? This fits perfectly well to the idea I just described which is that components can set so called cursors into a specific region/tree/leaf of the data structure and whenever the data changes there, they receive a change event and can reactively rerender.

Another idea refers to your statement, that it is sometimes not wanted to recreate the previous ongoing state but render the initial state after a reload again. I think, this could be easily accomplished by a simple flag for the route such as: var renderInitialState = false/true.

Of course ajax requests/async data loading should be always possible, Reacts statics seems to be a good place for this.

Finally I like your idea of being able to simply save a session with just a click! :)

Cheers, Arno

arqex commented 9 years ago

Ey Arno, interesting thoughts! Specially the one about writing a book, we just need some more pages :D

I agree in most of the points.

I think we can make a list of the tasks that the router should handle:

Client side

Server side

I am sure I am forgotting stuff... what do you think?

Cheers

ArnoBuschmann commented 9 years ago

@arqex Before I will answer more detailed later on, for now a short shot on the cursor discussion.

I just stumbled over this project:

https://github.com/Yomguithereal/baobab

Although it is not using immutable.js (what I would strongly prefer), it is explaining the concept of cursors quite nicely.

ArnoBuschmann commented 9 years ago

@arqex Yes, indeed the router has to stay independend/agnostic from the type of stores being used, good point.

Your feature list looks promising and I don't have to add something right now.

I guess the challenge is to create a neat, generalized implementation so the router can cover a wide spectrum of use cases. All upcoming problems/details are probably not foreseeable anyway, so it looks like a good starting point.

arqex commented 9 years ago

Nice you like the points I have written down! The first thing that we should do is create the repo. I see you have your github empty! Do you like to be the owner of the repo? We also need a name for the library....

I think the first thing we should determine is how to create routes in the server, and in the client, how to make asynchronous calls without depending on any library.

As for the stores, I have read about baobab, and other tree structures with cursor support, and it makes me think a lot about how to create a perfect store for a js application. I want to create a gist with some code to show what bothers me about the store issues.

ArnoBuschmann commented 9 years ago

Hey @arqex, haha, I'm glad I got you looking into structures with cursor support, mostly because I find the concept itself fascinating. The reasoning is different to a classical Flux concept, I'd say. So I'm curious about your gist! :) I could imagine, that a cursor based solution has not to be necessarily integrated into your Fluxify but could make up a separate project.

Concerning routes, I think there are three things to have in mind how to handle them:

Client side:

Server side:

So one thing is how to describe the basic information/route and the other how to implement the three scenarios.

I consider myself being creative, but my programming skills are actually on a beginner level, so I leave it up to you to create a repo. It will be your project but you can always show me what you did and ask for my opinion and I might test things how they work. Guess it's always nice to exchange creative ideas ;)

Probably we should use some communication service/project platform, so we can discuss ideas privately, I think that's not possible on Github, is it? Any suggestion?

arqex commented 9 years ago

Ey @ArnoBuschmann

It's been a while since the last message! Christmas hollidays and work kept me far from fluxify for a while, but I had a lot of time to think about stores and routers :)

I've been working on a new app and at the same time I've been creating a new store very influenced by your beloved cursors and using immutable data. I would like to show it to you and know your thoughts:

https://github.com/arqex/curxor

As for the router, last week there was the first react.js conference and facebook introduced their way of fetching the data, it is quite interesting:

https://www.youtube.com/watch?v=9sc8Pyc51uU https://gist.github.com/wincent/598fa75e22bdfa44cf47

Maybe we can communicate better if you drop a message here and I can reply via email. http://arqex.com/contact

Cheers, Javi

ArnoBuschmann commented 9 years ago

Hi Javi,

nice to hear you are all well and have been devoping things such as Curxor.

I'm going to drop my further thoughts there in a minute.

Cheers, Arno