Sunday, November 30, 2014

Isomorphic React + Flux using Yahoo's Fluxible, part 1

Earlier this year Facebook announced Flux, an "architecture for building user interfaces that eschews MVC in favor of a unidirectional data flow". The official Flux website does a pretty good job explaining the virtues and overall philosophy of Flux, but falls a bit short when demonstrating a real implementation.

That's where the Yahoo Flux examples come in. Not only do they demonstrate how to use Flux, but they do it by building an Isomorphic application.

For the examples, the Yahoo team is using some pretty cool, open source libraries they created: fluxible, dispatchr, fetchr, and routr, among others. At first glance I didn't quite understand what was going on and what role each library was playing, so I decided to rewrite the examples, one step at a time.

** All the source code for this post is available at this repo. Each commit corresponds to a new section on this post.

** Update: Feb. 8, 2015 - Code was updated to work with Fluxible v0.2.0 and nodemon v1.3.6 (thanks for the help Legogris!).

** Update: Mar. 26, 2015 - The Fluxible team has published a slightly modified version of this post on their site.

Hello World


We'll start by creating a very basic React component that renders the classic "Hello World".

We'll be rendering this component server-side, using an Express application.

This is how our server.js looks like:

This is a pretty basic Express app, but there are some React-specific details:

  • Line 5: Notice how we're requiring 'node-jsx' and calling its install() method. This allows us to require JSX files as if they were regular JS files, without having to worry about compiling.
  • Line 9-13: we're defining a custom middleware, which creates a new instance of our component and renders it using React's renderToString().

Using routes


To make our example more interesting, let's create two different components: <Home> and <About>, each representing one page.

And let's define some routes to match each page.

Let's also create an ApplicationStore, whose only job will be keeping track of which page should currently be displayed.

Things to note:
  • Line 8: we're using fluxible's createStore() to create our store.
  • Line 35-42: we're defining a getState() method that returns which pages are available in the application and which one should currently be displayed.
  • Line 11: we're defining an action handler for 'CHANGE_ROUTE_SUCCESS'. Whenever this action is dispatched, we'll call the handleNavigate() method. 
  • Line 19-31: handleNavigate() takes a route object and updates the page to be displayed.
Keep in mind the ApplicationStore doesn't do any rendering, it just keeps track of what should be rendered. We need to update Application.jsx so it reads the current page from the ApplicationStore.

Things to note:
  • Line 9: We're using the FluxibleMixin, which adds some convenient methods to interact with stores.
  • Line 11: We're getting the component state by reading from the ApplicationStore.
  • Line 16: We're using state.currentPageName to determine which component to render.
We now have all the pieces in place, but we need a way to tie them together. That's the job of our Fluxible App:

Things to note:
  • Line 6: We're creating our app by calling Fluxible() and defining Application.jsx as our top-level component.
  • Line 10: We're adding the routrPlugin to our app, and telling it to use our previously defined routes.
  • Line 14: We're registering the ApplicationStore with our app
And the last step to get our app working is to update our middleware on server.js, so it uses the Fluxible App

Let's take a step back and analyze what's going on here, by explaining what happens when a request is received:
  1. A new request is made by the browser, server.js receives it.

  2. Line 11: Our middleware takes over and creates a new context. Fluxible uses contexts as an encapsulation mechanism, to prevent data from leaking between requests.

  3. Line 13: The middleware then executes navigateAction, and passes it the current route as a param. navigateAction is a convenient method defined on the 'flux-router-component' library. It helps us deal with route matching.

  4. navigateAction uses the routrPlugin to look for a matching route. 
    1. If a match is found, a 'CHANGE_ROUTE_SUCCESS' action is dispatched. 
    2. If a match is not found, an error with 404 status is provided to the callback.

  5. The 'CHANGE_ROUTE_SUCCESS' action is dispatched to all stores registered with the app.

  6. ApplicationStore executes its handleNavigate() method in response to the 'CHANGE_ROUTE_SUCCESS' action, and updates its state.

  7. Line 25-28: The navigateAction callback is executed. Inside the callback, a new Application component instance is created, and is handed the current context. The context contains, among other things, the updated ApplicationStore.

  8. Line 29-31: We render the Application component as a string, and send the result as our response. Since the Application component gets its state from the ApplicationStore, the correct page is rendered. Note: the getStore() method provided by the FluxibleMixin knows how to get stores from the provided context.
The application should now be working. If we visit http://localhost:3000/ we'll se our home page, and if we visit http://localhost:3000/about we'll see our about page.

Adding a NavBar

1e08f68

Having to manually enter URLs to change pages is not much fun. How about we add a navigation bar at the top of our application to let users switch between pages?

Let's create a Nav.jsx component that renders one NavLink for each available page. A NavLink is a special React component defined by the flux-router-component library, which executes the navigateAction when clicked.

And let's modify Application.jsx so it renders our Nav component above the pages.

While this is enough for the NavBar to work, let's add some styling to the page to make it look nicer. Start by creating an HTML.jsx component to render a base template for our application. Among other things, this base template will make sure to fetch some styles from the Pure CSS library.

Finally, let's modify server.js so it renders our App component inside our new HTML component.

Client App

b7b6f4b

Up to this point all we've really done is create a traditional web application. We're still missing the client side code to get a real Isomorphic application in place.

The first thing we need is a way to have our server send the current state of the app to the client, so the client can then recreate it locally. Fluxible expects each store to define a "dehydrate" function to serialize its state, and a "rehydrate" function to de-serialize it.

We need to define these hydrate functions on our ApplicationStore:

Then we need to modify server.js so it attaches the current app state on the Express response object.

Things to note:

  • Line 12-13: We're extending our Express server with the express-state library. This library adds an expose() method to the Express response object. Whatever we set with expose() will be available on the response object's "locals.state".
  • Line 30: We're calling expose() on the response object, telling it to hold on to our dehydrated app state. 
  • Line 34: We're passing res.locals.state as a prop to our HTML component.
Now we can create a client app that grabs the dehydrated state sent by the server and rehydrates itself before re-rendering and taking over. 

We'll need a way to bundle up the client app so the browser can read it, so we'll use Webpack and its jsx-loader. The following config file should be enough to have it working:

At this point our client side app should be able to take over after the HTML is finished rendering.

Although the client app re-renders after it rehydrates, we shouldn't see any flashing. This is because React compares the new render with the current DOM state, and won't update the DOM unless something has actually changed.

However, if you try switching between pages, you'll notice the NavBar is now broken. Even though the navigateAction is being executed on click, and our ApplicationStore is being updated, our Application component is not re-rendering.  

This is easy to fix. Let's add a store listener so our Application component updates whenever the ApplicationStore changes.

Things to note:
  • Line 12-14: We're adding a store listener for the ApplicationStore. Whenever a store we're listening to emits a 'change' event, our component's onChange() function will be called (this functionality is provided by the FluxibleMixin of the fluxible library).
  • Line 18-21: our onChange function just gets a new state by asking the ApplicationStore for its current state.

You should now see the correct pages being rendered when you click on the NavBar, but you might notice the browser's URL is not actually changing. This can also be easily fixed, by using the RouterMixin provided by the flux-router-component library.

The RouterMixin updates the browser history whenever a route changes, and also handles popstate events (when a user clicks on the browser's back button) in a Flux-appropiate way.

Conclusion

In this post we've seen how easy it is to create an Isomorphic application with multiple routes by using Yahoo's Fluxible library. On part 2, I will expand this app so it can communicate with a backend API, in order to build a simple To-Do application.
Post a Comment