├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── browser │ └── index.js ├── server │ └── index.js └── shared │ ├── App.js │ ├── ColorfulBorder.js │ ├── Grid.js │ ├── Home.js │ ├── Navbar.js │ ├── NoMatch.js │ ├── api.js │ ├── routes.js │ └── styles.css └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | public -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ui.dev Logo 6 | 7 |
8 |

9 | 10 |

Server Rendering with React Router

11 | 12 | ### Info 13 | 14 | This is the code repo for [ui.dev](https://ui.dev)'s [Server Rendering with React Router](http://ui.dev/react-router-server-rendering) post. 15 | 16 | For the hosted project, visit __[rrssr.ui.dev](https://rrssr.ui.dev/)__. 17 | 18 | For more information on the full course, visit __[ui.dev/react-router-v6](https://ui.dev/react-router/)__. 19 | 20 | *** 21 | 22 | ### Other Versions 23 | 24 | - [Server Rendering with React Router v4](https://github.com/uidotdev/rrssr-v4) 25 | - [Server Rendering with React Router v5](https://github.com/uidotdev/react-router-v5-server-rendering) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-server-rendering", 3 | "description": "Example for server rendering with React Router.", 4 | "scripts": { 5 | "build": "webpack", 6 | "start": "node dist/server.js", 7 | "dev": "webpack && node dist/server.js" 8 | }, 9 | "babel": { 10 | "presets": [ 11 | "@babel/preset-env", 12 | "@babel/preset-react" 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-proposal-object-rest-spread" 16 | ] 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.14.6", 20 | "@babel/plugin-proposal-object-rest-spread": "^7.14.7", 21 | "@babel/preset-env": "^7.14.7", 22 | "@babel/preset-react": "^7.14.5", 23 | "babel-loader": "^8.2.2", 24 | "css-loader": "^5.2.6", 25 | "mini-css-extract-plugin": "^2.0.0", 26 | "webpack": "^5.42.0", 27 | "webpack-cli": "^4.7.2", 28 | "webpack-node-externals": "^3.0.0" 29 | }, 30 | "dependencies": { 31 | "cors": "^2.8.5", 32 | "express": "^4.17.1", 33 | "history": "^5.0.0", 34 | "isomorphic-fetch": "^3.0.0", 35 | "react": "^17.0.2", 36 | "react-dom": "^17.0.2", 37 | "react-router-dom": "^6.0.0-beta.0", 38 | "serialize-javascript": "^6.0.0" 39 | }, 40 | "version": "1.0.0", 41 | "main": "index.js", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/uidotdev/react-router-server-rendering.git" 45 | }, 46 | "author": "Tyler McGinnis", 47 | "license": "MIT", 48 | "homepage": "https://github.com/uidotdev/react-router-server-rendering#readme" 49 | } 50 | -------------------------------------------------------------------------------- /src/browser/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from '../shared/App' 4 | import { BrowserRouter } from 'react-router-dom' 5 | 6 | ReactDOM.hydrate( 7 | 8 | 9 | , 10 | document.getElementById('app') 11 | ); -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import cors from 'cors' 3 | import * as React from 'react' 4 | import ReactDOM from 'react-dom/server' 5 | import { matchPath } from 'react-router-dom' 6 | import { StaticRouter } from 'react-router-dom/server'; 7 | import serialize from 'serialize-javascript' 8 | import App from '../shared/App' 9 | import routes from '../shared/routes' 10 | 11 | const app = express() 12 | 13 | app.use(cors()) 14 | app.use(express.static('dist')) 15 | 16 | app.get('*', (req, res, next) => { 17 | const activeRoute = routes.find((route) => matchPath(route.path, req.url)) || {} 18 | 19 | const promise = activeRoute.fetchInitialData 20 | ? activeRoute.fetchInitialData(req.path) 21 | : Promise.resolve() 22 | 23 | promise.then((data) => { 24 | const markup = ReactDOM.renderToString( 25 | 26 | 27 | 28 | ) 29 | 30 | res.send(` 31 | 32 | 33 | 34 | SSR with React Router 35 | 36 | 37 | 38 | 39 | 40 | 41 |
${markup}
42 | 43 | 44 | `) 45 | }).catch(next) 46 | }) 47 | 48 | const PORT = process.env.PORT || 3000 49 | 50 | app.listen(PORT, () => { 51 | console.log(`Server is listening on port: ${PORT}`) 52 | }) -------------------------------------------------------------------------------- /src/shared/App.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import routes from './routes' 3 | import { Route, Routes } from 'react-router-dom' 4 | import Navbar from './Navbar' 5 | import NoMatch from './NoMatch' 6 | import ColorfulBorder from './ColorfulBorder' 7 | import './styles.css' 8 | 9 | export default function App ({ serverData=null }) { 10 | return ( 11 | 12 | 13 |
14 | 15 | 16 | 17 | {routes.map(({ path, fetchInitialData, component: C }) => ( 18 | } 22 | /> 23 | ))} 24 | } /> 25 | 26 |
27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/shared/ColorfulBorder.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function ColorfulBorder() { 4 | return ( 5 |
6 | ) 7 | } -------------------------------------------------------------------------------- /src/shared/Grid.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useParams } from 'react-router-dom' 3 | 4 | export default function Grid ({ fetchInitialData, data }) { 5 | const [repos, setRepos] = React.useState(() => { 6 | return __isBrowser__ 7 | ? window.__INITIAL_DATA__ 8 | : data 9 | }) 10 | 11 | const [loading, setLoading] = React.useState( 12 | repos ? false : true 13 | ) 14 | 15 | const fetchNewRepos = React.useRef( 16 | repos ? false : true 17 | ) 18 | 19 | const { id } = useParams() 20 | 21 | React.useEffect(() => { 22 | if (fetchNewRepos.current === true) { 23 | setLoading(true) 24 | 25 | fetchInitialData(id) 26 | .then((repos) => { 27 | setRepos(repos) 28 | setLoading(false) 29 | }) 30 | } else { 31 | fetchNewRepos.current = true 32 | } 33 | }, [id, fetchNewRepos]) 34 | 35 | if (loading === true) { 36 | return 🤹‍♂️ 37 | } 38 | 39 | return ( 40 | 50 | ) 51 | } -------------------------------------------------------------------------------- /src/shared/Home.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function Home () { 4 | return

Select a Language

5 | } -------------------------------------------------------------------------------- /src/shared/Navbar.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { NavLink } from 'react-router-dom' 3 | 4 | const languages = [ 5 | { 6 | name: 'All', 7 | param: 'all' 8 | }, 9 | { 10 | name: 'JavaScript', 11 | param: 'javascript', 12 | }, 13 | { 14 | name: 'Ruby', 15 | param: 'ruby', 16 | }, 17 | { 18 | name: 'Python', 19 | param: 'python', 20 | }, 21 | { 22 | name: 'Java', 23 | param: 'java', 24 | } 25 | ] 26 | 27 | export default function Navbar () { 28 | return ( 29 | 38 | ) 39 | } -------------------------------------------------------------------------------- /src/shared/NoMatch.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function NoMatch () { 4 | return

Four Oh Four

5 | } -------------------------------------------------------------------------------- /src/shared/api.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | export function fetchPopularRepos (language = 'all') { 4 | const encodedURI = encodeURI(`https://api.github.com/search/repositories?q=stars:>1+language:${language}&sort=stars&order=desc&type=Repositories`) 5 | 6 | return fetch(encodedURI) 7 | .then((data) => data.json()) 8 | .then((repos) => repos.items) 9 | .catch((error) => { 10 | console.warn(error) 11 | return null 12 | }); 13 | } -------------------------------------------------------------------------------- /src/shared/routes.js: -------------------------------------------------------------------------------- 1 | import Home from './Home' 2 | import Grid from './Grid' 3 | import { fetchPopularRepos } from './api' 4 | 5 | const routes = [ 6 | { 7 | path: '/', 8 | component: Home, 9 | }, 10 | { 11 | path: '/popular/:id', 12 | component: Grid, 13 | fetchInitialData: (path = '') => fetchPopularRepos(path.split('/').pop()) 14 | } 15 | ] 16 | 17 | export default routes -------------------------------------------------------------------------------- /src/shared/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://ui.dev/font"); 2 | 3 | :root { 4 | --black: #000; 5 | --white: #fff; 6 | --red: #f32827; 7 | --purple: #a42ce9; 8 | --blue: #2d7fea; 9 | --yellow: #f4f73e; 10 | --pink: #eb30c1; 11 | --gold: #ffd500; 12 | --aqua: #2febd2; 13 | --gray: #282c35; 14 | } 15 | 16 | *, 17 | *:before, 18 | *:after { 19 | box-sizing: inherit; 20 | } 21 | 22 | html { 23 | font-family: proxima-nova, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif; 24 | text-rendering: optimizeLegibility; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | box-sizing: border-box; 28 | font-size: 18px; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | padding: 0; 34 | min-height: 100vh; 35 | background: var(--black); 36 | color: var(--white); 37 | } 38 | 39 | .container { 40 | margin: 0 auto; 41 | max-width: 1100px; 42 | padding: 50px; 43 | } 44 | 45 | .border-container { 46 | padding: 0; 47 | margin: 0; 48 | display: flex; 49 | background: var(--yellow); 50 | height: 12px; 51 | width: 100vw; 52 | } 53 | 54 | a { 55 | color: var(--yellow); 56 | font-weight: 600; 57 | } 58 | 59 | li { 60 | list-style-type: none; 61 | } 62 | 63 | .nav { 64 | display: flex; 65 | justify-content: space-around; 66 | padding-left: 0; 67 | } 68 | 69 | .nav a { 70 | font-weight: 900; 71 | font-size: 25px; 72 | text-decoration: none; 73 | } 74 | 75 | .heading-center { 76 | margin: 50px; 77 | text-align: center; 78 | } 79 | 80 | .grid { 81 | padding-left: 0; 82 | display: flex; 83 | flex-wrap: wrap; 84 | justify-content: space-around; 85 | } 86 | 87 | .grid li { 88 | margin: 80px 50px; 89 | padding: 40px; 90 | } 91 | 92 | .grid li:nth-child(3n+1) { 93 | border-top: 8px solid var(--purple); 94 | } 95 | 96 | .grid li:nth-child(3n+2) { 97 | border-left: 8px solid var(--red); 98 | } 99 | 100 | .grid li:nth-child(3n+3) { 101 | border-bottom: 8px solid var(--blue); 102 | } 103 | 104 | .grid h2 { 105 | font-size: 40px; 106 | margin-bottom: 10px; 107 | } 108 | 109 | .grid h3 { 110 | font-size: 30px; 111 | margin: 0; 112 | } 113 | 114 | .grid p { 115 | margin-top: 0; 116 | } 117 | 118 | .loading { 119 | text-align: center; 120 | animation: spin 2s infinite linear; 121 | font-size: 40px; 122 | display: block; 123 | margin: 20px auto; 124 | font-style: normal; 125 | } 126 | 127 | @keyframes spin { 128 | to { 129 | transform: rotate(360deg); 130 | } 131 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const nodeExternals = require('webpack-node-externals') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | 6 | const browserConfig = { 7 | mode: "production", 8 | entry: './src/browser/index.js', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'bundle.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { test: /\.(js)$/, use: 'babel-loader' }, 16 | { test: /\.css$/, use: [ 'css-loader' ]} 17 | ] 18 | }, 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | __isBrowser__: "true" 22 | }) 23 | ] 24 | } 25 | 26 | const serverConfig = { 27 | mode: "production", 28 | entry: './src/server/index.js', 29 | target: 'node', 30 | externals: [nodeExternals()], 31 | output: { 32 | path: path.resolve(__dirname, 'dist'), 33 | filename: 'server.js' 34 | }, 35 | module: { 36 | rules: [ 37 | { test: /\.(js)$/, use: 'babel-loader' }, 38 | { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] } 39 | ] 40 | }, 41 | plugins: [ 42 | new MiniCssExtractPlugin(), 43 | new webpack.DefinePlugin({ 44 | __isBrowser__: "false" 45 | }), 46 | ] 47 | } 48 | 49 | module.exports = [browserConfig, serverConfig] --------------------------------------------------------------------------------