Skip to content

Commit

Permalink
init covfefe
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredpalmer committed Aug 27, 2017
0 parents commit 897f7af
Show file tree
Hide file tree
Showing 19 changed files with 6,346 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
logs
*.log
npm-debug.log*
.DS_Store

coverage
node_modules
build
.env.local
.env.development.local
.env.test.local
.env.production.local
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SSR Experiments with React Router 4

## Install

Clone the repo and install deps...

```
git clone ...
cd ssr-demo
yarn install & yarn start
```

## What is going on here?

This little app demonstrates some cool SSR stuff you can do with React Router 4:

- Next.js-like data fetching using an HoC, static route config, and react-router-config.
- "Client-only" routes...this translates to partial/selective SSR (because Routes are just components :wink:)
- Using RR4's `statusContext` to set HTTP status codes isomorphically.
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "my-razzle-app",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"start": "razzle start",
"build": "razzle build",
"test": "razzle test --env=jsdom",
"start:prod": "NODE_ENV=production node build/server.js"
},
"dependencies": {
"express": "^4.15.4",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-router-dom": "^4.2.2"
},
"devDependencies": {
"razzle": "^0.7.6-rc2"
}
}
Binary file added public/favicon.ico
Binary file not shown.
2 changes: 2 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *

38 changes: 38 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
*,
*:before,
*:after {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}

body {
font-family: system-ui, 'San Francisco', -apple-system, BlinkMacSystemFont,
'.SFNSText-Regular', 'Helvetica Neue', Helvetica, sans-serif;
color: #222;
font-size: 16px;
background-color: #fff;
margin: 0;
padding: 0;
font-feature-settings: "liga", "kern";
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}

.App {
margin: 1rem;
}

a {
color: #0af;
text-decoration: none;
-webkit-user-select: text;
}

a:hover {
cursor: pointer;
}
46 changes: 46 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import './App.css';

import NavLink from 'react-router-dom/NavLink';
import React from 'react';
import Route from 'react-router-dom/Route';
import Switch from 'react-router-dom/Switch';

const App = ({ routes, initialData }) => {
return routes
? <div className="App">
<nav>
{routes.map((route, index) =>
<NavLink
style={{ marginRight: '1rem', color: '#0af' }}
activeStyle={{ fontWeight: 800, color: '#000' }}
key={`nav-${index}`}
exact={index === 0}
to={route.path}
>
{route.name}
</NavLink>
)}
</nav>
<Switch>
{routes.map((route, index) => {
// pass in the initialData from the server or window.DATA for this
// specific route
return (
<Route
key={index}
path={route.path}
exact={route.exact}
render={props =>
React.createElement(route.component, {
...props,
initialData: initialData[index] || null,
})}
/>
);
})}
</Switch>
</div>
: null;
};

export default App;
18 changes: 18 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import App from './App';
import BrowserRouter from 'react-router-dom/BrowserRouter';
import React from 'react';
import { render } from 'react-dom';
import routes from './routes';

const data = window._INITIAL_DATA_;

render(
<BrowserRouter>
<App routes={routes} initialData={data} />
</BrowserRouter>,
document.getElementById('root')
);

if (module.hot) {
module.hot.accept();
}
24 changes: 24 additions & 0 deletions src/components/HttpStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import Route from 'react-router-dom/Route';
function HttpStatus(props) {
return (
<Route
render={({ staticContext }) => {
// we have to check if staticContext exists
// because it will be undefined if rendered through a BrowserRouter
if (staticContext) {
staticContext.statusCode = props.statusCode;
staticContext.url = props.url;
}
// @todo in Fiber, remove <div>
return (
<div>
{props.children}
</div>
);
}}
/>
);
}

export default HttpStatus;
15 changes: 15 additions & 0 deletions src/components/NotFound.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import HttpStatus from './HttpStatus';
import React from 'react';
import Route from 'react-router-dom/Route';

function NotFound() {
return (
<HttpStatus statusCode={404}>
<div>
<h1>404. Not Found.</h1>
</div>
</HttpStatus>
);
}

export default NotFound;
86 changes: 86 additions & 0 deletions src/components/withSSR.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';

// This is a Higher Order Component that abstracts duplicated data fetching
// on the server and client.
export default function SSR(Page) {
class SSR extends React.Component {
static getInitialData(ctx) {
// Need to call the wrapped components getInitialData if it exists
return Page.getInitialData
? Page.getInitialData(ctx)
: Promise.resolve(null);
}

constructor(props) {
super(props);
this.state = {
data: props.initialData,
isLoading: false,
};
this.ignoreLastFetch = false;
}

componentDidMount() {
if (!this.state.data) {
this.fetchData();
}
}

componentWillUnmount() {
this.ignoreLastFetch = true;
}

fetchData = () => {
// if this.state.data is null, that means that the we are on the client.
// To get the data we need, we just call getInitialData again on mount.
if (!this.ignoreLastFetch) {
console.log('refetching');
this.setState({ isLoading: true });
this.constructor.getInitialData({ match: this.props.match }).then(
data => {
this.setState({ data, isLoading: false });
},
error => {
this.setState(state => ({
data: { error },
isLoading: false,
}));
}
);
}
};

render() {
// Flatten out all the props.
const { initialData, ...rest } = this.props;

// if we wanted to create an app-wide error component,
// we could also do that here using <HTTPStatus />. However, it is
// more flexible to leave this up to the Routes themselves.
//
// if (rest.error && rest.error.code) {
// <HttpStatus statusCode={rest.error.code || 500}>
// {/* cool error screen based on status code */}
// </HttpStatus>
// }

return (
<Page
{...rest}
refetch={this.fetchData}
isLoading={this.state.isLoading}
{...this.state.data}
/>
);
}
}

SSR.displayName = `SSR(${getDisplayName(Page)})`;
return SSR;
}

// This make debugging easier. Components will show as SSR(MyComponent) in
// react-dev-tools.
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
20 changes: 20 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import app from './server';
import http from 'http';

const server = http.createServer(app);

let currentApp = app;

server.listen(process.env.PORT || 3000);

if (module.hot) {
console.log('✅ Server-side HMR Enabled!');

module.hot.accept('./server', () => {
console.log('🔁 HMR Reloading `./server`...');
server.removeListener('request', currentApp);
const newApp = require('./server').default;
server.on('request', newApp);
currentApp = newApp;
});
}
30 changes: 30 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import About from './screens/About';
import Home from './screens/Home';
import Users from './screens/Users';

// This is a static route configuration. It should include all of your top level
// routes, regardless of whether they are going to server render. In fact, you
// can totally point multiple routes to the same component! This is great for
// when you only need to server render a handful of routes and not your entire
// application!
const routes = [
{
path: '/',
component: Home,
name: 'Home',
exact: true,
},
{
path: '/about',
component: About,
name: 'About',
exact: true,
},
{
path: '/users',
component: Users,
name: 'Users',
},
];

export default routes;
41 changes: 41 additions & 0 deletions src/screens/About.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import withSSR from '../components/withSSR';

class About extends React.Component {
// This works similarly to Next.js's `getInitialProps`
static getInitialData({ match, req, res }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
article: `
This text is ALSO server rendered if and only if it's the initial render.
`,
currentRoute: match.pathname,
});
}, 500);
});
}

render() {
const { isLoading, article, error } = this.props;
return (
<div>
<h1>About</h1>
{isLoading && <div>Loading...</div>}
{error &&
<div>
{JSON.stringify(error, null, 2)}
</div>}
{article &&
<div>
{article}
<div style={{ marginTop: '1rem', color: '#aaa' }}>
{'>> '}Go to another route (Users)
</div>
</div>}
</div>
);
}
}

export default withSSR(About);
Loading

0 comments on commit 897f7af

Please sign in to comment.