Awesome
UniversalJS
A universal Javascript starter kit inc. React, Redux, Redux Dev Tools, Universal Redux Router, CSS Modules, hot module reloading, Babel for ES2015+ and ESLint.
Demonstrates
- Flux using Redux
- Redux Dev Tools
- Routing using Universal Redux Router
- Server rendering
- Progressive enhancement
- Hot module reloading
- CSS Modules
- Style themes
- Testing Redux and React components
- Bundling using Webpack with Babel
- Enforcing coding style with ESLint
Read more about the reasoning behind these decisions.
Although this setup is far from simple or minimal, I have purposefully avoided all but what I see as core for a large project. For example, authentication and data fetching is outside the scope of this starter kit.
Usage
mkdir new-project; cd new-project
git clone git@github.com:colinmeinke/universal-js.git .
npm install
npm run build:dev
npm run start:dev
Structure
Entry points
The /src
directory is where all of the code for the app
lives.
The build process npm run build:dev
or npm run build:pro
will compile this code and output it into the /dist
directory.
Files that will be directly requested by the browser are then
copied into their appropriate directory within /static
.
There are two files in the root of the /src
directory:
server.js
client.js
These two files are the entry points into the app for their respective environments.
server.js
will listen for HTTP requests sent from a client
to the server. On receiving a request it will render the
response on the server and send it back to the client to
display.
client.js
will kick in if Javascript is enabled and
initialises in the browser. This will hydrate the app on the
client, and thereafter handle requests itself, removing the
need for additional requests to the server.
Shared code
The /src/common
directory is where all of the shared code
lives for both server and client. This is the beauty of
universal Javascript – shared code.
Within are three directories for Redux
actions,
reducers and
store configuration –
/actions
, /reducers
, /store
.
Components
The /components
directory also resides within the /common
directory. This is where all React components live. Both
presentational and container (those that connect to the Redux
store or have access to its dispatch method).
Each presentational component lives within its own
directory within /components
. The directory is named after
the component in question, e.g. <Button />
would be:
/components/Button/...
.
Connecting components to the Redux store is done by adding a
file to the root of the /components
directory. This file is
also named after the component, e.g. /components/Button.js
.
This structure means that all components can be imported into any file as follows:
import Button from './components/Button';
The file importing the component does not need to know if it is a presentational or a container component.
This is because of how imports resolve. It will first look
for a file within the /components
directory called
Button.js
. If that does not exist, it will then look for the
index.js
file within a directory called /Button
.
|-- common
|-- components
|-- Button
|-- index.js
|-- base.css
|-- Button.js
Terms, concepts and reasoning
Progressive enhancement
I get worried when I see very complex things getting built, things that are reliant of JavaScript. While its true that very few people are going to turn off JavaScript, the JavaScript can still fail. That can be okay if you're building in the right way. It will fall back to what's underneath the JavaScript.
- Jeremy Keith
Server-side rendering
If progressive enhancement is an aim, then we must provide the core experience of our app to users who have disabled Javascript in their browser, or situations where client-side Javascript has failed.
This necessitates that we render the same app on the server as we might on the client. The response from the server should be usable regardless of whether the client-side Javascript kicks in. That's just a bonus!
Server-side rendering isn't just about progressive enhancement and accessibility. It's also a huge win for performance and SEO.
Universal Javascript
If we are rendering the same experience on both the server and the client, then it follows that we should use the same language to build both.
When we use the same language for everything, it means we can abstract common code and share it between environments. Huge wins for maintainability, testing, cognitive load ... the list goes on and on.
NodeJS
NodeJS makes universal Javascript possible by running Javascript on the server.
Express
Express runs in a NodeJS environment and makes it easy to handle HTTP requests to the server and send a response.
React
At its core, this is a React app. React is how we write our components, render the user interface and keep it in sync with the state of the app.
React also makes rendering on the server really easy.
Flux
We need a way to manage the state of our React components. Flux is a pattern that can be used to architect how state flows through our app and how we update state.
However, Flux itself is only an idea. You can't download or install it.
Redux
Redux is a Flux implementation. It is a library that you can download or install.
It's beautifully simple and stores all app state in a single object.
It allows us to treat our user interface as a pure function, which when passed identical state will always render identical output.
It also allows our state to be serializable, and therefore storable or shareable. This makes the possibility of things like undo, redo, debugging and cross-device syncing very achievable.
Universal Redux Router
Universal Redux Router is a router I built to turn URL params into first-class Redux state. For full documentation head over to that repository.
CSS Modules
As much as I love working with inline styles, and using the power of Javascript to output styles, there is a lot to be said for CSS Modules.
CSS Modules allow you to write CSS that is locally scoped. This means that a CSS file can use the same class names that are in another CSS file without worrying about clashing. When the CSS is output, each class gets a unique hash – no need to rely on long-winded naming conventions like BEM.
Most importantly for me is that you can extract the CSS written this way into an external style sheet. This means you still get styling even if Javascript fails on the client.
A downside for me was getting CSS Modules setup in the first place to work how I wanted. For more comprehensive documentation on how to setup CSS modules with Webpack, check out another repository of mine that describes exactly that.
PostCSS
PostCSS is a preprocesser, a bit like Sass or Less. The difference is it works on a plugin system.
With the right plugins installed PostCSS allows you to write future CSS, and compile it to something that works on today's browsers. This is its major strength for me – you can just write CSS.
CSS themes
If we write all user interface as components, all of our CSS can be split by component too.
I like to allow theming, giving each presentational component
a base.css
file and then overriding those styles with a
${theme-name}-theme.css
file.
base.css
typically contains base layout rules and a very
simple grayscale color palette.
ES2015+
The Javascript features and syntax used within this repository follows the ES2015 spec.
Babel
The reason we don't have to care about browser support for the Javascript features and syntax we write, is because Babel takes care of transpiling our code to ES5. ES5 works on all modern browsers.
Build process
All build tasks are run using the surprisingly powerful scripts property built into npm.
Everything that can be done on the command line, can be done with scripts.
Here's a list of the some of the scripts I have setup:
npm run build:dev
– build to run in a development environment.npm run build:pro
– build to run in a production environment.npm run changelog
– create or update a changelog.npm run commit
– create a conventional commit message.npm run lint
– lint the code.npm run start:dev
– start the server in a development environment.npm run start:pro
– start the server in a production environment.npm run test
– run the tests.
Webpack
Webpack is the powerhouse behind
the build scripts npm run build:dev
and npm run build:pro
.
Webpack takes a Javascript file as an entry point. It runs through that file's dependencies, and its dependents' dependencies, bundling all that code into an output file.
In your Webpack config, you can tell Webpack to run various loaders on specific file types during bundle-time. For example, in this case we run all our Javascript files through the Babel loader to convert our ES2015+ features and syntax to regular ES5.
Loaders can be chained together, which can be very powerful.
For more information, check out the section on entry points above.
Hot module reloading
Part of a great development environment is not having to manually recompile your code and refresh your browser every time you make a change to your Javascript or CSS.
Hot module reloading solves this.
ESLint
Maintaining a consistent coding style is important, especially when there is more than one contributor.
npm run lint
will run ESLint on the Javascript using
standard style.
Commitizen
Commitizen helps us
write
conventional commit messages.
When commiting code, instead of git commit -m "..."
type
npm run commit
.
This guides us through the process of writing a conventional commit message by prompting us for various data about the changes we have made.
As well as maintaining consistent commit messages across the project, this can have other extremely useful benefits.
There are libraries such as sematic release or conventional changelog that can understand the conventional commit message syntax and run tasks based on that.
Help make this better
Issues and pull requests gratefully received!
I'm also on twitter @colinmeinke.
Thanks :star2:
License
ISC.