Skip to content

captainill/isomorphic500

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

isomorphic500

Isomorphic500 is a small isomorphic web application featuring photos from 500px.

It is built on express using React and Flux with yahoo/fluxible. It is developed with webpack and react-hot-loader and written with babeljs with the help of eslint. It supports multiple languages using react-intl.

Join the chat at https://gitter.im/gpbl/isomorphic500

The intent of this project is to solidify my experience with these technologies and (maybe) to inspire other developers in their journey with React and Flux. It works also as example of a javascript development environment with all the cool recent stuff :-)

  • see the demo on isomorphic500.herokuapp.com (with source maps!)
  • clone this repo and run the server to confirm it is actually working
  • edit a react component or a css style, and see the updated app as you save your changes!
  • read on for some technical details
  • write issues and join the gitter chat to discuss :-)

PS. The previous, non-flux version of this repo is in the [isomorphic-react-template] (https://github.com/gpbl/isomorphic500/tree/isomorphic-react-template) branch.

Clone this repo

git clone https://github.com/gpbl/isomorphic500.git
cd isomorphic500
npm install

Start the app

npm run dev

and open localhost:3000.

You can also try the built app:

npm run build   # First, build for production
npm run prod    # then, run the production version

then open localhost:8080.

Table of Contents

Application structure

$ tree src

├── Application.js       # The root Application component
├── actions              # Actions creators
├── app.js               # The Fluxible app
├── assets               # Dir with static files
├── client.js            # Entry point for the client
├── components           # React components
├── config.js            # Load the config on dev or prd
├── constants            # Constants values (e.g. action types)
├── pages                # Contains route handlers components
│   ...
│   └── RouteActions.js  # Actions executed when rendering a route
├── public               # Only in prod: contains static assets loaded with webpack
├── routes.js            # Routes config
├── server               # Server-side-only code
│   ├── ga.js            # Contains Google Analytics code to inject into HtmlDocument
│   ├── HtmlDocument.js  # Components containing <html>...</html> page
│   └── render.js        # Middleware to render HtmlDocument server-side
├── server.js            # Run the express server, setup fetchr service
├── services             # Fetchr services (e.g. load data from 500px API)
├── stores               # Flux stores
├── style                # Contains the Sass styles
└── utils                # Some useful utils

The fluxible app

The src/app.js file is the core of the Fluxible application:

  • it configures Fluxible with Application.js as the root component.
  • it registers the stores so they can work on the same React context
  • it adds the routr plugin (the routing interface) and the fetchr plugin (to share the same API requests both client and server-side)
  • it makes possible to dehydrate the stores on the server and rehydrate them on the client
  • it provides a componentActionHandler to make the app react to errors sent by flux actions

Async data

I used Fetchr and the relative fluxible-plugin-fetchr. Fetchr services run only on server and send superagent requests to 500px.

Router

Using fluxible-plugin-routr, I could keep the router in a "flux flow": the current route is stored in the RouteStore, and the Application component listens to it to know which page component should render.

Before setting the route, this plugin can execute an action to prefill the stores with the required data. (see the action attributes in the routes’s config).

Note that these actions can send an error to the done() callback, so that we can render an error page, as explained below in the "RouteStore" section.

Stores

Components do not listen to stores directly: they are wrapped in an high-order component using the fluxible connectToStores add-on. See for example the PhotoPage.

(Thanks @gaearon for exploring this technique in his article).

Resource stores

While REST APIs usually return collections as arrays, a resource store keeps items as big object – like the PhotoStore. This simplifies the progressive resource updates that may happen during the app’s life.

The RouteStore

The RouteStore keeps track of the current route.

Loading state

When a route is loading (e.g. waiting for the API response), the store set the isLoading property to the route object. The Application component will then render a loading page.

Route errors

A route error happens when a route is not found or when the service fetching critical data has returned an error.

In these cases, the RouteStore set its currentPageName to 404 or 500, so that the Application component can render a NotFoundPage or an ErrorPage.

Note that a not-found route may come from the router itself (i.e. the route is missing in the config) but also when a route action sends to the callback an error with {status: 404}.

Internationalization (i18n)

To give an example on how to implement i18n in a React application, isomorphic500 supports English and Italian.

Here, for "internationalization" I mean:

  • to format numbers/currencies and dates/times according to the user's locale
  • to provide translated strings for the texts displayed in the components.

This app adopts React Intl, which is a solid library for this purpose.

How the user’s locale is detected

The app sniffs the browser's accept-language request header. The locale npm module has a nice express middleware for that. Locales are restricted to those set in the app's config.

The user may want to override the detected locale: the LocaleSwitcher component set a cookie when the user chooses a language. Also, we enable the ?hl parameter in the query string to override it. Server-side, cookie and query string are detected by the setLocale middleware.

So, the locale middleware will attach the desired locale to req.locale, which come useful to set the lang attribute in the <html> tag. This attribute it is also used by client.js to load the locale data client-side.

The difficult parts

React-intl requires some boilerplate to work properly. Difficulties here arise mainly for two reasons:

  1. React Intl relies on the Intl global API, not always available on node.js or some browsers (e.g. Safari). Luckly there's an Intl polyfill: on the server we can just "require" it – however on the browser we want to download it only when Intl is not supported.

  2. For each language, we need to load a set of locale data (used by Intl to format numbers and dates) and the translated strings, called messages (used by react-intl). While on node.js we can load them in memory, on the client they need to be downloaded first – and we want to download only the relevant data for the current locale.

On the server the solution is easy: as said, the server loads a polyfill including both Intl and the locale data. For supporting the browser, we can instead rely on our technology stack, i.e. flux and webpack.

Webpack on the rescue

On the client, we have to load the Intl polyfill and its locale data before rendering the app, i.e. in client.js.

For this purpose, I used webpack's require.ensure() to split Intl and localized data in multiple chunks. Only after they have been downloaded, the app can be mounted. See the loadIntlPolyfill() and loadLocaleData() functions in IntlUtils: they return a promise that is resolved when the webpack chunks are downloaded and required.

They are used in client.js before mounting the app.

Important: since react-intl assumes Intl is already in the global scope, we can't import the fluxible app (which imports react-intl in some of its components) before polyfilling Intl. That's why you see in client.js require("./app") inside the in the renderApp() function, and not as import on the top of the file.

Internationalization, the flux way

Lets talk about the data that react-intl needs to deliver translated content. It is saved for each language in the intl directory and can be shared between client and server using a store, i.e. the IntlStore.

The store listens to a LOAD_INTL action dispatched by IntlActionCreator. We execute this action server side before rendering the HtmlDocument component in server/render.js, together with the usual navigateAction. The store will be rehydrate by Fluxible as usual.

An higher-order component would pass the store state to the react-intl components as props. For doing this, I used a custom implementation of FormattedMessage and FormattedNumber, adopting a small connectToIntlStore utils.

Sending the locale to the API

While this is not required by the 500px API, we can send the current locale to the API so it can deliver localized content. This is made very easy by the Fetchr services, since they expose the req object: see for example the photo service.

Development

Webpack

Webpack is used as commonjs module bundler, css builder (using sass-loader) and assets loader (images and svg files).

The development config enables source maps, the Hot Module Replacement and react-hot-loader. It loads CSS styles with <style>, to enable styles live reload). This config is used by the webpack-dev-server, serving the files bundled by Webpack.

The production config is used to build the production version with npm run build: similar to the dev config, it minifies the JS files, removes the debug statements and produces an external .css file. Files are served from a express static directory (i.e. /public/assets).

Both configs set a process.env.BROWSER global variable, useful to require CSS from the components, e.g:

// MyComponent
if (process.env.BROWSER) {
  require('../style/MyComponent.scss');
}

Files loaded by webpack are hashed. Javascript and CSS file names are saved in a JSON file and passed to the HtmlDocument component from the server/render middleware.

Babeljs

This app is written in Javascript-Babel. Babel config is in .babelrc (it only enables class properties). On Sublime Text, I installed babel-sublime to have full support of the Babel syntax!

Linting

I use eslint with babel-eslint and the react plugin – config in .eslintrc. I also configured Sublime Text with SublimeLinter-eslint.

Code style with jscs using a config inspired by Airbnb's one. On Sublime Text, I installed SublimeLinter-jscs.

You can use this command to run both linters from the command line:

npm run linter

I use SublimeLinter-scss-lint for linting the Sass files (.scss-lint.yml).

Testing

I'm still a beginner with Flux unit testing – so tests are missing :-) I use mocha, using chai as assertion library.

To run the tests, use this command:

npm test

There's also the test coverage with isparta (based on istanbul):

npm run coverage

Debugging

The app uses debug to log debug messages. You can enable/disable the logging from Node by setting the DEBUG environment variable before running the server:

# enable logging for isomorphic500 and Fluxible
DEBUG=isomorphic500,Fluxible node index

# disable logging
DEBUG= node index

From the browser, you can enable/disable them by sending this command in the JavaScript console:

debug.enable('isomorphic500')
debug.disable()
// then, refresh!

About

Isomorphic javascript (ES6) app built with React and Fluxible

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 92.2%
  • CSS 7.8%