Next.js is a framework that renders react applications on the server. This means when we create a normal react app (using create-react-app
for example) and load it to the browser, all the content is created by client-side javascript. Hence users or devices that don't have javascript will not be able to access your app.
You can see this in action, open a react application in the browser then view the page source. You will not see the content you've created! In fact, if the project was created through create-react-app
you'll see this:
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
Which is obvious as all we're doing with react is appending content to #root
(hence document.getElementById('root')
). Content that vanishes if javascript is disabled.
Hence Next.js, it fixes that issue for us.
Whilst learning next.js I am trying to create a small application that will have the basic functionality of instagram - a mini instagram clone if you like. I know that the code we'll write today will not be scalable, hence will not contribute that much to the react application but it will cover the basics of next.js
Let's get started
First we need to initialise npm
and install the only packages needed for next.js to work:
npm init
npm install --save next react react-dom
(If you use yarn
you know what to do)
Now we need to add the following scripts to the generated package.json
:
"scripts": {
"start": "next"
},
When we want to run this app in the browser we'll need to run npm start
.
Next.js allows us to organise our routes via pages. In this tutorial we'll create two pages to see how they can interact. Let's first create the index page. It needs to be at ./pages/index.js
. In there add the following:
export default () => (
<div className="app">
<header>
<h1>gallery</h1><h2>Original art</h2>
</header>
</div>
)
And that's it.
If we run npm start
we'll have the index page displayed. And of course, the most important thing here, if you view the page source, you'll now see the above HTML "hard coded" for us by next js.
Styling
CSS styles can go right into the pages/index.js
. Making each section modular. Let's make that header pretty:
export default () => (
<div className="app">
<header>
<h1><Link href={{ pathname: '/' }}><a>gallery</a></Link></h1>
</header>
<style jsx>{`
@import url('https://fonts.googleapis.com/css?family=Oleo+Script');
//... you can get the full code at github
header h1{
font-family: 'Oleo Script', cursive;
font-size: 25rem;
color: #AD0044;
text-shadow: #7F0D23 0px 0 40px;
letter-spacing: -20px;
display: inline-block;
}
h2{
font-size: 3.5rem;
color: white;
text-shadow: none;
letter-spacing: normal;
font-weight: normal;
font-family: 'Dancing Script', cursive;
position: absolute;
top: 50%;
left: 50%;
}
`}</style>
</div>
)
From the documentation "We bundle styled-jsx to provide support for isolated scoped CSS"
Adding styles this way feels like a hack! I like the external styles better, but it really does help with keeping everything isolated. I just need to get used to it - maybe.
Adding components
Let's just further split this code up so that it does not get messy by adding a layout component in ./components/Layout.js
import Link from 'next/link'
import Head from 'next/head'
export default ({ children, title = 'gallery: original art' }) => (
<div>
<Head>
<title>{ title }</title>
<meta charSet='utf-8' />
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
</Head>
<div className="app">
<header>
<h1><Link href={{ pathname: '/' }}>gallery</Link></h1>
<h2>Original art</h2>
</header>
{ children }
<footer>
Footer
</footer>
</div>
<style global jsx>{`
:root {
--green: #65C5D9;
--white: #F4F5F7;
--light-gray: #EAEEEF;
}
...
body{...}
`}</style>
<style jsx>{`
header{ ...}
header h1 a{ ...}
h2{....}
footer { ...}
`}</style>
</div>
)
Few important things to note above:
- Next.js gives us a
Head
component where by we can manipulate what goes inside the HTML<head>
tag. For example you might want more meta information. - We should use the
Link
component to navigate through the pages. It does require an anchor tag as well. I added an empty anchor tag but Next does populate it accordingly. - I used two style tags, one being
global
. The perfect example are the css variables and the body tag. Each page/component needs access to that information, henceglobal
.
Back in the ./pages/index.js
we proceed as such:
import Layout from '../components/Layout';
export default () => (
<Layout>
<p>this is the content </p>
</Layout>
)
Working with some data.
We could convert the Index
page component to a react component and get the data in the react component lifecycle or keep going with the stateless function approach. For this tutorial we're going to keep it simple and will not introduce react components (we'll do that in the next tutorial)
import Layout from '../components/Layout';
import getPhotos from '../data/data.js'
const Index = (props) => (
<Layout>
<p>{props.images[0].tagline}</p>
</Layout>
)
Index.getInitialProps = async ({ }) => {
// Would fetch data
return { images: getPhotos() } // return { images: [ { }, { } ] }
}
export default Index
getInitialProps
we would perform fetch operations and pass the fetched data to the Index
props, but we are hard coding it here just for convenience. We have the hard coded data at ./data/data.js
which looks like this:
export default () => {
return [
{
id: 0,
tagline: 'You\'re looking at me punk?',
image: '927756_283684128492129_838664181_n',
likes: 2,
comments: [
{
user: 'rex2018',
body: 'Hey this is dope! xxx'
}
]
},
...
]
}
All that data is available through the props. How we play with that data inside the Index
component is basic react stuff. First lets edit the Index
component
import Layout from '../components/Layout';
import getPhotos from '../data/data.js'
import Photo from '../components/Photo';
const Index = (props) => (
<Layout>
{
props.images.map((image, key) => <Photo id={key} id={key} data={image} />)
}
</Layout>
)
...
We loop through each image and render a Photo
component. We need to create that component at ./components/Photo.js
.
import CommentsFunctionality from './InteractiveButtons'
import Link from 'next/link'
export default (props) => {
return (
<div className="photoComponent">
<div style={{flex: '1 0 auto'}}>
<Link href={{ pathname: '/photo', query: { id: props.id } }}>
<img src={`/static/art/${props.data.image}.jpg`} alt=""/>
</Link>
<div className="meta">
<p className="tagline">{props.data.tagline}</p>
<CommentsFunctionality likes={props.data.likes} />
</div>
</div>
<style>{`
.photoComponent {
...
}
`}</style>
</div>
)
}
To recap, in ./pages/Index.js
we loop through all the images. For each image we render the above Photo
component. The new thing in there is that I decided to move a little bit of code into a separate InteractiveButtons
component. Initially I did so purely to keep the CSS of Photo
component clean. Anyway, heres the code for InteractiveButtons
import { MdModeComment, MdFavoriteBorder } from 'react-icons/md'
export default ({likes}) => (
<div className="meta">
<a href="/" className="heart"><MdFavoriteBorder />34</a>
<a href="/"><MdModeComment />{likes}</a>
<style>{`....`}</style>
</div>
)
I just found the react-icons
npm module whilst looking for a way to add icons without downloading any css framework, and I really like this. It's so easy to use. You can see all the icons provided here. Above I am requesting MdModeComment
and MdFavoriteBorder
from the Material Design icons (hence /md
)
Static Files
In the ./components/Photo.js
component we are trying to access the image file from a static
directory. All we need to do is to create that directory and add the image file, and Next.js takes care of the rest.
The end result so far:
Creating another page
We now need to be able to click on one of the above photos and see more information about them.
In the ./components/Photo.js
component we had this code
<Link href={{ pathname: '/photo', query: { id: props.id } }}>
<img src={`/static/art/${props.data.image}.jpg`} alt=""/>
</Link>
Which means, on click we go to /photo?id=0
. So the Link
component accepts a query where we can add our own query strings. In this case we simply get the id from the props and pass it on.
We then need to create ./pages/photo.js
page.
import Layout from '../components/Layout';
import Photo from '../components/Photo';
import CommentsFunctionality from '../components/InteractiveButtons'
import getPhotos from '../data/data.js'
const PhotoPage = (props) => (
<Layout>
<div className="container">
<div className="display_image">
<img src={`/static/art/${props.image.image}.jpg`} alt=''/>
<CommentsFunctionality />
</div>
<div className="comments">
<p className="tagline">{props.image.tagline}</p>
{
props.image.comments.map((comment, key) => <p key={key}><strong>{comment.user}:</strong>{comment.body}</p>)
}
<form className="comment-form" >
<input type="text"placeholder="Author" />
<input type="text" placeholder="comment..." />
<input type="submit" hidden />
</form>
</div>
</div>
<style>{` `}</style>
</Layout>
)
PhotoPage.getInitialProps = async ({query}) => {
// could fetch data here
let {id} = {...query}
let image = getPhotos().find(m => m.id == id)
return { image }
}
export default PhotoPage
The important functionality is inside getInitialProps
. Amongst other information getInitialProps
has access to the query string, hence we pass the as {query}
, from which we get the id that comes from the index page. Then we just find the image
object that has the same id and pass it as the component props.
That's the only reason I added the data in a separate file (at ./data/data.js
). The most basic way of showing how getInitialProps
could fetch data based on the query strings. The fetch url would look something like this fetch("http:.../" + id)
The single photo page is done:
Conclusion
As it can easily be seen this was not the best approach when it comes to handling data. We would really need to add react components and make use of react's component life cycle. Though I would argue even that would fall short compared to redux.
In the next tutorial, as I said, we'll use react components, fetch the exact same data from an API and get the likes and comments more dynamicly.
Meanwhile you can get all this code in github (branch 'part1')
Top comments (3)
if anyone has a problem installing this having cloned from GitHub, see the 4th answer here: stackoverflow.com/questions/470081...
ie downgrade your version of npm to 10.15, to install all the dependencies (this worked on macOS Catalina)
curl -o- raw.githubusercontent.com/creation... | bash
sudo npm install -g n
sudo n 10.15
npm install
npm audit fix
npm start
Would you happen to have a video tutorial of this? Because I got lost in the reading. Plus, I'm unable to run your project files from Github :/
Hi.
I don't have it on video, unfortunately.
I was about to just clone the repo, checkout part1 and
npm install
and thennpm start
and it just ran.