- Create-React-App with typescript
npx create-react-app my-app --template typescript
- Add files from this gist:
cd my-app
mkdir server
touch server.tsconfig.json webpack.server.js server/index.html server/server.tsx server/GenerateClient.ts server/hydrate.tsx
- Copy and paste code from this gist into the appropriate files
- modify
package.json
from the file in this gist and runyarn
ornpm install
- if you're using vscode, add the debug config
mkdir .vscode
touch .vscode/launch.json
- copy and paste .vscode/launch.json from this gist
- Run
yarn dev
ornpm run dev
(this will appear to crash the first time you run it b/c node tries to run the server file before it's been compiled. If you give it a few seconds it will sort itself out.)- alternatively,
yarn dev:build-server
andyarn dev:start
in separate consoles
- Debug (vscode)
- Debug Client
yarn start
- run debug task 'Debug Create React App'
- Debug Server
yarn dev:build-server
- run debug task 'Debug SSR Server'
- Debug Client rendered by Server
yarn dev
- run debug task 'Debug SSR Client'
- Debug Client
-
-
Save uxder/6ce6368438897c6d3d0924a826c08904 to your computer and use it in GitHub Desktop.
Typescript React + Express SSR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import serialize from 'serialize-javascript'; | |
import path from 'path'; | |
import fs from 'fs'; | |
import React from 'react'; | |
import ReactDOMServer from 'react-dom/server'; | |
export default <P>( | |
App: React.ReactElement<P>, | |
globalState: string | undefined, | |
): Promise<string> => new Promise((resolve, reject) => { | |
const app = ReactDOMServer.renderToString(App); | |
const indexFile = path.resolve('./server/index.html'); | |
fs.readFile(indexFile, 'utf8', (err, data) => { | |
if (err) { | |
reject(err); | |
} | |
const clientString = data | |
.replace( | |
'<div id="root"></div>', | |
`<div id="root">${app}</div>`, | |
) | |
.replace( | |
'<head>', | |
`<head><script>window.__INITIAL__DATA__ = ${serialize(globalState)}</script>`, | |
); | |
resolve(clientString); | |
}); | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server/hydrate.tsx | |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import '../src/index.css'; | |
import App from '../src/App'; | |
ReactDOM.hydrate( | |
<App />, | |
document.getElementById('root') | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- server/index.html --> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<link rel="icon" href="favicon.ico" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<meta name="theme-color" content="#000000" /> | |
<meta | |
name="description" | |
content="Web site created using create-react-app" | |
/> | |
<link rel="apple-touch-icon" href="logo192.png" /> | |
<link rel="manifest" href="manifest.json" /> | |
<title>React App</title> | |
</head> | |
<body> | |
<noscript>You need to enable JavaScript to run this app.</noscript> | |
<div id="root"></div> | |
<script src="/server-build/hydrate.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
// Use IntelliSense to learn about possible attributes. | |
// Hover to view descriptions of existing attributes. | |
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | |
"version": "0.2.0", | |
"configurations": [ | |
{ | |
"type": "chrome", | |
"request": "launch", | |
"name": "Debug Create React App", | |
"url": "http://localhost:3000", | |
"webRoot": "${workspaceFolder}" | |
}, | |
{ | |
"type": "chrome", | |
"request": "launch", | |
"name": "Debug SSR Client", | |
"url": "http://localhost:3006", | |
"webRoot": "${workspaceFolder}", | |
"sourceMaps": true | |
}, | |
{ | |
"name": "Debug SSR Server", | |
"type": "node", | |
"request": "launch", | |
"cwd": "${workspaceRoot}", | |
"program": "${workspaceRoot}/server/server.tsx", | |
"outFiles": [ | |
"${workspaceRoot}/server-build/**/*.js" | |
], | |
"sourceMaps": true | |
} | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
... | |
"dependencies": { | |
... | |
"@types/express": "^4.17.6", | |
"@types/serialize-javascript": "^1.5.0", | |
"css-loader": "^3.6.0", | |
"express": "^4.17.1", | |
"file-loader": "^6.0.0", | |
"nodemon": "^2.0.4", | |
"npm-run-all": "^4.1.5", | |
"serialize-javascript": "^3.1.0", | |
"style-loader": "^1.2.1", | |
"ts-loader": "^7.0.5", | |
"webpack-cli": "^3.3.11", | |
"webpack-node-externals": "^1.7.2" | |
}, | |
"scripts": { | |
... | |
"dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development -w", | |
"dev:start": "nodemon --exec 'node' ./server-build/server.js", | |
"dev": "npm-run-all --parallel dev:*" | |
}, | |
... | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"compilerOptions": { | |
"target": "es5", | |
"lib": [ | |
"dom", | |
"dom.iterable", | |
"esnext" | |
], | |
"allowJs": true, | |
"skipLibCheck": true, | |
"esModuleInterop": true, | |
"allowSyntheticDefaultImports": true, | |
"strict": true, | |
"forceConsistentCasingInFileNames": true, | |
"module": "esnext", | |
"moduleResolution": "node", | |
"resolveJsonModule": true, | |
"isolatedModules": true, | |
"jsx": "react", | |
"outDir": "./dist/", | |
"noEmit": false, | |
"sourceMap": true | |
}, | |
"include": [ | |
"server", | |
"src/react-app-env.d.ts" | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server/server.tsx | |
import express from 'express'; | |
import React from 'react'; | |
import App from '../src/App'; | |
import generateClient from './GenerateClient'; | |
const PORT = process.env.PORT || 3006; | |
const app = express(); | |
app.use(express.static('./public', { | |
index: false, | |
})); | |
app.use('/server-build', express.static('./server-build')); | |
app.get('*', async (req, res) => { | |
try { | |
const clientString = await generateClient( | |
<App />, | |
undefined | |
) | |
return res.send(clientString); | |
} catch (untypedErr) { | |
const err: NodeJS.ErrnoException = untypedErr; | |
console.error('Something went wrong:', err); | |
return res.status(500).send('Oops, better luck next time!'); | |
} | |
}); | |
app.listen(PORT, () => { | |
console.log(`😎 Server is listening on port ${PORT}`); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const path = require('path'); | |
const nodeExternals = require('webpack-node-externals'); | |
const serverConfig = { | |
entry: './server/server.tsx', | |
target: 'node', | |
externals: [nodeExternals()], | |
output: { | |
path: path.resolve('server-build'), | |
filename: 'server.js', | |
// Bundle absolute resource paths in the source-map, | |
// so VSCode can match the source file. | |
devtoolModuleFilenameTemplate: '[absolute-resource-path]' | |
}, | |
module: { | |
rules: [ | |
{ | |
test: /\.tsx?$/, | |
loader: 'ts-loader', | |
exclude: /node_modules/, | |
options: { | |
allowTsInNodeModules: true, | |
configFile: 'server.tsconfig.json' | |
}, | |
}, | |
{ | |
test: /\.css$/i, | |
use: ['css-loader'], | |
}, | |
{ | |
test: /\.(jpg|jpeg|png|svg|gif)$/, | |
use: [{ | |
loader: 'file-loader', | |
options: { | |
name: '[md5:hash:hex].[ext]', | |
publicPath: '/server-build/img', | |
outputPath: 'img', | |
} | |
}] | |
} | |
] | |
}, | |
devtool: 'source-map', | |
resolve: { | |
extensions: [ '.tsx', '.ts', '.js', '.css', '.svg', '.png' ], | |
}, | |
}; | |
const clientConfig = { | |
entry: './server/hydrate.tsx', | |
target: 'web', | |
output: { | |
path: path.resolve('server-build'), | |
filename: 'hydrate.js', | |
// Bundle absolute resource paths in the source-map, | |
// so VSCode can match the source file. | |
devtoolModuleFilenameTemplate: '[absolute-resource-path]' | |
}, | |
module: { | |
rules: [ | |
{ | |
test: /\.(ts|tsx)?$/, | |
loader: 'ts-loader', | |
exclude: /node_modules/, | |
options: { | |
allowTsInNodeModules: true, | |
configFile: 'server.tsconfig.json' | |
}, | |
}, | |
{ | |
test: /\.css$/i, | |
use: ['style-loader', 'css-loader'], | |
}, | |
{ | |
test: /\.(jpg|jpeg|png|svg|gif)$/, | |
use: [{ | |
loader: 'file-loader', | |
options: { | |
name: '[md5:hash:hex].[ext]', | |
publicPath: '/server-build/img', | |
outputPath: 'img', | |
} | |
}] | |
} | |
] | |
}, | |
devtool: 'source-map', | |
resolve: { | |
extensions: [ '.tsx', '.ts', '.js', '.css', '.svg', '.png' ], | |
}, | |
}; | |
module.exports = [serverConfig, clientConfig]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment