├── .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 |
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 |
30 | {languages.map(({ name, param }) => (
31 | -
32 |
33 | {name}
34 |
35 |
36 | ))}
37 |
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]
--------------------------------------------------------------------------------