Closed greaber closed 9 years ago
http://acdlite.github.io/flummox
Again, because Flummox does not rely on singletons, you get isomorphism for free: just create a new Flux instance on each request! Here’s a very basic example how that might look using Express and React:
Flummox is agnostic about your stores, your actions, or how any of them get data. See this from react-router on how to provision routes using a static fetchData
method for example.
Here's a simplified but relatively complete example of the general structure and server side flow I follow in my own isomorphic apps:
var UserRoute = React.createClass({
statics: {
willRender: function (flux, state) {
// async action that will return a promise which, when resolved, populates the UserStore
return flux.getActions('user').fetch(state.params.username);
}
},
propTypes: {
flux: P.instanceOf(Flux).isRequired
},
render: function () {
return (<User { ...this.props }/>);
},
componentDidMount: function () {
var props = this.props;
var flux = props.flux;
var user = flux.getStore('user').getUser();
if (user == null || user.username !== props.params.username) {
this.constructor.willRender(flux, props);
}
},
componentWillReceiveProps: function (props) {
if (props.params.username !== this.props.params.username) {
this.constructor.willRender(props.flux, props);
}
}
});
document.addEventListener('DOMContentLoaded', function (event) {
var flux = new Flux;
// populate Flux instance with data from the server; see below
flux.deserialize(window.flux);
var router = Router.create({
routes: require('../application/routes.jsx'),
location: Router.HistoryLocation
});
router.run(function (Root, state) {
var props = Object.assign({ flux: flux }, state);
var element = React.createElement(Root, props);
// don't render directly to document.body
var body = document.getElementById('body');
React.render(element, body);
});
});
<!DOCTYPE html>
<html lang="en">
<body>
<div id="body"><%- body %></div>
<script>
(function (window) {
// FLUX
window.flux = <%- flux %>;
})(this);
</script>
</body>
</html>
var flux = new Flux;
var router = Router.create({
routes: require('../application/routes.jsx'),
// request path from express, koa, etc.
location: request.path
});
router.run(function (Root, state) {
Promise.all(
state.routes.map(function (route) {
// call willRender on all routes to get async data
return route.handler.willRender && route.handler.willRender(flux, state);
}).filter(Boolean)
).then(function () {
var props = Object.assign({ flux: flux }, state);
var element = React.createElement(Root, props);
var body = React.renderToString(element);
var template = ejs.compile('../templates/index.html');
// response body from express, koa, etc.
response.body = template({
body: body,
flux: flux.serialize()
});
})
});
There's really nothing magical to it. You get data by calling Flummox action methods inside willRender
, which populate the stores before the server side rendering is done. The same willRender
method is called on the client side in componentDidMount
and componentWillReceiveProps
if the route data has changed. You serialize and deserialize your flux instance so the state is passed from the client to the server; this is what satisfies the conditions in the componentDidMount
method so your data only gets fetched once.
Thanks a bunch, this is super helpful! I hope you don't mind if I ask a few followup questions.
When you call willRender from componentDidMount or componentWillReceiveProps, it just fires off an async request to the server, so the next render will probably not have the new data, right? Do you need an onChange event handler to rerender when the store actually changes?
Passing the flux store through props is hygienic, but it leads to a certain amount of boilerplate. Is it possible/advisable to avoid this boilerplate somehow? On the client it is easy to use a global variable like you did for the serialized store, but on the server there is the complication that node doesn't really give you an easy way to scope something like that to a request. I guess this is the "singleton stores" issue I have seen discussed online.
How do you recommend loading the javascript on the client? It looks like the template might be missing a script tag to load the application code. Is the best practice to use async for that and to put the script tag at the beginning? Or would it be reasonable to inline it right after the
tag and make it synchronous instead of a response to DOMContentLoaded? And as for the serialized state, all the examples I have seen inline it as you do, but I don't understand why it would be bad to load that as an external resource if it is good to lode the application code that way.
I assume in @jordansexton's example, the async action will trigger an onChange event in the UserStore which the User component is subscribed to. There is no need for the UserRoute to update itself when the User is loaded in this case.
You can retrieve the Flux instance from context as long as you define the contextTypes in the component that requires it. Without the contextType definition, this.context wont be populated. This removes the need of passing flux down the chain of components.
class Component extends React.Component {
someFunction() {
const flux = this.context.flux;
//...
}
}
Component.contextTypes = {
flux: React.PropTypes.object.isRequired
};
@greaber
When you call willRender from componentDidMount or componentWillReceiveProps, it just fires off an async request to the server, so the next render will probably not have the new data, right?
var user = flux.getStore('user').getUser();
if (user == null || user.username !== props.params.username) {
this.constructor.willRender(flux, props);
}
It won't fire an async request to the server if the username hasn't changed because it expects the server rendered already.
Passing the flux store through props is hygienic, but it leads to a certain amount of boilerplate. Is it possible/advisable to avoid this boilerplate somehow?
This only occurs at the top level route, wherein I use FluxComponent
to pass it through the context. Nested routes don't need it. I use it here in the example for simplicity, but @merk is correct. I use connect
to link the component to the applicable store(s).
How do you recommend loading the javascript on the client?
This is stripped out of the template again for simplicity. I build my template with webpack using html-webpack-plugin with the EJS template variables preserved. A script tag referencing application.js?[hash]
is inserted into the head at this point.
@greaber
And as for the serialized state, all the examples I have seen inline it as you do, but I don't understand why it would be bad to load that as an external resource if it is good to lode the application code that way.
How would you do this? The serialized state depends on, well, serializing the state of your Flux instance. This is not available "as an external resource" because it's not static and is (and must be) created in the rendering context.
@greaber
I may not have explained this well, but you're not just serializing the state of static Flux stores. It's important to note that willRender(flux, state)
will call an action, fetch
in this case. This action will cause the store to change. In addition, you may have actions in your components on componentWillMount
which will also cause the stores to change. That's why I serialize the Flux instance after not only willRender
but also renderToString
.
In short, you can't just load Flux state into the application like any other file because even if we assume it's completely deterministic over its inputs (and it's probably not if it requires persistent data at a specific moment in time), it's still based on dynamic application state (route params, query string, user agent headers, cookies, session data, etc.) available on the server.
Thanks, @merk!
@jordansexton, thanks, I did understand that the stores are not static; I was thinking they could be served as an external resource (from you app, not a CDN or something), but maybe there is no point to doing this as it would be more complicated and possibly less performant. I didn't understand the subtlety about actions in componentWillMount that can cause the stores to change. What is a good example of when you would do this? You would have to limit yourself to synchronous actions, right?
I have been comparing Flummox with React Nexus. In general, React Nexus seems really interesting, and I'd love to hear what other people think about it. But I am very reluctant to use it because it seems to only be used by the people who wrote it.
One possible advantage of React Nexus is that it walks the whole component hierarchy to find willRender-like static functions to run, whereas the approach outlined in this thread can only pick up willRender functions in RouteHandlers, if I understand right. (Does react-router do component hierarchy walking internally to construct states.route, btw?) I don't have enough experience to judge how big of a deal this difference is, however.
Also, it is easy to walk the component hierarchy without using React Nexus, though you have to use undocumented stuff. Does this seem like a bad idea?
@greaber
Yes, you'd have to limit yourself yourself to synchronous actions in componentWillMount
. But this is easy when you have your async calls in willRender
because the props you need from there will already be in the stores.
An example would be isomorphic dynamic page titles that depend on async loaded data. I have a PageActions
class including a setTitle
method and a corresponding PageStore
. When the title changes, the store sets document.title
; this is wrapped in a if (typeof window !== 'undefined')
so it only runs on the client. On the server side, I use flux.getStore('page').getTitle()
in the template
call (after the renderToString
call) to set the <%- title %>
variable in the template.
Let's say in the UserRoute
example above that the User
component is connected to the UserStore
and has the following method:
componentWillMount: function () {
this.flux.getActions('page').setTitle('Application | Users | ' + this.props.user.fullName);
}
This results in some boilerplate, so I have a mixin and a higher order Page
component that does this succinctly throughout my app. It does the same things for meta description.
I'm not in a position to comment on React Nexus. I don't use it, and since this thread is ostensibly about isomorphic Flux as implemented in Flummox, it would make more sense to ask about it in the issues for that project.
@greaber
I did understand that the stores are not static; I was thinking they could be served as an external resource (from you app, not a CDN or something), but maybe there is no point to doing this as it would be more complicated and possibly less performant.
It would definitely be less performant since the browser would be making another (blocking) script request for data you could inline that needs to be there onDOMContentLoaded
. Not to mention the fact that you'd either have to write the data to a file and then manage that file, or somehow store the data in memory for the subsequent script request and then render it out as a script file when the route is hit.
I can't think of a reason to do any of these things, or any reason to persist the serialized data at all since you can't do so in a granular fashion. If you need to speed up static stores, you could serialize those separately and cache them, but you're probably better off just worrying about caching things that are actually slow like the async requests.
@jordansexton I am closing this issue since you have answered my main questions and I have decided to give flummox a try. The approach you suggest to setting dynamic titles seems fine, but just to test my understanding, is it right that some alternatives would be:
setDocumentTitle
function that is defined differently defined on the client and server.) I'm not sure how easy it is to swap in different definitions of things like this depending on whether you are on the client or server, however.?
Flummox promises isomorphism "for free", but it looks like it doesn't provide any special support for the trickiest part of isomorphism, namely injecting data from the database into the html on the server. The docs seem to suggest that doing this is an optional extra part of being isomorphic, but all the benefits I have heard about for isomorphic apps (namely, seo and fast perceived load time) depend on this data loading. Am I overlooking a key benefit of isomorphism? Or does flummox provide more than I think? (I admit I have not yet spent much time investigating it.)