A minimal starter kit with React, Redux, server side rendering, hot module replacement, and Webpack 2. 100% TypeScript.
What you're here for:
What helps those things above:
- awesome-typescript-loader - TypeScript loader for Webpack
- css-loader - CSS loader for Webpack
- express - used for server side rendering also serves your bundle
- extract-text-webpack-plugin - create a separate CSS file to include it in the server side rendered pages
- 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
What is actually not necessary but will most likely be used:
- ts-node - runs TypeScript code without compiling it in your file system
- tslint - your code linter
- normalize.css - makes your CSS consitent across all browsers
- 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
Tooling:
- cross-env - allows to set NODE_ENV on all platforms
Most of the code is just the React/Redux application. The action creators and reducers are located in the src/modules
directory 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 the Hot Module Replacement, the server side rendering, and is also itself a tiny API service for the client app.
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.
The tsconfig.json
file is only used for the webpack bundle and debugging. The reason behind this is because we need to define different configurations when compiling webpack or the server or else the server code gets into the webpack bundle and vice versa. The server tsconfig can be found in scripts/build.ts
:
let program = ts.createProgram(["./src/server.tsx"], { lib: ["lib.es6.d.ts"], jsx: ts.JsxEmit.React, noEmitOnError: true, noImplicitAny: true, noUnusedLocals: true, sourceMap: true, outDir: "./dist/server", target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS });
However, you may prefer to use a secondary tsconfig.json
and run tsc
in parralel.
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 src/server.tsx
, the final request handler is where server side rendering is done. First the match
method of react-router
will get the components to render according to the URL (req.originalUrl
) and our defined routes.
In each of our components (i.e. src/components/Main.tsx
) that require data to be fetched we have a fetchData
static method. These methods will dispatch the necessary data to a newly created Redux store. This method must return a Promise
so we can wait for data that needs to be fetched asynchronously with Promise.all
.
After the data has been fetched we dispatch an action (setRendered
)that will tell the client not to fetch data again (see componentWillMount
method in src/components/Main.tsx
).
The Helmet.rewind()
call returns an object that we can use to write HTML tags with attributes that will be used by react-helmet
. If react-helmet
is used by any of our component the tags will be updated server side.
We can finally render our page using react-dom
's renderToString
with the help of react-router
's RouterContext
and react-redux
's Provider
. In the final page markup 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.
Then finally in /src/index.tsx
, we get our Redux state then we initialize our store with it.
You can debug in the VSCode editor by using the debug configurations in the .vscode
directory. Start the application with npm debug
and start the Node
debugger VSCode. 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 test in this package to let the choice up to you. If you wish to write tests I recommend that you use ts-node
with your test framework CLI.
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 or async/await) 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" ] }
And you're good to go !