A minimal starter kit with React, Redux, server side rendering with React-Router 4, hot reloading, and Webpack 2. 100% TypeScript.
git clone https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit.git <directory_name> cd <directory_name> npm install npm start
This will start the demo application available at http://localhost:3000. At this point you can delete the src/example
directory and you will get a blank page! (and a few errors you will have to fix 😔 in src/index.tsx
, src/reducer.ts
and src/routes.tsx
)
- react and react-dom - your favourite JavaScript library!
- react-hot-loader - used for hot reloading
- redux - store for the application state
- react-redux - use Redux with React
- typescript - your favourite language!
- webpack - module bundler
- react-router-dom - routing
- react-router-config - helpful for server side rendering
- express - used for server side rendering also serves the Webpack bundle
- awesome-typescript-loader - TypeScript loader for Webpack
- css-loader - CSS loader for Webpack
- style-loader - load CSS from the Webpack bundle
- extract-text-webpack-plugin - create a separate CSS file from your Webpack config
- isomorphic-fetch - allows fetch to be used server side
- serialize-javascript - allows to safely pass the redux state from the server to the client
- webpack-dev-middleware and webpack-hot-middleware - hot module replacement and serve the bundle
- ts-node - runs TypeScript code without compiling it in your file system
- tslint and tslint-react - TypeScript code linter
- postcss-loader and autoprefixer - adds vendor prefixes to your CSS code
- react-helmet - easily changes your head tag
- @types/<your_favourite_package> - type definitions for TypeScript
- cross-env - allows to set NODE_ENV on all platforms
And that's about it!
Most of the code is just the React/Redux application. The action creators and reducers are located in the src/example/modules
and src/modules
directories following the convention proposed here. The root reducer is located in src/reducer.ts
.
Everything that is server side is located in src/server.tsx
which has a single express
application that is responsible for serving the webpack bundle, HMR, and server side rendering.
ts-node
allows TypeScript to be executed without compiling it in the file system. It is used to start the dev server, to compile the webpack bundle, and/or the server application using only Webpack. It also allows us to have a Webpack config in TypeScript. The only JavaScript files we get are for the production environment when building the application.
In your webpack config you can see that when not in production the following entries are added: react-hot-loader/patch
and webpack-hot-middleware/client
. Because of webpack.optimize.CommonsChunkPlugin
they'll be compiled into a separate JavaScript file that I named hot.js
unless I'm mistaken...
In src/server.tsx
, when not in production we have webpack-dev-middleware
and webpack-hot-middleware
used by the only express
application. They're reponsible of compiling and sending the webpack bundle.
Then finally in src/index.tsx
you will see:
if (module.hot) { module.hot.accept("./routes", () => { const App: any = require("./routes").default; render(App); }); }
This is what will be called whenever your webpack bundle is updated. This will re-render your application with the changes.
In production we use a separate CSS file that we get thanks to extract-text-webpack-plugin
. It allows us to send the CSS from a simple link
tag rather than via the bundle avoiding having to wait for the bundle to load.
However, since the HMR updates the bundle it wouldn't reflect your style changes if you used a separate CSS file so we use style-loader
in development in order to enable hot reloading with CSS.
In short this is how server side rendering is done:
- Find matched Route components
- Fetch data
- Render the components as HTML code
- Send the HTML code as the HTTP response
It's very similar to what is explained in react-router
's documentation.
In the routes.tsx
we have a bunch of routes in an array as plain JS objects. They will be used by react-router-config
's matchRoutes
and renderRoutes
.
Of course, we can still use the Route component in our application they will be rendered server side but we won't be able to retrieve them for server side data fetching.
In src/server.tsx
, the final request handler is where server side rendering is done.
We start by creating a store that will hold our application's data which we will eventually pass to the client. The setIsServerSide
call allows us to notify the components that we are in a server side context.
We get the matched routes with react-router-config
's matchRoutes
.
In each of our components that require data to be fetched we have a fetchData
static method. These methods will dispatch the necessary data to the Redux store.
i.e. in src/example/components/Main.tsx
we fetch the matched GitHub user by passing the dispatch method of our store and the Route params. Obviously, the fetchData
parameters need to be the same across all components.
This method must return a Promise
so we can wait for data that needs to be fetched asynchronously with Promise.all
. After async data fetching has been done we can finally render the application.
Keep in mind that the componentWillMount
method of each component will be called when rendering. But because we can't await promises created in there we can't use it for async actions. Awaiting the fetchData
calls also allows us to have an up-to-date store before rendering.
We can now render the application as a string with an up-to-date store.
Since our routes (previously obtained with matchRoutes
) are not real Route components but react-router-config
routes, renderRoutes
is needed to get the components. The components are wrapped in a StaticRouter
component to pass the request URL and get catch any redirections and then wrapped in a Provider
to pass the application's state to our components. The whole thing is passed as a parameter in a renderToString
call.
After rendering if a single Redirect
component has been rendered the context object passed in the StaticRouter
will have an url which allows us to know if the user needs to be redirected and where to redirect. Otherwise we can send the rendered application to the client.
We have only rendered the application just like we would with a ReactDOM.render
but this doesn't set the <head>
tag. In order to set the <head>
tag server side we need to use react-helmet
.
We start by calling renderStatic
before rendering which will return an object. When rendering every Helmet usage will update this object.
After rendering we can use the object that has updated head tags (such as <title>
, <link>
, etc.) in the final markup.
We just have to put everything together and send it back to the client. I choose to create the HTML code with the JSX syntax but then we need to use renderToStaticMarkup
with it.
We fill the <head>
tag with the help of the Helmet object and the application's HTML in the root element. We add a polyfill.io
script so we can use fetch
client-side and a script containing our Redux state that will be used when initializing the store client side. serialize-javascript
is needed for safety purposes you can read more about it here.
The webpack generated scripts are also added note that we don't add the hot.js
in production since it is the bit relevant to HMR and hot reloading and we don't want that in production.
Then finally in /src/index.tsx
, we get our Redux state then we initialize our store with it.
The tslint.json
config is taken from piotrwitek's React & Redux in TypeScript - Static Typing Guide.
If you wish to write tests I recommend that you use ts-node
with your test framework.
We can run mocha
tests with ts-node
with this command:
mocha --compilers ts:ts-node/register,tsx:ts-node/register <files>
We can also use the --fast fast option in ts-node
for faster compilation. For this we need to create a register JavaScript file similar to ts-node
that you should be located at node_modules/ts-node/register.js
except that we will add the fast
option:
require("ts-node").register({ fast: true });
You can find an example of an unit test in the mocha branch a test file a located in src/example/components/__tests__/About.test.tsx
. I added a test
script in package.json
with the updated command to run mocha with the new register file (that I named ts-node-register.js
):
mocha --compilers ts:./ts-node-register.js,tsx:./ts-node-register.js src/**/__tests__/*.ts*
In order to run the tests I need the required packages:
npm i -D mocha chai enzyme react-test-renderer @types/mocha @types/chai @types/enzyme
Then we can simply run the test script:
npm test
You can debug in the VSCode editor adding those configurations to your.vscode/launch.json
file:
{ "version": "0.2.0", "configurations": [ { "type": "node2", "request": "attach", "name": "Node", "address": "localhost", "port": 9229, "restart": true, "localRoot": "${workspaceRoot}" }, { "name": "Chrome", "type": "chrome", "request": "attach", "port": 9222, "url": "http://localhost:3000", "webRoot": "${workspaceRoot}" } ] }
Start the application with ts-node --inspect <file>
and launch the Node
VSCode debugger. It will automatically attach the debugger to your application instance.
In order to debug the client application you need to install the vscode-chrome-debug extension, then run Chrome with the --remote-debugging-port=9222
argument and open client application in http://localhost:3000
, and then run the Chrome
debugger in VSCode.
There is no any Babel insanity because it is not required if you have set target
to es5
and jsx
to react
in your tsconfig.json
. However, if you wish to use Babel (i.e. for plugins) this is what you can do:
Install Babel and friends:
npm i -S babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2
Then add it after the awesome-typescript-loader
like so:
{ test: /\.tsx?$/, use: ["babel-loader", "awesome-typescript-loader"], exclude: /node_modules/ }
You'll need to create a .babelrc
file with this (more explanation here)
{ "presets": [ [ "es2015", { "modules": false } ], "stage-2", "react" ], "plugins": [ "react-hot-loader/babel" ] }