DEV Community

aurel kurtula
aurel kurtula

Posted on • Updated on

Introduction to Next.js - fetching data from API

Following from the last tutorial

Today we'll cover

  1. Fetching data from an API.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
...
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
                ...

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Oldest comments (2)

Collapse
 
mehdiyaq profile image
Mehdi Yaghoubi

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!

Collapse
 
francisrod01 profile image
Francis Rodrigues

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.