isomorphic500
Isomorphic500 is a small isomorphic (universal) 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.
The intent of this project is to solidify my experience with these technologies and perhaps 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
Get help Join the gitter chat or the #isomorphic500 on reactiflux :-)
Clone this repo
Note This app has been tested on node 4
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.
If you are starting the server on Windows, please read #58
Table of Contents
Application structure
.
βββ index.js # Starts the express server and the webpack dev server
βββ config # Contains the configuration for dev and prod environments
βββ nodemon.json # Configure nodemon to watch some files
βββ src
β βββ app.js # The fluxible app
β βββ client.js # Entry point for the client
β βββ config.js # Config loader (load the config files from /config)
β βββ routes.js # Routes used by fluxible-router
β βββ server.js # Start the express server and render the routes server-side
β β
β βββ actions # Fluxible actions
β βββ components # React components
β βββ constants # Constants
β βββ containers # Contains React containers components
β βΒ Β βββ ...
β βΒ Β βββ Html.js # Used to render the <html> document server-side
β βΒ Β βββ Root.js # Root component
β βββ intl # Contains the messages for i18n
β βββ server # Server-side only code
β βΒ Β βββ ga.js # Google Analytics script
β βΒ Β βββ intl-polyfill.js # Patch node to support `Intl` and locale-data
β βΒ Β βββ render.js # Middleware to render server-side the fluxible app
β βΒ Β βββ setLocale.js # Middleware to detect and set the request's locale
β βββ services # Fetchr services
β βββ stores # Fluxible stores
β βββ style # Contains the Sass files
β βββ utils
β βββ APIUtils.js # Wrapper to superagent for communicating with 500px API
β βββ CookieUtils.js # Utility to write/read cookies
β βββ IntlComponents.js # Exports wrapped react-intl components
β βββ IntlUtils.js # Utilities to load `Intl` and locale-data
β βββ connectToIntlStore.js # Connects react-intl components with the IntlStore
β βββ getIntlMessage.js # Get react-intl messages
β βββ trackPageView.js # Track a page view with google analitics
βββ static
βΒ Β βββ assets # Static files
βΒ Β βββ dist # Output files for webpack on production
βββ webpack
βββ dev.config.js # Webpack config for development
βββ prod.config.js # Webpack config for building the production files
βββ server.js # Used to starts the webpack dev server
The fluxible app
The src/app file is the core of the Fluxible application:
- it configures Fluxible with src/containers/Root.js as the root component.
- it registers the stores so they can work on the same React context
- it adds 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
Async data
I used Fetchr and fluxible-plugin-fetchr. Fetchr services run only on server and send superagent requests to 500px.
Router
This app uses fluxible-router for routing. Fluxible-router works pretty well in fluxible applications since it follows the flux paradigm. The Application component uses the @handleHistory
decorator to bind the router to the app.
Stores
Instead of directly listening to stores, components use fluxible's @connectToStores
decorator: a store state is passed to components as prop. See for example the PhotoPage or the FeaturedPage.
connectToStore
can also "consume" store data without actually listening to any store. This is the case of NavBar or LocaleSwitcher.
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.
List stores
A list store keeps references to a resource store, as the FeaturedStore holds the ids of the photos in PhotoStore.
The HtmlHeadStore
The HtmlHeadStore is a special store used to set the <head>
meta-tags in the Html
component, during server-side rendering. It is also listened by the Application
component to change the browser's document.title
.
This store listens to route actions and set its content according to the current route. It also get data from other stores (e.g. the photo's title from the PhotoStore
), or the localized messages from the IntlStore
.
Internationalization (i18n)
To give an example on how to implement i18n in a React application, isomorphic500 supports English, Italian, Portuguese and French.
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.
Setting up react-intl
React-intl requires some boilerplate to work properly. Difficulties here arise mainly for two reasons:
-
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. -
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 byreact-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.
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 require
d.
They are used in client.js before mounting the app.
Important: since
react-intl
assumesIntl
is already in the global scope, we can't import the fluxible app (which imports react-intl in some of its components) before polyfillingIntl
. That's why you see in client.jsrequire("./app")
inside the in therenderApp()
function, and not asimport
on the top of the file.
Internationalization, the flux way
Lets talk about the data that react-intl
needs to deliver translated content. Translated messages are saved in the intl directory and shared between client and server using the IntlStore.
This store listens to a LOAD_INTL_SERVER
action dispatched by IntlActionCreator. We execute this action only server side before rendering the Html
component together with the usual navigateAction
. This allows to dehydrate/rehydrate the store content.
React-intl components need to have access to the IntlStore
. Plus, since I'm using ES6 classes, I can't adopt the react-intl Mixin
in my components. To solve this, I wrap the Formatted*
components and make them available from IntlComponents.
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
Run the development version with
npm run dev
nodemon
This task runs the server with nodemon. Nodemon will restart the server when some of the files specified in its config change.
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.
This config uses the webpack-error-notification plugin. To get notified on errors while compiling the code, on Mac you must
brew install terminal-notifier
.
The production config builds the client-side production bundle from npm run build
.
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');
}
On production, files bundled by webpack are hashed. Javascript and CSS file names are saved in a static/dists/stats.json
which is read by the Html component.
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!
.editorconfig
The .editorconfig file can be used with your IDE/editor to mantain a consistent coding style. See editorconfig.org for more info. (thanks to @lohek)
Linting
I use eslint with babel-eslint and the react plugin. I also configured Sublime Text with SublimeLinter-eslint.
I use the rules from my own eslint-config-gpbl shared configs.
npm run lint
I use SublimeLinter-scss-lint for linting the Sass files (.scss-lint.yml) (only with Sublime Text).
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!