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

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 [email protected]: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:

  1. server.js
  2. 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]

GitHub

https://github.com/colinmeinke/universal-js