- Section 11: Integrating a Server-Side-Rendered React App
- Table of Contents
- Starting the React App
- Reminder on Server Side Rendering
- Basics of Next JS
- Building a Next Image
- Running Next in Kubernetes
- Note on File Change Detection
- Adding Global CSS
- Adding a Sign Up Form
- Handling Email and Password Inputs
- Successful Account Signup
- Handling Validation Errors
- The useRequest Hook
- Using the useRequest Hook
- An onSuccess Callback
- Overview on Server Side Rendering
- Fetching Data During SSR
- Why the Error?
- Two Possible Solutions
- Cross Namespace Service Communication
- When is GetInitialProps Called?
- On the Server or the Browser
- Specifying the Host
- Passing Through the Cookies
- A Reusable API Client
- Content on the Landing Page
- The Sign In Form
- A Reusable Header
- Moving GetInitialProps
- Issues with Custom App GetInitialProps
- Handling Multiple GetInitialProps
- Passing Props Through
- Building the Header
- Conditionally Showing Links
- Signing Out
Client Side Rendering
Server Side Rendering
- install react, react-dom, next
- create pages folder and add page components
- npm run dev
FROM node:alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
docker build -t chesterheng/client .
docker push chesterheng/client
# client-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: client-depl
spec:
replicas: 1
selector:
matchLabels:
app: client
template:
metadata:
labels:
app: client
spec:
containers:
- name: client
image: chesterheng/client
---
apiVersion: v1
kind: Service
metadata:
name: client-srv
spec:
selector:
app: client
ports:
- name: client
protocol: TCP
port: 3000
targetPort: 3000
# skaffold.yaml
apiVersion: skaffold/v2alpha3
kind: Config
deploy:
kubectl:
manifests:
- ./infra/k8s/*
build:
local:
push: false
artifacts:
- image: chesterheng/auth
context: auth
docker:
dockerfile: Dockerfile
sync:
manual:
- src: 'src/**/*.ts'
dest: .
- image: chesterheng/client
context: client
docker:
dockerfile: Dockerfile
sync:
manual:
- src: '**/*.js'
dest: .
# ingress-srv.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-service
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/use-regex: 'true'
spec:
rules:
- host: ticketing.dev
http:
paths:
- path: /api/users/?(.*)
backend:
serviceName: auth-srv
servicePort: 3000
- path: /?(.*)
backend:
serviceName: client-srv
servicePort: 3000
skaffold dev
- Goto chrome - https://ticketing.dev/
- Type 'thisisunsafe'
// next.config.js
module.exports = {
webpackDevMiddleware: config => {
config.watchOptons.poll = 300;
return config;
}
};
kubectl get pods
kubectl delete pod client-depl-b955695bf-8ws8j
kubectl get pods
Global CSS Must Be in Your Custom
// _app.js
import 'bootstrap/dist/css/bootstrap.css';
export default ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
// signup.js
export default () => {
return (
<form>
<h1>Sign Up</h1>
<div className="form-group">
<label>Email Address</label>
<input className="form-control" />
</div>
<div className="form-group">
<label>Password</label>
<input type="password" className="form-control" />
</div>
<button className="btn btn-primary">Sign Up</button>
</form>
);
};
// signup.js
import { useState } from 'react';
export default () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const onSubmit = event => {
event.preventDefault();
console.log(email, password);
};
return (
<form onSubmit={onSubmit}>
<h1>Sign Up</h1>
<div className="form-group">
<label>Email Address</label>
<input
value={email}
onChange={e => setEmail(e.target.value)}
className="form-control"
/>
</div>
<div className="form-group">
<label>Password</label>
<input
value={password}
onChange={e => setPassword(e.target.value)}
type="password"
className="form-control"
/>
</div>
<button className="btn btn-primary">Sign Up</button>
</form>
);
};
// signup.js
const onSubmit = async event => {
event.preventDefault();
const response = await axios.post('/api/users/signup', {
email,
password
});
console.log(response.data);
};
// signup.js
const [errors, setErrors] = useState([]);
const onSubmit = async event => {
event.preventDefault();
try {
const response = await axios.post('/api/users/signup', {
email,
password
});
console.log(response.data);
} catch (err) {
setErrors(err.response.data.errors);
}
};
// use-request.js
import axios from 'axios';
import { useState } from 'react';
export default ({ url, method, body }) => {
const [errors, setErrors] = useState(null);
const doRequest = async () => {
try {
setErrors(null);
const response = await axios[method](url, body);
return response.data;
} catch (err) {
setErrors(
<div className="alert alert-danger">
<h4>Ooops....</h4>
<ul className="my-0">
{err.response.data.errors.map(err => (
<li key={err.message}>{err.message}</li>
))}
</ul>
</div>
);
}
};
return { doRequest, errors };
};
// signup.js
const { doRequest, errors } = useRequest({
url: '/api/users/signup',
method: 'post',
body: {
email, password
}
})
const onSubmit = async event => {
event.preventDefault();
doRequest();
};
// use-request.js
import axios from 'axios';
import { useState } from 'react';
export default ({ url, method, body, onSuccess }) => {
const [errors, setErrors] = useState(null);
const doRequest = async () => {
try {
setErrors(null);
const response = await axios[method](url, body);
if (onSuccess) {
onSuccess(response.data);
}
return response.data;
} catch (err) {
setErrors(
<div className="alert alert-danger">
<h4>Ooops....</h4>
<ul className="my-0">
{err.response.data.errors.map(err => (
<li key={err.message}>{err.message}</li>
))}
</ul>
</div>
);
}
};
return { doRequest, errors };
};
// signup.js
const { doRequest, errors } = useRequest({
url: '/api/users/signup',
method: 'post',
body: {
email, password
},
onSuccess: () => Router.push('/')
})
const onSubmit = async event => {
event.preventDefault();
await doRequest();
};
// index.js
const LandingPage = ({ color }) => {
console.log('I am in the component', color);
return <h1>Landing Page</h1>;
};
LandingPage.getInitialProps = () => {
console.log('I am on the server!');
return { color: 'red' };
};
export default LandingPage;
LandingPage.getInitialProps = async () => {
const response = await axios.get('/api/users/currentuser');
return response.data;
};
const LandingPage = ({ currentUser }) => {
console.log(currentUser);
axios.get('/api/users/currentuser');
return <h1>Landing Page</h1>;
};
LandingPage.getInitialProps = async () => {
const response = await axios.get('/api/users/currentuser');
return response.data;
};
Request Option #1 is selected
Request Option #1 is selected
kubectl get services -n ingress-nginx
kubectl get namespace
- service.namespace.svc.cluster.local
- http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
Optional
Request from a component | Request from getInitialProps |
---|---|
Always issued from the browser, so use a domain of '' | Might be executed from the client or the server! Need to figure out what our env is so we can use the correct domain |
LandingPage.getInitialProps = async () => {
if(typeof window === 'undefined') {
// we are on the server!
// requests should be made to http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
} else {
// we are on the browser!
// requests should be made with a base url of ''
}
return {};
};
kubectl get services -n ingress-nginx
kubectl get namespace
- service.namespace.svc.cluster.local
- http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
LandingPage.getInitialProps = async () => {
if(typeof window === 'undefined') {
// we are on the server!
// requests should be made to http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
const { data } = await axios.get(
'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local/api/users/currentuser',
{
headers: {
Host: 'ticketing.dev'
}
}
);
return data;
} else {
// we are on the browser!
// requests should be made with a base url of ''
const { data } = await axios.get('/api/users/currentuser');
return data;
}
return {};
};
LandingPage.getInitialProps = async ({ req }) => {
if(typeof window === 'undefined') {
// we are on the server!
// requests should be made to http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
const { data } = await axios.get(
'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local/api/users/currentuser',
{
headers: req.headers
}
);
return data;
} else { ... }
return {};
};
// build-client.js
import axios from 'axios';
export default ({ req }) => {
if(typeof window === 'undefined') {
// we are on the server
return axios.create({
baseURL: 'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local',
headers: req.headers
});
} else {
// we are on the browser
return axios.create({
baseURL: ''
});
}
};
// index.js
LandingPage.getInitialProps = async (context) => {
const client = buildClient(context);
const { data } = await client.get('/api/users/currentuser');
return data;
};
const LandingPage = ({ currentUser }) => {
return currentUser ? (
<h1>You are signed in</h1>
) : (
<h1>You are NOT signed in</h1>
);
};
// signin.js
import { useState, useEffect } from 'react';
import Router from 'next/router';
import useRequest from '../../hooks/use-request';
export default () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { doRequest, errors } = useRequest({
url: '/api/users/signin',
method: 'post',
body: {
email,
password
},
onSuccess: () => Router.push('/')
});
const onSubmit = async event => {
event.preventDefault();
await doRequest();
};
return (
<form onSubmit={onSubmit}>
<h1>Sign In</h1>
<div className="form-group">
<label>Email Address</label>
<input
value={email}
onChange={e => setEmail(e.target.value)}
className="form-control"
/>
</div>
<div className="form-group">
<label>Password</label>
<input
value={password}
onChange={e => setPassword(e.target.value)}
type="password"
className="form-control"
/>
</div>
{errors}
<button className="btn btn-primary">Sign In</button>
</form>
);
};
// _app.js
import 'bootstrap/dist/css/bootstrap.css';
export default ({ Component, pageProps }) => {
return (
<div>
<h1>Header!</h1>
<Component {...pageProps} />
</div>
);
};
// _app.js
import 'bootstrap/dist/css/bootstrap.css';
import buildClient from '../api/build-client';
const AppComponent = ({ Component, pageProps }) => {
return (
<div>
<h1>Header!</h1>
<Component {...pageProps} />
</div>
);
};
AppComponent.getInitialProps = () => {};
export default AppComponent;
- LandingPage.getInitialProps is not invoked the moment we add AppComponent.getInitialProps
AppComponent.getInitialProps = async appContext => {
const client = buildClient(appContext.ctx);
const { data } = await client.get('/api/users/currentuser');
return data;
};
AppComponent.getInitialProps = async appContext => {
const client = buildClient(appContext.ctx);
const { data } = await client.get('/api/users/currentuser');
let pageProps = {};
if(appContext.Component.getInitialProps) {
pageProps = await appContext.Component.getInitialProps(appContext.ctx);
}
console.log(pageProps);
return data;
};
import 'bootstrap/dist/css/bootstrap.css';
import buildClient from '../api/build-client';
const AppComponent = ({ Component, pageProps, currentUser }) => {
return (
<div>
<h1>Header! {currentUser.email} </h1>
<Component {...pageProps} />
</div>
);
};
AppComponent.getInitialProps = async appContext => {
const client = buildClient(appContext.ctx);
const { data } = await client.get('/api/users/currentuser');
let pageProps = {};
if(appContext.Component.getInitialProps) {
pageProps = await appContext.Component.getInitialProps(appContext.ctx);
}
return {
pageProps,
...data
};
};
export default AppComponent;
// header.js
import Link from 'next/link';
export default ({ currentUser }) => {
return (
<nav className="navbar navbar-light bg-light">
<Link href="/">
<a className="navbar-brand">GitTix</a>
</Link>
<div className="d-flex justify-content-end">
<ul className="nav d-flex align-items-center">
{currentUser ? 'Sign out' : 'Sign in/up'}
</ul>
</div>
</nav>
);
};
import Link from 'next/link';
export default ({ currentUser }) => {
const links = [
!currentUser && { label: 'Sign Up', href: '/auth/signup' },
!currentUser && { label: 'Sign In', href: '/auth/signin' },
currentUser && { label: 'Sign Out', href: '/auth/signout' }
]
.filter(linkConfig => linkConfig)
.map(({ label, href }) => {
return (
<li key={href} className="nav-item">
<Link href={href}>
<a className="nav-link">{label}</a>
</Link>
</li>
);
});
return (
<nav className="navbar navbar-light bg-light">
<Link href="/">
<a className="navbar-brand">GitTix</a>
</Link>
<div className="d-flex justify-content-end">
<ul className="nav d-flex align-items-center">{links}</ul>
</div>
</nav>
);
};
import { useEffect } from 'react';
import Router from 'next/router';
import useRequest from '../../hooks/use-request';
export default () => {
const { doRequest } = useRequest({
url: '/api/users/signout',
method: 'post',
body: {},
onSuccess: () => Router.push('/')
});
useEffect(() => {
doRequest();
}, []);
return <div>Signing you out...</div>;
};