Following from the last tutorial
Today we'll cover
- Fetching data from an API.
- Performing CRUD operations onfetch API.
In future tutorials I might cover how we could actually incorporate express within next.js and create our own API, but for now I am going to use an NPM package called json-server. It's a fantastic tool to quickly run a local REST API. As it states in the documentation "a full fake REST API with zero coding in less than 30 seconds". As this is not a tutorial for json-server
I am not going to say much just that I've added a JSON file with all our data at ./data/db.json
and then ran json-server data/db.json --port 4000
, giving us a REST API at port 4000 (read the documentation if you want to learn more on json-server). Leave that running in a separate terminal window and let's get on.
I am going to assume that you've read the last tutorial and if you are following along you have already cloned the files we have so far (github - part one).
Fetching data from API
In the last tutorial we used stateless functional components. This time we'll need to convert some of those components to class based, starting with ./pages/index.js
.
const Index = (props) => ( <Layout> ... </Layout>)
Index.getInitialProps = async ({ }) => { ...}
export default Index
That's what we have from the previous tutorial. Let's convert that into a class component and fetch data from the API
import React, { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/Layout';
import Photo from '../components/Photo';
export default class extends Component {
static async getInitialProps() {
const res = await fetch('http://localhost:4000/photos')
const images = await res.json()
return { images }
}
componentWillMount() {
this.setState({
images: this.props.images
})
}
render() {
return (
<Layout>
{
this.state.images.map((image, key) => <Photo id={key} id={key} data={image} />)
}
</Layout>
)
}
}
As you can see, the core code is the same. We just wrapped what we had into a react class component.
When using next.js we do not have access to the browser's fetch API, hence we need to install the isomorphic-unfetch
library (npm i -S isomorphic-unfetch
) which implements the same functionality as the browser's fetch.
Just as previously, getInitialProps
pushes the fetched data into the props. We then inject that data into the component state, via componentWillMount
. Basic react stuff - we need access to the data via the state rather than props so that we'll be able to propagate data changes through the app.
Functionality to like a photo
Thinking back to the previous tutorial, we have a likes button that shows the number of likes. We of course want the ability to increase the likes upon button click
As usual in react development we are going to create the functionality in the Index page component (./pages/index.js
) and pass it down as a prop (this is why learning redux is great in these cases)
...
LikesEntry(id){
let images = this.state.images
let image = images.find( i => i.id === id )
image.likes = parseInt(image.likes) + 1
this.setState({
images
})
fetch(`http://localhost:4000/photos/${id}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(image)
})
}
render(){
return (
<Layout>
{
this.state.images.map((image, key) =>
<Photo
LikesEntry={this.LikesEntry.bind(this)}
id={image.id}
key={key} data={image} /> )
}
</Layout>
)
}
...
When the likes button is clicked, LikesEntry
get's triggered. In there we increase the number of likes to the image that's being pressed. That change is pushed back to the state, then we perform a put request where the server also gets the changes.
The button that's being pressed is actually two components deep. Above we pass LikesEntry
to the ./components/Photo.js
component (via props). The Photo
component would equally pass LikesEntry
to the CommentsFunctionality
component
In ./components/Photo.js
<CommentsFunctionality
LikesEntry={() =>
props.LikesEntry(props.data.id)}
commentsNum={props.data.comments.length}
likes={props.data.likes} />
And lastly the ./components/InteractiveButtons.js
get's to use it upon click:
export default ({likes, LikesEntry, commentsNum}) => (
<div className="meta">
<button className="heart" onClick={LikesEntry} ><MdFavoriteBorder />{likes}</button>
<p><MdModeComment />{ commentsNum }</p>
That's it with the likes button. On click the component state updates along with the server.
(As you can see I added the comments number there as well, just to make UI feel complete)
Getting individual photograph
Almost the same process is involved in the ./pages/photo.js
component, converting it to a class component and add props to state
The same process as before, now a single image needs to be fetched. So ./pages/photo.js
component needs to be converted into a class component.
import react, { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/Layout';
import Photo from '../components/Photo';
import CommentsFunctionality from '../components/InteractiveButtons'
export default class extends Component {
static async getInitialProps({query}) {
const {id} = {...query}
const res = await fetch(`http://localhost:4000/photos/${id}`)
const image = await res.json()
return { image }
}
componentWillMount(){
this.setState({
image: this.props.image
})
}
render(){
return(
<Layout>
...
<img src={`/static/art/${this.state.image.image}.jpg`} alt=''/>
<CommentsFunctionality />
</div>
<div className="comments">
<p className="tagline">{this.state.image.tagline}</p>
{
this.state.image.comments.map((comment, key) => <p key={key}><strong>{comment.user}:</strong>{comment.body}</p>)
}
<form className="comment-form">
<input type="text" ref="author" placeholder="Author" />
<input type="text" ref="comment" placeholder="comment..." />
<input type="submit" />
</form>
...
Exactly as we did in the Index
component, the data is fetched and injected in the component state. The returned JSX (react's version of HTML) is exactly the same just that we return from this.state
. Also ref
is added to the form inputs. ("Refs provide a way to access DOM nodes or React elements created in the render method" (react docs))
Add comments to the image
Finally we need to deal with the form.
Lets add the functionality which adds a comment to the state and the server. Start by creating the onSubmit
functionality and linking it to the form.
submitComments(e){
e.preventDefault();
const user = this.refs.author.value;
const body = this.refs.comment.value;
const comments = this.state.image.comments;
comments.push({user, body})
this.setState({comments})
fetch(`http://localhost:4000/photos/${this.state.image.id}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state.image)
})
}
...
<form className="comment-form" onSubmit={(e)=> this.submitComments(e)} >
<input type="text" ref="author" placeholder="Author" />
<input type="text" ref="comment" placeholder="comment..." />
<input type="submit" />
</form>
That's about it!
The code can be found at the same repository but in part2 branch.
Next time we'll explore how to add express to this next.js project and recreate the basic API required for this project
Top comments (2)
It will be better with a real fetch example instead of a
json-server
one.But in my case, I think the implementation of
ctx.renderPage
is breaking the pages/api rendering.thank you it was helpful and question is that: is it possible to send multiple data in body: JSON.stringify(this.state.image, this.state.another)? I did but I get error!