- create-react-app
- GatsbyJS
- Next.js
Today I want to try out Next.js (Github). More specifically the new Next4 release that includes React16 and React Router 4 - as I already gathered some positive experiences with those. There is also an update to Styled JSX 2, that now allows us to prop-up our styles, to avoid having to overwrite defaults inline inside components - here the documented example:
export default ({ color }) => (
<div>
Hello there <span>my friend</span>
<style jsx>{`
/* this style only applies to the span within lexical scope */
span { color: ${color}; }
`}</style>
</div>
)
There are many different examples available to start building your App. All righty, lets go then!
TOC
First create a directory for our app and npm init -y it - then install Next.js and React:
npm install next@latest react@latest react-dom@latest --save
We can add the following npm scripts to the package.json file to make things easier:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
After that, the file-system is the main API. Every .js file becomes a route that gets automatically processed and rendered.
Populate ./pages/index.js inside your project:
export default () => <div>Welcome to next.js!</div>
and then just run npm run dev and go to http://localhost:3000. To use another port, you can run npm run dev -- -p .
So far, we get:
- Automatic transpilation and bundling (with webpack and babel)
- Hot code reloading
- Server rendering and indexing of ./pages
- Static file serving. ./static/ is mapped to /static/
- Automatic code splitting - Every import you declare gets bundled and served with each page. That means pages never load unnecessary code!
Next.js 4 bundles styled-jsx 2 to provide support for isolated scoped CSS - the documentation can be found here. A few examples are:
isolated scoped CSS
export default () => (
<div>
<p>only this paragraph will get the style :)</p>
{ /* you can include <Component />s here that include
other <p>s that don't get unexpected styles! */ }
<style jsx>{`
p {
color: red;
}
`}</style>
</div>
)
Styles can be defined in separate JavaScript modules by tagging with css any template literal that contain CSS. css must be imported from styled-jsx/css:
Keeping CSS in separate files
/* styles.js */
import css from 'styled-jsx/css'
export const button = css`button { color: purple; }`
export default css`div { color: teal; }`
and imported as regular strings:
import styles, { button } from './styles'
export default () => (
<div>
<button>styled-jsx</button>
<style jsx>{styles}</style>
<style jsx>{button}</style>
</div>
)
in this example all "potential" <div>'s inside the components JSX - as well as the enclosing root <div> is targeted by {styles}. If you just want to target the root:
Targeting The Root
export default () => (
<div className="root">
<style jsx>{`
.root {
color: green;
}
`}</style>
</div>
)
To skip scoping entirely, you can make the global-ness of your styles explicit by adding global:
Global styles
export default () => (
<div>
<style jsx global>{`
body {
background: red
}
`}</style>
</div>
)
You can also use modifiers for your CSS classes to create components that can be called in different "versions", e.g. <Button>Hi</Button> or <Button large>Big</Button>.
className toggling
const Button = (props) => (
<button className={ 'large' in props && 'large' }>
{ props.children }
<style jsx>{`
button {
padding: 20px;
background: #eee;
color: #999
}
.large {
padding: 50px
}
`}</style>
</button>
)
You can override the CSS you configure via inline-styles:
inline style
const Button = ({ padding, children }) => (
<button style={{ padding }}>
{ children }
<style jsx>{`
button {
padding: 20px;
background: #eee;
color: #999
}
`}</style>
</button>
)
In this example, the padding defaults to the one set in <style> (20), but the user can pass a custom one via <Button padding={30}>.
It is possible to use constants like so:
Constants
import { colors, spacing } from '../theme'
import { invertColor } from '../theme/utils'
const Button = ({ children }) => (
<button>
{ children }
<style jsx>{`
button {
padding: ${ spacing.medium };
background: ${ colors.primary };
color: ${ invertColor(colors.primary) };
}
`}</style>
</button>
)
or to pass them down by Props:
Dynamic styles
const Button = (props) => (
<button>
{ props.children }
<style jsx>{`
button {
padding: ${ 'large' in props ? '50' : '20' }px;
background: ${props.theme.background};
color: #999;
display: inline-block;
font-size: 1em;
}
`}</style>
</button>
)
./pages/index.js
import styles, { hipsterum, imagefloat } from './styles/hipsterum'
export default () =>
<div>
<h1>Welcome to next.js!</h1>
<h3>The awesome World of Server-side Rendering</h3>
<h5>lets see what this is all about</h5>
<img src="/static/test.png" className="imagefloat"/>
<p className="hipsterum">
[ipsum...]
</p>
<style jsx>{`
h1, h5 {
color: white;
}
@media (max-width: 600px) {
h1, h5 {
color: black;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
@media (max-width: 600px) {
body {
background-image: url("/static/test.png");
}
}
`}</style>
<style jsx>{styles}</style>
<style jsx>{imagefloat}</style>
<style jsx>{hipsterum}</style>
</div>
./pages/styles/hipsterum.js
import css from 'styled-jsx/css'
export const hipsterum = css`.hipsterum {
color: blue;
text-align: justify;
}`
export const imagefloat = css`.imagefloat {
float:right;
margin-left:10px;
}`
export default css`h3 { color: red; }`
This leads you to an style abomination that will make you cringe :) but uses all the important features of Style-JSX:
- @media queries for responsive designs
- usage of static assets for your designs from the ./static folder
- inline css and separated css styles in their own files
- separation of styles into their own functions to allow you to import only necessary styles
Trying to figure out the best way to add Bootstrap to the Mix - for now I will just add the CDN links to the layout Component in ./components/layout. This works just like the template component with React-Helmet in create-react-app:
import Link from 'next/link'
import Head from 'next/head'
import NavBar from './navbar'
export default ({ children, title = 'This is the default title' }) => (
<div>
<Head>
<title>{ title }</title>
<meta charSet='utf-8' />
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossOrigin="anonymous" />
</Head>
<header>
<NavBar />
</header>
<div className="container">
<div className="row">
<br/><br/><br/><br/>
{ children }
</div>
</div>
<footer>
{'I`m a Footer Component'}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossOrigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossOrigin="anonymous" />
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossOrigin="anonymous" />
</footer>
</div>
)
As a first bootstrap component, I added the Bootstrap 4 Navbar we created earlier and imported it into the header area above:
import Link from 'next/link'
const NavBar = () => (
<div>
<nav className="navbar navbar-expand-md navbar-dark bg-dark fixed-top mb">
<Link href="/"><a className="nav-item nav-link"><img src="/static/logo.svg" alt="INSTAR Wiki" /></a></Link>
<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#TopNavbar" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="TopNavbar">
<ul className="navbar-nav mr-auto">
<li className="nav-item">
<Link href="/page-1"><a className="nav-item nav-link">Page 1</a></Link>
</li>
<li className="nav-item">
<Link href="/page-2"><a className="nav-item nav-link">Page 2</a></Link>
</li>
<li className="nav-item">
<Link href="/nested-routes"><a className="nav-item nav-link">Nested Routes</a></Link>
</li>
</ul>
</div>
</nav>
</div>
)
export default NavBar
As we already see, Next.js builds routes for each component it finds inside the ./pages directory. So our index component shows up at the root URL, without us having to do anything - sweet. But what if we need nested routes for our components - say page01 is a child of chapter01 and we want it to appear when we type in localhost:3000/chapter01/page01 ? There are two ways that I found so far:
- next-routes: a neat little npm module from also featured as an Example @Zeit.
- Using an Express.js webserver as featured @Zeit and @Medium
Lets try out next-routes for this example:
npm install next-routes --save [...]
Ok, this basically wrecked the whole application
I will copy the code to ./next-routes-wtf and - maybe - try it again later... The result is very inconsistent. You can click on a link and the page loads just fine. If you click on the same link again, or just reload the page, or copy it's URL into another browser, you are very likely to end up seeing the 404 page.
Update It might just have been the way I linked components - the solution, that is coming up below, showed a similar behaviour, when you forget the "as=" attribute in a link tag.
Ok - so lets try Express.js now, since I wanted to use it for deployment anyhow. Brandon Richey says, that he ran into the same problems I had with next-routes, when using the official documentation for the custom server.js setup. So I will stay away from it for now and try his version.
First install Express.js from npm:
npm install --save express
then create ./server.js:
const express = require('express');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
// CUSTOM ROUTES GO HERE
server.get('/Products/:slug', (req, res) => {
const mergedQuery = Object.assign({}, req.query, req.params);
return app.render(req, res, '/Products', mergedQuery);
});
// THIS IS THE DEFAULT ROUTE, DON'T EDIT THIS
server.get('*', (req, res) => {
return handle(req, res);
});
const port = process.env.PORT || 3000;
server.listen(port, err => {
if (err) throw err;
console.log(`> Ready on port ${port}...`);
});
});
This will give you a param that gets sent to your blog.js component inside of your pages/ directory and give you the custom routing that you want! The client-side linking, assuming we have the route setup above /products/:slug, your links to specific slugs would need to be structure using next/link’s Link component via the following:
<Link href={`/base?slug=${slug}`} as={`/base/${slug}`} prefetch>
...
</Link>
e.g.
<Link href={`/Products?slug=${'Outdoor_Cameras'}`} as={`/Products/${'Outdoor_Cameras'}`} prefetch>
as is what the user will see in their browser, but href is what next.js will interpret to figure out how things need to get routed. Both of these steps are required to make the link behavior and routing behavior behave the same no matter where the page is rendered from!
Now create a Product component in ./pages/Products.js:
import React from 'react'
import Layout from '../components/layout'
const posts = [
{ slug: 'Indoor_Cameras', title: 'Indoor Cameras' },
{ slug: 'Outdoor_Cameras', title: 'Outdoor Cameras' }
]
export default class extends React.Component {
static async getInitialProps ({ query, res }) {
const post = posts.find(post => post.slug === query.slug)
if (!post && res) {
res.statusCode = 404
}
return { post }
}
render () {
const { post } = this.props
if (!post) return <Layout><h1>Products</h1></Layout>
return <Layout><h1>{post.title}</h1></Layout>
}
}
This will load the corresponding posts when you add the right slugs for it - /Products/Outdoor_Cameras or /Products/Indoor_Cameras or defaults to whatever you add here: if (!post) return <Layout><h1>Products</h1></Layout> in case that no match is found.
Finally, you’ll need to modify your package.json file to include everything so that next.js knows how to run the server.js file:
"scripts": {
"build": "next build",
"start": "node server.js",
}
Now you can continue to run your dev server with npm run dev and you can build/start your production server as well!