├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── postcss.config.js ├── scripts └── build.ts ├── src ├── components │ └── Layout.tsx ├── example │ ├── components │ │ ├── About.tsx │ │ ├── GitHubSearchLayout.tsx │ │ ├── GitHubUserPreview.tsx │ │ └── Main.tsx │ ├── global.css │ ├── modules │ │ └── user.ts │ └── selectors.ts ├── index.tsx ├── modules │ └── serverSide.ts ├── reducer.ts ├── routes.tsx ├── server.tsx ├── store.ts └── typings.d.ts ├── tsconfig.json ├── tslint.json └── webpack.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # VS Code 40 | .vscode 41 | 42 | # Compiled files 43 | dist 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 François Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal React Redux starter kit with TypeScript and Webpack 2 2 | 3 | A minimal starter kit with React, Redux, server side rendering with React-Router 4, hot reloading, and Webpack 2. 100% TypeScript. 4 | 5 | ## Table of contents 6 | 7 | * [Demo](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#demo) 8 | * [Quick start](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#quick-start) 9 | * [Packages used](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#packages-used) 10 | * [How it works / Explanation / Deep dive](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#deep-dive) 11 | * [npm scripts and compiled JavaScript files](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#npm-scripts-and-compiled-javascript-files) 12 | * [Hot Module Replacement and hot reloading](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#hot-module-replacement-and-hot-reloading) 13 | * [CSS loaders](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#css-loaders) 14 | * [Server side rendering and async data fetching](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#server-side-rendering-with-async-data-fetching) 15 | * [tslint](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#tslint) 16 | * [Possible improvements](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#improvements) 17 | * [Extras](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#extras) 18 | * [How to run tests](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#testing) 19 | * [How to separate the Webpack dev server](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#separate-webpack-dev-server) 20 | * [How to use this project with a separate API server](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#separate-back-end--proxying-requests) 21 | * [How to use CSS Modules](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#css-modules) 22 | * [VSCode debugging](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#visual-studio-code-vscode-debugging) 23 | * [Add Babel](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#babel) 24 | 25 | ## Demo 26 | 27 | [https://universal-react-typescript.herokuapp.com](https://universal-react-typescript.herokuapp.com) 28 | 29 | ## Quick start 30 | 31 | ``` 32 | git clone https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit.git 33 | cd 34 | npm install 35 | npm start:dev 36 | ``` 37 | 38 | This will start the demo application available at [http://localhost:3000](http://localhost:3000) with Hot Module Replacement and hot reloading enabled. 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`) 39 | 40 | ## Packages used 41 | 42 | * [react](https://github.com/facebook/react) and [react-dom](https://github.com/facebook/react) - your favourite JavaScript library! 43 | * [react-hot-loader](https://github.com/gaearon/react-hot-loader) - used for hot reloading 44 | * [redux](https://github.com/reactjs/redux) - store for the application state 45 | * [react-redux](https://github.com/reactjs/react-redux) - use Redux with React 46 | * [typescript](https://github.com/Microsoft/TypeScript) - your favourite language! 47 | * [webpack](https://github.com/webpack/webpack) - module bundler 48 | * [react-router-dom](https://github.com/ReactTraining/react-router) - routing 49 | * [react-router-config](https://github.com/ReactTraining/react-router) - helpful for server side rendering 50 | * [express](https://github.com/expressjs/express) - used for server side rendering also serves the Webpack bundle 51 | * [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader) - TypeScript loader for Webpack 52 | * [css-loader](https://github.com/webpack-contrib/css-loader) - CSS loader for Webpack 53 | * [style-loader](https://github.com/webpack-contrib/css-loader) - load CSS from the Webpack bundle 54 | * [extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin) - create a separate CSS file from your Webpack config 55 | * [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) - allows fetch to be used server side 56 | * [serialize-javascript](https://github.com/yahoo/serialize-javascript) - allows to safely pass the redux state from the server to the client 57 | * [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) and [webpack-hot-middleware](https://github.com/glenjamin/webpack-hot-middleware) - hot module replacement and serve the bundle 58 | * [ts-node](https://github.com/TypeStrong/ts-node) - runs TypeScript code without compiling it in your file system 59 | * [tslint](https://github.com/palantir/tslint) and [tslint-react](https://github.com/palantir/tslint-react) - TypeScript code linter 60 | * [postcss-loader](https://github.com/postcss/postcss) and [autoprefixer](https://github.com/postcss/autoprefixer) - adds vendor prefixes to your CSS code 61 | * [react-helmet](https://github.com/nfl/react-helmet) - easily changes your head tag 62 | * [@types/](https://github.com/DefinitelyTyped/DefinitelyTyped) - type definitions for TypeScript 63 | * [cross-env](https://github.com/kentcdodds/cross-env) - allows to set NODE_ENV on all platforms 64 | 65 | And that's about it! 66 | 67 | ## Deep dive 68 | 69 | 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](https://github.com/erikras/ducks-modular-redux). The root reducer is located in `src/reducer.ts`. 70 | 71 | 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. 72 | 73 | *You will see a lot of links but they just point to the relevant code or the relevant documentation. Sorry in advance!* 74 | 75 | ### npm scripts and compiled JavaScript files 76 | 77 | `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. 78 | 79 | **In order to have everything in a single script without adding any extra package the [`scripts/build.ts`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/scripts/build.ts) compiles the Webpack bundle *and* the server application. I recommend that you remove the TypeScript compilation at [lines 16-39](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/scripts/build.ts#L16-L39) and compile it via [`tsc`](https://www.typescriptlang.org/docs/tutorial.html#compiling-your-code). Doing so will use `tsconfig.json` and make things more consistent.** 80 | 81 | ### Hot Module Replacement and Hot reloading 82 | 83 | [In your webpack config](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/webpack.config.ts#L31) 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`. 84 | 85 | [In `src/server.tsx`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L26-L37), when not in production we have `webpack-dev-middleware` and `webpack-hot-middleware` used by the *only* `express` application. They're reponsible of compiling, serving the webpack bundle and hot module replacement. 86 | 87 | Then finally in [`src/index.tsx`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/index.tsx#L35-L40) you will see: 88 | ```typescript 89 | if (module.hot) { 90 | module.hot.accept("./routes", () => { 91 | const App: any = require("./routes").default; 92 | render(App); 93 | }); 94 | } 95 | ``` 96 | This is what will be called whenever your webpack bundle is updated. This will re-render your application with the changes. 97 | 98 | ### CSS Loaders 99 | 100 | 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](https://en.wikipedia.org/wiki/Flash_of_unstyled_content). 101 | 102 | 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](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/webpack.config.ts#L66). 103 | 104 | ### Server side rendering with async data fetching 105 | 106 | In short this is how server side rendering is done: 107 | 1. Find matched Route components 108 | 2. Fetch data 109 | 3. Render the components as HTML code 110 | 4. Send the HTML code as the HTTP response 111 | 112 | It's very similar to what is explained in [`react-router`'s documentation](https://reacttraining.com/react-router/web/guides/server-rendering). 113 | 114 | #### Routes 115 | 116 | In the `routes.tsx` we have [a bunch of routes](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/routes.tsx#L7-L21) in an array as [plain JS objects](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config#route-configuration-shape). They will be used by `react-router-config`'s [`matchRoutes`](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config#matchroutesroutes-pathname) and [`renderRoutes`](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config#renderroutesroutes). 117 | 118 | Of course, we can still use the [Route component](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md) in our application they will be rendered server side but we won't be able to retrieve them for server side data fetching. 119 | 120 | #### Initializing the Redux store and async data fetching 121 | 122 | In `src/server.tsx`, [the final request handler](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L42-L102) is where server side rendering is done. 123 | 124 | We start by [creating a store](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L43) that will hold our application's data which we will eventually pass to the client. 125 | The [`setIsServerSide` call](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L48) allows us to notify the components that we are in a server side context. It just sets a boolean in the Redux store. 126 | 127 | We [get the matched routes](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L52) with `react-router-config`'s [`matchRoutes`](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config#matchroutesroutes-pathname). 128 | 129 | In each of our [components that require data to be fetched we have a `fetchData` static method](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/example/components/Main.tsx#L37-L39). These methods will dispatch the necessary data to the Redux store. 130 | 131 | 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. The `fetchData` parameters need to be the same across all components because they'll be [called in a loop](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L55-L60). 132 | 133 | This method must return a `Promise` so we can [wait for data that needs to be fetched asynchronously with `Promise.all`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L63). 134 | After async data fetching has been done we can finally render the application. 135 | 136 | 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. Notice that because of this we might not want to call some functions server side. In `src/example/components/Main.tsx` [we also fetch data in `componentWillMount`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/example/components/Main.tsx#L48-L50) (for the case where render client side) to prevent fetching twice I use [the boolean we have previously set](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L48). 137 | 138 | #### Rendering 139 | 140 | We can now render the application as a string with an up-to-date store. 141 | 142 | Since our routes (previously obtained with `matchRoutes`) are not real Route components but `react-router-config` routes, [`renderRoutes`](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config#renderroutesroutes) is needed to get the components. The components are wrapped in a [`StaticRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/StaticRouter.md) component to pass the request URL and get catch any redirections and then wrapped in a [`Provider`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#provider-store) to pass the application's state to our components. [The whole thing](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L67-L73) is passed as a parameter in a [`renderToString`](https://facebook.github.io/react/docs/react-dom-server.html#rendertostring) call. 143 | 144 | #### Redirections 145 | 146 | After rendering if a single [`Redirect`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Redirect.md) 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](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L76-L79). Otherwise we can send the rendered application to the client. 147 | 148 | #### Head tag and React-Helmet 149 | 150 | We have only rendered the application just like we would with a `ReactDOM.render` but this doesn't set the `` tag. In order to set the `` tag server side we need to use `react-helmet`. 151 | 152 | We start by calling [`renderStatic`](https://github.com/nfl/react-helmet#server-usage) before rendering [which will return an object](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L66). When rendering every Helmet usage will update this object. 153 | 154 | After rendering we can use the object that has updated head tags (such as ``, `<link>`, etc.) in the final HTML code. 155 | 156 | #### Final HTML code and sending it 157 | 158 | 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`](https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup) with it. 159 | 160 | [We fill the `<head>` tag with the help of the Helmet object](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L83-L89) and the [application's HTML in the root element](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L91). We add a [`polyfill.io`](https://qa.polyfill.io/v2/docs) script so we can use `fetch` client-side and [a script containing our Redux state](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L93-L96) that will be used when initializing the store client side. `serialize-javascript` is needed for safety purposes you can read more about it [here](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0#.4nbt3j38f). 161 | 162 | The [webpack generated scripts are also added](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L97) note that we don't add [the `hot.js` in production](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/server.tsx#L64-L65) since it is the bit relevant to HMR and hot reloading and we don't want that in production. 163 | 164 | Then finally in `/src/index.tsx`, we get our Redux state then [we initialize our store with it](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/index.tsx#L15-L16). 165 | 166 | ### tslint 167 | 168 | The `tslint.json` config is taken from [piotrwitek's React & Redux in TypeScript - Static Typing Guide](https://github.com/piotrwitek/react-redux-typescript-guide#tslintjson). 169 | 170 | ### Improvements 171 | 172 | I recommend that you [use `tsc` for building the server application](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#npm-scripts-and-compiled-javascript-files). 173 | 174 | You may also want to improve the production build by [separating the webpack dev server](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#separate-webpack-dev-server) and use an `src/index.tsx` file [without `react-hot-loader`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/master/src/index.tsx#L19). 175 | 176 | ## Extras 177 | 178 | I have put below some tips regarding universal React and/or TypeScript. 179 | 180 | ### Testing 181 | 182 | If you wish to write tests I recommend that you use [`ts-node`](https://github.com/TypeStrong/ts-node#mocha) with your test framework. 183 | 184 | #### Example with mocha 185 | 186 | * Example code: [https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/mocha](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/mocha) 187 | 188 | We can run [`mocha`](https://mochajs.org) tests with [`ts-node`](https://github.com/TypeStrong/ts-node#mocha) with this command: 189 | ``` 190 | mocha --compilers ts:ts-node/register,tsx:ts-node/register <files> 191 | ``` 192 | 193 | We can also use the [--fast fast option in `ts-node`](https://github.com/TypeStrong/ts-node#configuration-options) 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: 194 | ```js 195 | require("ts-node").register({ 196 | fast: true 197 | }); 198 | ``` 199 | You can find this file [at the root of the project](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/mocha/ts-node-register.js). 200 | 201 | You can find an example of an unit test in file a located at [`src/example/components/__tests__/About.test.tsx`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/mocha/src/example/components/__tests__/About.test.tsx). I added a [`test` script in `package.json`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/mocha/package.json#L12) with the **updated command to run mocha with the new register file (that I named `ts-node-register.js`)**: 202 | ``` 203 | mocha --compilers ts:./ts-node-register.js,tsx:./ts-node-register.js src/**/__tests__/*.ts* 204 | ``` 205 | 206 | In order to run the tests I need the required packages: 207 | ``` 208 | npm i -D mocha chai enzyme react-test-renderer @types/mocha @types/chai @types/enzyme 209 | ``` 210 | 211 | Then we can simply run the test script: 212 | ``` 213 | npm test 214 | ``` 215 | 216 | ### Separate webpack dev server 217 | 218 | * Example code: [https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/separate-dev-server](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/separate-dev-server) 219 | 220 | In the current setup the webpack dev server is located in [the server application](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/mocha/src/server.tsx#L26-L37). Although it is not run in production they are still required to be installed in order to build the application. And running the file will always trigger a Webpack compilation. 221 | 222 | You can create a separate webpack dev server and point the `<script>` tags to it. Let's say we want to server the webpack bundle from `http://localhost:3001`. First we're going to add [that to the script tags in `src/server.tsx`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/separate-dev-server/src/server.tsx#L45-L55). 223 | 224 | Then we're going to create the [new webpack dev server `src/devServer.ts`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/separate-dev-server/src/devServer.ts). In that file we have an express application listening on port **3001**. We're going to serve the webpack bundle with `webpack-dev-middleware` and `webpack-hot-middleware`. But if we only do so it's not going to work very well. So we'll have to change a few things: 225 | - We need to allow CORS because the webpack HMR is going to poll that server from another domain (`http://localhost:3000`). In order to do that we added [a `headers: { 'Access-Control-Allow-Origin': '*' }` to the webpack dev middleware options](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/separate-dev-server/src/devServer.ts#L26-L28). 226 | - With the current webpack configuration the HMR is going to poll on the same domain which is `http://localhost:3000` in our case but we want it to poll `http://localhost:3001` instead. In order to do that we modify our webpack config by [adding the URL to this server to the `webpack-hot-middleware` entry](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/separate-dev-server/src/devServer.ts#L18-L21). 227 | - And at last, since the application is going to request the bundle at `http://localhost:3001` (instead of `/`) we need to [adjust the `output.publicPath` accordingly](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/separate-dev-server/src/devServer.ts#L22). 228 | 229 | I added a [new npm script `start:webpack`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/separate-dev-server/package.json#L9) that starts this server. We can now start both `start:dev` and `start:webpack` and have the webpack bundle served from another server while retaining HMR and Hot reloading! 230 | 231 | ### Separate back-end / Proxying requests 232 | 233 | * Example code: [https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/proxy-requests](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/proxy-requests) 234 | 235 | #### Proxying requests 236 | 237 | If your application queries a server from another domain it might get cookies that will not be used when doing the first page request (since it wouldn't be on the same domain). In order to use the same domain we can use [`http-proxy`](https://github.com/nodejitsu/node-http-proxy) and pass the requests to another server. 238 | ``` 239 | npm i -S http-proxy 240 | ``` 241 | 242 | Unfortunately there is no type definitions available for it via `@types`. So I have used [type definitions](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/proxy-requests/src/http-proxy.d.ts) that I found [there](https://github.com/typed-contrib/node-http-proxy). 243 | 244 | I have setup [a simple express app located at `src/api.ts`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/proxy-requests/src/api.ts) that stores a string in memory (via session) and we can retrieve it on page load. That server listens to port **8000**. This is its API: 245 | * `GET /api`: retrieves the stored string 246 | * `POST /api/:string`: stores a string in session 247 | 248 | We also need to install `express-session`: 249 | ``` 250 | npm i -S express-session @types/express-session 251 | ``` 252 | 253 | [In `src/server.tsx`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/proxy-requests/src/server.tsx#L41-L46) we are going to create a proxy server with `http-proxy` that proxies every request that starts with `/api` to `http://localhost:8000`: 254 | ```ts 255 | const apiProxy: httpProxy = httpProxy.createProxyServer({ 256 | target: 'http://localhost:8000', 257 | }); 258 | app.all('/api*', (req: Request, res: Response) => { 259 | apiProxy.web(req, res); 260 | }); 261 | ``` 262 | 263 | #### Redux-Thunk and server side requests 264 | 265 | Now we can perform requests to `http://localhost:3000` and it will use the API server located at `http://localhost:8000` and set the cookies for `http://localhost:3000`. But let's not do that now. 266 | 267 | What happens when we perform the first page request? The request might send a nice cookie but the code still remain a simple `fetch` call. The server fetches data one the behalf of the server and does not pass the cookie which is not what we want. We need to find a way to pass the proper cookie depending on the context. 268 | 269 | An elegant solution would be to use [`redux-thunk` with the extra argument](https://github.com/gaearon/redux-thunk#injecting-a-custom-argument). In this extra argument we're going to set the cookie and we'll know by its presence if we're coming from a server side context. First let's install `redux-thunk`: 270 | ``` 271 | npm i -S redux-thunk @types/redux-thunk 272 | ``` 273 | 274 | Then add it to [our store factory](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/proxy-requests/src/store.ts#L9) with the extra argument: 275 | ```ts 276 | import reduxThunk from 'redux-thunk'; 277 | const configureStore: (initialState?: IReduxState, cookie?: { Cookie?: string }) => Store<IReduxState> = 278 | (initialState?: IReduxState, cookie: { Cookie?: string } = {}): Store<IReduxState> => { 279 | return createStore<IReduxState>(reducer, 280 | initialState as IReduxState, 281 | applyMiddleware(reduxThunk.withExtraArgument(cookie))); 282 | }; 283 | ``` 284 | 285 | And finally we can create [action creators](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/proxy-requests/src/example/modules/session.ts#L18-L49) like so: 286 | ```ts 287 | export function fetchValue(): ThunkAction<Promise<SetValue>, ISessionState, { Cookie?: string }> { 288 | return ( 289 | dispatch: Dispatch<ISessionState>, 290 | getState: () => ISessionState, 291 | extra: { Cookie?: string }) => fetch('http://localhost:3000/api', { 292 | credentials: 'include', 293 | headers: { 294 | ...extra, 295 | }, 296 | }) 297 | .then<{ value: string | undefined }>((response: Response) => response.json()) 298 | .then<SetValue>((result: { value: string | undefined }) => dispatch({ 299 | type: SET_VALUE, 300 | value: result.value, 301 | })); 302 | } 303 | ``` 304 | 305 | Now we just need to [pass the cookie](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/proxy-requests/src/server.tsx#L52) in the new store factory when we create the store in `src/server.tsx`: 306 | ```ts 307 | const store: Store<IReduxState> = createStore(undefined, req.get('Cookie')); 308 | ``` 309 | 310 | This is what happens in the first page request: 311 | 1. If the browser has a cookie for `http://localhost:3000` it will send it to the server. 312 | 2. The server initiates a new store with a `redux-thunk` extra argument containing the cookie. 313 | 3. When performing async data fetching the action creators will be called and we pass the cookie to the `fetch` options. 314 | 4. The async request is proxied to the API server at `http://localhost:8000` that receives the request with the cookie. 315 | 5. The API server responds with the correct data. 316 | 317 | After the first page request subsequent requests are done client side: 318 | 1. In the client side the store is not initialized with a cookie. 319 | 2. When performing an async request the cookie is not present. 320 | 3. `fetch` will use the browser's cookie. 321 | 322 | You can find all relevant code in: 323 | * `src/example/session.ts`: a Redux module with action creators, action types and reducer relevant to our application. 324 | * `src/example/components/Session.tsx`: a component that fetch the stored value and displays a form to change it. 325 | * `src/store.ts`: the new store factory using the `redux-thunk` middleware with an extra argument 326 | * `src/server.tsx`: uses the new store factory by passing the cookie and a request handler that uses a proxy server. 327 | * `src/api.ts`: an express application listening to port **8000**. 328 | 329 | You can now start both servers with `npm run start:dev` and `npm run start:api` and open `http://localhost:3000/session`. 330 | 331 | ### CSS Modules 332 | 333 | * Example code: [https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/css-modules](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/tree/css-modules) 334 | 335 | #### CSS Modules in TypeScript 336 | 337 | Let's say we would like to import styles from a CSS file named `styles.css`. The TypeScript compiler will throw an error because the `styles.css` module does not exist! The compiler is unaware of that technology so we must teach it... by simply adding a `.d.ts` file alongside it. 338 | 339 | In the example, I exported styles from the `src/example/global.css` files into separate CSS modules located at [`src/example/components/layout.css`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/src/example/components/layout.css) and [`src/example/components/user.css`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/src/example/components/user.css) with [theirs respectives `.d.ts` files](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/src/example/components/layout.css.d.ts). The `.d.ts` files just tell the compiler that those modules exist but it's still Webpack that is going to handle those CSS modules. 340 | 341 | We have a `webpack.config.ts` file with [an updated CSS loader options to enable CSS modules](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/webpack.config.ts#L63-L85). You can notice that I have [exported the class name pattern](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/webpack.config.ts#L15). It will be used to compute class names server side. 342 | 343 | #### CSS Modules server side 344 | 345 | In fact, webpack does not compile the server application so the CSS modules do not exist. Compiling the server application with Webpack would actually make it work (you need to set the `target` option to `node`). However you won't be able to use it with `ts-node` in development. In order to solve that problem we can use [`css-modules-require-hook`](https://github.com/css-modules/css-modules-require-hook) instead: 346 | ``` 347 | npm i -S css-modules-require-hook @types/css-modules-require-hook 348 | ``` 349 | 350 | I created [a `src/cssHook.ts` file](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/src/cssHook.ts) that uses it. 351 | ```ts 352 | hook({ 353 | prepend: postCssConfig.plugins, 354 | generateScopedName: cssModulePattern, 355 | rootDir: resolve(__dirname, '..'), 356 | }); 357 | ``` 358 | 359 | And [use it in `src/server.tsx`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/src/server.tsx#L20-L22): 360 | ```ts 361 | import hook from './cssHook'; 362 | hook(); 363 | import routeConfig from './routes'; 364 | ``` 365 | **Note that I use it before importing my routes so I CSS modules server side enabled before importing components that make use of CSS modules** 366 | 367 | We can find the class name pattern that I exported from the webpack config. The PostCSS plugins (that is only autoprefixer in this case). And a `rootDir`. 368 | We import the PostCSS plugins directly from the configuration that is a JavaScript file. We need to set `allowJs` compiler option to `true` in our `tsconfig.json` for it to work. The `rootDir` is necessary because we use the `outDir` compiler option in `tsconfig.json`. And `outDir` is necessary in our case because TypeScript would refuse to compile JavaScript at the same location as the source. But because we use `outDir` in our production environment the generated class names are going to be different (since the hash is based on the absolute path the class names will be different from those generated by Webpack) so we use `rootDir` to solve the issue. 369 | 370 | #### Compiling CSS modules in production 371 | 372 | When compiling TypeScript files into JavaScript the application will be located in `dist`. But TypeScript only compiles TypeScript files (crazy right?). So when we run the server application in production it is unable to create CSS class names (because the CSS files are not there!). In order to solve the issue we need to create a separate script that copies CSS files over the `dist` directory. I have used [`glob`](https://github.com/isaacs/node-glob) in order to retrieve CSS files and copy them. You can find [the script in `scripts/css.ts`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/scripts/css.ts) and I have also added an [npm script `build:css`](https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/blob/css-modules/package.json#L11). 373 | 374 | You can now build in production and run the application with CSS modules: 375 | ``` 376 | npm run build:prod 377 | npm run build:css 378 | npm run start:prod 379 | ``` 380 | 381 | ### Visual Studio Code (VSCode) debugging 382 | 383 | You can debug in the VSCode editor adding those configurations to your`.vscode/launch.json` file: 384 | ```json 385 | { 386 | "version": "0.2.0", 387 | "configurations": [ 388 | { 389 | "type": "node2", 390 | "request": "attach", 391 | "name": "Node", 392 | "address": "localhost", 393 | "port": 9229, 394 | "restart": true, 395 | "localRoot": "${workspaceRoot}" 396 | }, 397 | { 398 | "name": "Chrome", 399 | "type": "chrome", 400 | "request": "attach", 401 | "port": 9222, 402 | "url": "http://localhost:3000", 403 | "webRoot": "${workspaceRoot}" 404 | } 405 | ] 406 | } 407 | ``` 408 | 409 | Start the application with `ts-node --inspect <file>` and launch the `Node` VSCode debugger. It will automatically attach the debugger to your application instance. 410 | 411 | In order to debug the client application you need to install the [vscode-chrome-debug](https://github.com/Microsoft/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. 412 | 413 | ### Babel 414 | 415 | 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: 416 | 417 | Install Babel and friends: 418 | ``` 419 | npm i -S babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2 420 | ``` 421 | 422 | Then add it after the `awesome-typescript-loader` like so: 423 | ```typescript 424 | { 425 | test: /\.tsx?$/, 426 | use: ["babel-loader", "awesome-typescript-loader"], 427 | exclude: /node_modules/ 428 | } 429 | ``` 430 | 431 | You'll need to create a `.babelrc` file with this (more explanation [here](https://webpack.js.org/guides/hmr-react/)) 432 | ```json 433 | { 434 | "presets": [ 435 | [ 436 | "es2015", { "modules": false } 437 | ], 438 | "stage-2", 439 | "react" 440 | ], 441 | "plugins": [ 442 | "react-hot-loader/babel" 443 | ] 444 | } 445 | ``` 446 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-react-redux-typescript-starter-kit", 3 | "version": "1.0.0", 4 | "description": "A minimal starter kit with React, Redux, server side rendering with React-Router 4, hot reloading, and Webpack 2. 100% TypeScript.", 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=production node dist/src/server.js", 7 | "build": "cross-env NODE_ENV=production ts-node scripts/build.ts", 8 | "start:dev": "ts-node ./src/server.tsx", 9 | "postinstall": "cross-env NODE_ENV=production ts-node scripts/build.ts", 10 | "lint": "tslint --type-check -p tsconfig.json ./src/**/*.ts*" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit.git" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "webpack", 19 | "react", 20 | "react-router", 21 | "redux", 22 | "universal", 23 | "hot", 24 | "boilerplate", 25 | "starter", 26 | "kit", 27 | "minimal" 28 | ], 29 | "author": "François Nguyen (https://github.com/lith-light-g)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit/issues" 33 | }, 34 | "homepage": "https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit#readme", 35 | "dependencies": { 36 | "@types/express": "^4.0.35", 37 | "@types/express-serve-static-core": "^4.0.44", 38 | "@types/extract-text-webpack-plugin": "^2.0.1", 39 | "@types/isomorphic-fetch": "^0.0.34", 40 | "@types/node": "^7.0.18", 41 | "@types/react": "^15.0.24", 42 | "@types/react-dom": "^15.5.0", 43 | "@types/react-helmet": "^5.0.2", 44 | "@types/react-redux": "^4.4.40", 45 | "@types/react-router-config": "^1.0.1", 46 | "@types/react-router-dom": "^4.0.4", 47 | "@types/redux": "^3.6.0", 48 | "@types/serialize-javascript": "^1.3.1", 49 | "@types/webpack": "^2.2.15", 50 | "@types/webpack-dev-middleware": "^1.9.1", 51 | "@types/webpack-hot-middleware": "^2.15.0", 52 | "autoprefixer": "^6.7.7", 53 | "awesome-typescript-loader": "^3.1.3", 54 | "cross-env": "^4.0.0", 55 | "css-loader": "^0.28.1", 56 | "express": "^4.15.2", 57 | "extract-text-webpack-plugin": "^2.1.0", 58 | "isomorphic-fetch": "^2.2.1", 59 | "postcss-loader": "^1.3.3", 60 | "react": "^15.5.4", 61 | "react-dom": "^15.5.4", 62 | "react-helmet": "^5.0.3", 63 | "react-hot-loader": "^3.0.0-beta.7", 64 | "react-redux": "^5.0.4", 65 | "react-router-dom": "^4.1.1", 66 | "react-router-config": "^1.0.0-beta.3", 67 | "redux": "^3.6.0", 68 | "serialize-javascript": "^1.3.0", 69 | "ts-node": "^3.0.4", 70 | "typescript": "^2.3.2", 71 | "webpack": "^2.5.0", 72 | "webpack-dev-middleware": "^1.10.2", 73 | "webpack-hot-middleware": "^2.18.0" 74 | }, 75 | "devDependencies": { 76 | "style-loader": "^0.17.0", 77 | "tslint": "^5.2.0", 78 | "tslint-react": "^3.0.0" 79 | } 80 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("autoprefixer")({ 4 | browsers: "last 2 versions" 5 | }) 6 | ] 7 | }; -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | import * as webpack from 'webpack'; 3 | import webpackConfig from '../webpack.config'; 4 | import * as ts from 'typescript'; 5 | 6 | // Build webpack 7 | webpack(webpackConfig(process.env.NODE_ENV)).run((err: Error, stats: webpack.Stats) => { 8 | if (err) { 9 | throw err; 10 | } 11 | console.log(stats.toString({ 12 | colors: true, 13 | })); 14 | }); 15 | 16 | // Build server app 17 | const program = ts.createProgram(['./src/server.tsx'], { 18 | lib: ['lib.es6.d.ts'], 19 | jsx: ts.JsxEmit.React, 20 | noEmitOnError: true, 21 | strict: true, 22 | noUnusedLocals: true, 23 | sourceMap: true, 24 | outDir: './dist', 25 | target: ts.ScriptTarget.ES5, 26 | module: ts.ModuleKind.CommonJS, 27 | }); 28 | const emitResult = program.emit(); 29 | const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 30 | allDiagnostics.forEach(diagnostic => { 31 | const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 32 | const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 33 | console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); 34 | }); 35 | if (emitResult.emitSkipped) { 36 | throw new Error('Server compilation failed'); 37 | } else { 38 | console.log('Server successfully compiled'); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteConfig, renderRoutes } from 'react-router-config'; 3 | import { RouteComponentProps } from 'react-router-dom'; 4 | 5 | export interface ILayoutProps extends RouteComponentProps<void> { 6 | route?: RouteConfig; 7 | } 8 | 9 | export class Layout extends React.Component<ILayoutProps, {}> { 10 | constructor(props: ILayoutProps) { 11 | super(props); 12 | } 13 | render(): JSX.Element { 14 | return ( 15 | <div> 16 | {renderRoutes(this.props.route && this.props.route.routes)} 17 | </div> 18 | ); 19 | } 20 | } 21 | 22 | export default Layout; 23 | -------------------------------------------------------------------------------- /src/example/components/About.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | 4 | const projectURL = 'https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit'; 5 | 6 | export default (props: RouteComponentProps<void>) => ( 7 | <div> 8 | <h2>About</h2> 9 | <p>Find more about this starter kit on <a href={projectURL}>GitHub</a>.</p> 10 | </div> 11 | ); 12 | -------------------------------------------------------------------------------- /src/example/components/GitHubSearchLayout.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-line-length 2 | import * as React from 'react'; 3 | import Helmet from 'react-helmet'; 4 | import { Link, RouteComponentProps } from 'react-router-dom'; 5 | import { RouteConfig, renderRoutes } from 'react-router-config'; 6 | 7 | export interface IGitHubSearchParams { 8 | username?: string; 9 | } 10 | 11 | export interface IGitHubSearchProps extends RouteComponentProps<IGitHubSearchParams> { 12 | route?: RouteConfig; 13 | } 14 | 15 | export class GitHubSearchLayout extends React.Component<IGitHubSearchProps, {}> { 16 | constructor(props?: IGitHubSearchProps) { 17 | super(props); 18 | } 19 | render(): JSX.Element { 20 | return ( 21 | <div> 22 | <Helmet> 23 | <html lang="en" /> 24 | <title>Universal React Redux with TypeScript and Webpack 2 25 | 26 | 27 | 28 | 29 | 30 | Fork me on GitHub 36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 |

Universal React Redux starter kit with TypeScript and Webpack 2

78 |
79 |
80 | 86 |
87 | {renderRoutes(this.props.route && this.props.route.routes)} 88 |
89 | 90 | ); 91 | } 92 | } 93 | 94 | export default GitHubSearchLayout; 95 | -------------------------------------------------------------------------------- /src/example/components/GitHubUserPreview.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IGitHubUserData } from '../modules/user'; 3 | 4 | interface IGitHubUserPreviewProps { 5 | user: IGitHubUserData; 6 | error: string; 7 | } 8 | 9 | class GitHubUserPreview extends React.Component { 10 | constructor(props: IGitHubUserPreviewProps) { 11 | super(props); 12 | } 13 | render(): JSX.Element { 14 | const { user, error }: IGitHubUserPreviewProps = this.props; 15 | if (user) { 16 | const { avatar_url, login, name, bio, email }: IGitHubUserData = user; 17 | return ( 18 |
19 | {`${login}'s 20 |

{name} {login}

21 |

{email ? email : Email not shown}

22 |

{bio ? bio : Bio empty}

23 |
24 | ); 25 | } else { 26 | return ( 27 |
28 |

{error}

29 |
30 | ); 31 | } 32 | } 33 | } 34 | 35 | export default GitHubUserPreview; 36 | -------------------------------------------------------------------------------- /src/example/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IReduxState } from '../../reducer'; 3 | import { fetchUser, IGitHubUserData } from '../modules/user'; 4 | import { Dispatch } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import GitHubUserPreview from './GitHubUserPreview'; 7 | import { RouteComponentProps } from 'react-router-dom'; 8 | import { getError, getUser, isServerSide } from '../selectors'; 9 | 10 | // :) 11 | const me = 'lith-light-g'; 12 | 13 | export const mapStateToProps: (state: IReduxState, ownProps: IMainProps) => Partial = 14 | (state: IReduxState, ownProps: IMainProps): Partial => ({ 15 | user: getUser(state), 16 | error: getError(state), 17 | isServerSide: isServerSide(state), 18 | }); 19 | 20 | export const mapDispatchToProps: (dispatch: Dispatch, ownProps: IMainProps) => Partial = 21 | (dispatch: Dispatch, ownProps: IMainProps): Partial => ({ 22 | fetchUser: (username: string) => fetchUser(dispatch, username), 23 | }); 24 | 25 | export interface IMainParams { 26 | username: string; 27 | } 28 | 29 | export interface IMainProps extends RouteComponentProps { 30 | fetchUser: (username: string) => void; 31 | user: IGitHubUserData; 32 | error: string; 33 | isServerSide: boolean; 34 | } 35 | 36 | class MainComponent extends React.Component { 37 | static fetchData(dispatch: Dispatch, { username }: IMainParams): Promise { 38 | return fetchUser(dispatch, username || me); 39 | } 40 | private usernameInput: HTMLInputElement; 41 | constructor(props: IMainProps) { 42 | super(props); 43 | } 44 | componentWillMount(): void { 45 | const { isServerSide, fetchUser, match: { params: { username } } }: IMainProps = this.props; 46 | // this prevents the data to be fetched on page load by the client 47 | // if it was already loaded server side 48 | if (!isServerSide) { 49 | fetchUser(username || me); 50 | } 51 | } 52 | fetchUser: (event: React.FormEvent) => void = (event: React.FormEvent): void => { 53 | event.preventDefault(); 54 | this.props.fetchUser(this.usernameInput.value || me); 55 | } 56 | fetchMe: () => void = (): void => { 57 | this.props.fetchUser(me); 58 | } 59 | setInputRef: (input: HTMLInputElement) => void = (input: HTMLInputElement): void => { 60 | this.usernameInput = input; 61 | } 62 | render(): JSX.Element { 63 | return ( 64 |
65 | 66 |

Search user

67 |
68 |
69 | 74 | 77 | 80 |
81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | export default connect(mapStateToProps, mapDispatchToProps)(MainComponent); 88 | -------------------------------------------------------------------------------- /src/example/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 3 | font-size: 16px; 4 | } 5 | 6 | .logos * { 7 | margin: 0 0.5em; 8 | } 9 | 10 | .react { 11 | width: 10vw; 12 | } 13 | 14 | .redux { 15 | width: 10vw; 16 | } 17 | 18 | .typescript { 19 | width: 24vw; 20 | margin-bottom: 1em; 21 | } 22 | 23 | .webpack { 24 | width: 8vw; 25 | } 26 | 27 | .header { 28 | background: rgb(21, 39, 64); 29 | color: white; 30 | font-size: 1.5rem; 31 | text-align: center; 32 | padding: 1em; 33 | font-weight: bold; 34 | } 35 | 36 | .navbar { 37 | background: #DDD; 38 | text-align: center; 39 | } 40 | 41 | .navbar ul { 42 | color: black; 43 | list-style: none; 44 | margin: 0; 45 | } 46 | 47 | .navbar li { 48 | display: inline; 49 | } 50 | 51 | .navbar a { 52 | color: black; 53 | padding: 1em; 54 | text-decoration: none; 55 | display: inline-block; 56 | } 57 | 58 | .navbar a:hover { 59 | background-color: #555; 60 | color: #FFA700; 61 | } 62 | 63 | .container { 64 | max-width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .face { 69 | float: right; 70 | max-width: 100px; 71 | } 72 | 73 | .github-preview { 74 | border: 1px solid black; 75 | border-radius: 5px; 76 | padding: 2em; 77 | margin: 2em; 78 | } 79 | -------------------------------------------------------------------------------- /src/example/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, Action } from 'redux'; 2 | 3 | // default state 4 | export interface IGitHubUserState { 5 | user?: IGitHubUserData | undefined; 6 | error?: string | undefined; 7 | } 8 | const defaultState: IGitHubUserState = { 9 | }; 10 | 11 | // types 12 | type SetUserAction = { user: IGitHubUserData | undefined } & Action; 13 | type SetUserError = { error: string | undefined } & Action; 14 | export interface IGitHubUserData { 15 | avatar_url?: string; 16 | bio?: string; 17 | blog?: string; 18 | company?: string; 19 | created_at?: string; 20 | updated_at?: string; 21 | email?: string; 22 | location?: string; 23 | public_gists?: number; 24 | public_repos?: number; 25 | followers?: number; 26 | following?: number; 27 | login?: string; 28 | name?: string; 29 | } 30 | 31 | // actions 32 | const SET_USER_DATA = 'user/SET_USER_DATA'; 33 | const SET_USER_ERROR = 'user/SET_USER_ERROR'; 34 | 35 | // action creators 36 | export function fetchUser(dispatch: Dispatch, username: string): Promise { 37 | dispatch({ 38 | type: SET_USER_DATA, 39 | user: undefined, 40 | }); 41 | dispatch({ 42 | type: SET_USER_ERROR, 43 | error: undefined, 44 | }); 45 | return fetch(`https://api.github.com/users/${username}`) 46 | .then((response: Response) => { 47 | if (response.status >= 400) { 48 | return dispatch({ 49 | type: SET_USER_ERROR, 50 | error: response.status === 404 ? `User '${username}' could not be found` : 'An error occurred', 51 | }); 52 | } else { 53 | return response.json().then((user: IGitHubUserData) => dispatch({ 54 | type: SET_USER_DATA, 55 | user, 56 | })); 57 | } 58 | }); 59 | } 60 | 61 | // reducer 62 | const reducer: (state: IGitHubUserState, action: Action) => IGitHubUserState = 63 | (state: IGitHubUserState = defaultState, action: Action): IGitHubUserState => { 64 | switch (action.type) { 65 | case SET_USER_DATA: 66 | return { 67 | ...state, 68 | user: (action as SetUserAction).user, 69 | }; 70 | case SET_USER_ERROR: 71 | return { 72 | ...state, 73 | error: (action as SetUserError).error, 74 | }; 75 | default: 76 | return state; 77 | } 78 | }; 79 | 80 | export default reducer; 81 | -------------------------------------------------------------------------------- /src/example/selectors.ts: -------------------------------------------------------------------------------- 1 | import { IReduxState } from '../reducer'; 2 | import { IGitHubUserData } from './modules/user'; 3 | 4 | export const getUser: (state: IReduxState) => IGitHubUserData | undefined = 5 | (state: IReduxState): IGitHubUserData | undefined => state.user.user; 6 | 7 | export const getError: (state: IReduxState) => string | undefined = 8 | (state: IReduxState): string | undefined => state.user.error; 9 | 10 | export const isServerSide: (state: IReduxState) => boolean = 11 | (state: IReduxState): boolean => state.serverSide.isServerSide; 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Routes from './routes'; 5 | import { Provider } from 'react-redux'; 6 | import { Store } from 'redux'; 7 | import { IReduxState } from './reducer'; 8 | import createStore from './store'; 9 | import { setIsServerSide } from './modules/serverSide'; 10 | import { renderRoutes, RouteConfig } from 'react-router-config'; 11 | import { BrowserRouter } from 'react-router-dom'; 12 | import './example/global.css'; 13 | 14 | // this is defined with an up-to-date state if we come from a server side rendering context 15 | const store: Store = createStore(window.__REDUX_STATE__); 16 | 17 | const render: (routes: RouteConfig[]) => void = (routes: RouteConfig[]) => { 18 | ReactDOM.render( 19 | 20 | 21 | 22 | {renderRoutes(routes)} 23 | 24 | 25 | , 26 | document.getElementById('root'), 27 | ); 28 | }; 29 | render(Routes); 30 | 31 | // set rendered to false so newly mounted components can load 32 | setIsServerSide(store.dispatch, false); 33 | 34 | // hot reloading 35 | if (module.hot) { 36 | module.hot.accept('./routes', () => { 37 | const App: any = require('./routes').default; 38 | render(App); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/serverSide.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, Action } from 'redux'; 2 | 3 | // default state 4 | export interface IRenderingState { 5 | isServerSide: boolean; 6 | } 7 | const defaultState: IRenderingState = { 8 | isServerSide: false, 9 | }; 10 | 11 | // types 12 | type SetRenderedAction = { isServerSide: boolean } & Action; 13 | 14 | // actions 15 | const SET_RENDERED = 'rendering/SET_RENDERED'; 16 | 17 | // action creators 18 | export const setIsServerSide: (dispatch: Dispatch, isServerSide: boolean) => void = 19 | (dispatch: Dispatch, isServerSide: boolean): void => { 20 | dispatch({ 21 | type: SET_RENDERED, 22 | isServerSide, 23 | }); 24 | }; 25 | 26 | // reducer 27 | const reducer: (state: IRenderingState, action: Action) => IRenderingState = 28 | (state: IRenderingState = defaultState, action: Action): IRenderingState => { 29 | switch (action.type) { 30 | case SET_RENDERED: 31 | return { 32 | isServerSide: (action as SetRenderedAction).isServerSide, 33 | }; 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export default reducer; 40 | -------------------------------------------------------------------------------- /src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { IRenderingState, default as serverSide } from './modules/serverSide'; 3 | import { IGitHubUserState, default as user } from './example/modules/user'; 4 | 5 | export interface IReduxState { 6 | user: IGitHubUserState; 7 | serverSide: IRenderingState; 8 | } 9 | 10 | export default combineReducers({ 11 | user, 12 | serverSide, 13 | }); 14 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'react-router-config'; 2 | import Layout from './components/Layout'; 3 | import GitHubSearchLayout from './example/components/GitHubSearchLayout'; 4 | import Main from './example/components/Main'; 5 | import About from './example/components/About'; 6 | 7 | const routeConfig: RouteConfig[] = [ 8 | { 9 | component: Layout, 10 | routes: [{ 11 | component: GitHubSearchLayout, 12 | routes: [{ 13 | component: About, 14 | path: '/about', 15 | }, { 16 | component: Main, 17 | path: '/:username?', 18 | }], 19 | }], 20 | }, 21 | ]; 22 | 23 | export default routeConfig; 24 | -------------------------------------------------------------------------------- /src/server.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderToString, renderToStaticMarkup } from 'react-dom/server'; 3 | import * as express from 'express'; 4 | import * as webpackDevMiddleware from 'webpack-dev-middleware'; 5 | import * as webpackHotMiddleware from 'webpack-hot-middleware'; 6 | import * as webpack from 'webpack'; 7 | import { Compiler, Configuration } from 'webpack'; 8 | import webpackConfig from '../webpack.config'; 9 | import { Express, Request, Response } from 'express-serve-static-core'; 10 | import { StaticRouter } from 'react-router-dom'; 11 | import { matchRoutes, renderRoutes, MatchedRoute } from 'react-router-config'; 12 | import { Helmet, HelmetData } from 'react-helmet'; 13 | import routeConfig from './routes'; 14 | import { Provider } from 'react-redux'; 15 | import createStore from './store'; 16 | import { IReduxState } from './reducer'; 17 | import { Store, Dispatch } from 'redux'; 18 | import * as serialize from 'serialize-javascript'; 19 | import { setIsServerSide } from './modules/serverSide'; 20 | import 'isomorphic-fetch'; 21 | 22 | const app: Express = express(); 23 | const port = process.env.PORT || 3000; 24 | 25 | // hot module replacement 26 | if (process.env.NODE_ENV !== 'production') { 27 | const config: Configuration = webpackConfig(process.env.NODE_ENV); 28 | const compiler: Compiler = webpack(config); 29 | app.use(webpackDevMiddleware(compiler, { 30 | index: 'index.html', 31 | publicPath: (config.output as webpack.Output).publicPath as string, 32 | stats: { 33 | colors: true, 34 | }, 35 | })); 36 | app.use(webpackHotMiddleware(compiler)); 37 | } 38 | 39 | // needed to serve our application in production 40 | app.use(express.static('./dist/static')); 41 | 42 | app.get('*', (req: Request, res: Response) => { 43 | const store: Store = createStore(); 44 | const dispatch: Dispatch = store.dispatch; 45 | const context: { url?: string } = {}; 46 | 47 | // dispatch this action to prevent data from being fetched from componentWillMount 48 | setIsServerSide(dispatch, true); 49 | 50 | // fetch async data here 51 | let promises: Array> = []; 52 | const matchedRoutes: Array> = matchRoutes<{}>(routeConfig, req.originalUrl); 53 | for (const { route, match } of matchedRoutes) { 54 | const component: any = route.component; 55 | if (component && component.fetchData && typeof component.fetchData === 'function') { 56 | const promise: Promise = component.fetchData(dispatch, match.params); 57 | if (typeof promise.then === 'function') { 58 | promises = [...promises, promise]; 59 | } 60 | } 61 | } 62 | 63 | Promise.all(promises).then(() => { 64 | const scripts: string[] = process.env.NODE_ENV === 'production' ? 65 | ['common.js', 'vendor.js', 'main.js'] : ['common.js', 'vendor.js', 'hot.js', 'main.js']; 66 | const head: HelmetData = Helmet.renderStatic(); 67 | const reactAppElement: string = renderToString(( 68 | 69 | 70 | {renderRoutes(routeConfig)} 71 | 72 | 73 | )); 74 | 75 | // if redirect has been used 76 | if (context.url) { 77 | res.redirect(302, context.url); 78 | return; 79 | } 80 | 81 | res.send(`${renderToStaticMarkup(( 82 | 83 | 84 | {head.base.toComponent()} 85 | {head.title.toComponent()} 86 | {head.meta.toComponent()} 87 | {head.link.toComponent()} 88 | 89 | 90 | 91 |
92 |