Skip to content

Latest commit

 

History

History
248 lines (198 loc) · 11.1 KB

File metadata and controls

248 lines (198 loc) · 11.1 KB

Hämta data från API i React

Introduktion

Statisk data är ju lite trist, när vi skapar applikationer vill vi jobba med levande och dynamisk data från externa (eller internal) källor. Som tur är finns det galet många publika API:er (Application Programmable Interface) att hämta data ifrån.

Det "enklaste" sättet är att använda fetch API som är native till JavaScript och har bra coverage (Can I Use) på de flesta webbläsare.

fetch

fetch använder sig utav JavaScripts Promises som innehåller ett värde som eventuellt är tillgängligt:

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.

Det finns en väldigt bra playground för om man vill testa sig fram till hur Promises fungerar: Promisees

Problemet med att kalla på API:er är att vi aldrig vet hur lång tid det tar att få tillbaka informationen eftersom den hämtas asynkront och därför följer inte AJAX det "vanliga" kodflödet (alltså att det inte körs exakt rad för rad. Förövrigt är även this.setState({}) asynkront 😎).


MÅSTE-VIDEO FÖR ATT FATTA ASYNKRONITET I JAVASCRIPT:

What the heck is the event loop anyway? by Philip Roberts


Viktigt att förstå att det alltid är ett Promise som returneras och att ett Promise innehåller vårt värde. Vi måste plocka ut vår JSON från detta Promise!

//skinny version
fetch("https://api.me/get")
    .then(response => response.json())           //Get JSON, implicit return
    .then(json => console.log(json))    //Log the JSON
//fat version
fetch("https://api.me/get")
    .then(function(response){
        return response.json()
    })
    .then(function(json){ 
        console.log(json)
    });

När vi har har fått tillbaka vår data json kan vi lagra informationen i state!


Om man inte vill använda fetch så finns det massa andra bibliotek: axios, superagent, reqwest, request för att nämna några. Men fetch är det som är native till JavaScript.

Övningar

Hämta filmer!

För att underlätta testningen av att hämta information via API så ska vi i övningarna hämta data från detta temporära API:

API:et innehåller en array av objekt som är hämtad från IMDB:s databas. Ni ska rendera ut innehållet på er sida. Om ni vill t.ex. köra något css-ramverk så ladda ner det och importera det: import './bootstrap.css.

  1. Använd fetch för att hämta arrayen från urlen ovan och lagra arrayen i ditt state. Detta kan göras via att kalla på en funktion som hämtar informationen eller att du laddar in informationen direkt via componentDidMount(). Om du är osäker på om du lyckats kan du alltid logga: console.log(this.state).
  2. Skapa en komponent som ska vara till för varje film, komponenten ska skriva ut filmens title, betyg samt bild på ett välformatterat sätt. Använd t.e.x bootstrap om du inte orkar cssa själv. Filmen kan vara t.ex. ett Card från bootstrap. Använd sedan en loop för att skapa en komponent för varje film:
//ofullständigt exempel
import Card from './Card';
//För varje film i state, skapa en ny komponent.
const movies = this.state.movies.map(movie => <Card title={movie.title} grade={movie.grade} poster={movie.posterUrl} />)
  1. Implementera så att man kan filtera innehållet. Återanvänd ert inputfält från tidigare i veckan och få det att istället filtrera filmerna. Filtrera t.ex. på filmens namn. Tänk tillbaka på hur du gjorde i JS1.

Extra

Fetch hanterar GET som default. Om vi vill göra något annat än en GET måste vi specifiera det i inställningarna. Inställningarna är ett objekt som skickas med som argument till fetch. Vi måste sätta metoden samt säga vilket innehåll vi ska skicka med. Du kan använda POST, PATCH och DELETE på alla objekt i APIet egentligen men det finns även en samling med "notes" i APIet som du kan använda:

fetch("https://fend-api.herokuapp.com/notes", {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },  
      body: JSON.stringify({ text : "Hello!", completed: false })
  });
  1. Testa att posta en "todo" till https://fend-api.herokuapp.com/notes med hjälp av fetch. Värdet du skickar in ska skickas in i din request ska plockas från ett input-fält. Använd tidigare kod. Värdena som du skickar med hämtas via state. Statet sätts via dina event-funktioner (onChange t.ex.).
  2. Fortsätt implementera en todoapplikation fast med detta remote API. Fast den ska kommunicera via detta API. Så applikationen ska först hämta informationen som ni gjorde med filmerna. Sedan ska varje todo ha en checkbox. När man checkar i checkboxen ska ett API-call skickas till API:et som ändra completed på er todo. Detta betyder att du måste i din komponent någonstans spara id så att du checkar i rätt todo. Checkbox returnerar true eller false. Checkboxar kan också lyssna på onChange.
  3. Vilka fler saker från er gamla todo-applikation saknas? Två olika listor? complete och incomplete. Kolla på er gamla Todo-lista från JS1 t.ex. och försök implementera de delar som saknas. Kom ihåg att ta det i små steg, dela upp det i mindre delar, vad ska göras först?

Super super extra: async/await

  • Implementera async/await i din applikation

Det nya coola sättet (och väldigt smidiga sättet) att hantera Promises och asynkrona handlingar är async/await. Det gör så att man fortfarande använder Promises, men vi får ett mer synkront flöde på vår applikation. Alla funktioner kan använda detta flöde, men vi behöver dock använda dessa två nya nyckelord på rätt ställe:

//async säger att vi ska hantera asynkronitet i denna funktion. Hela denna
//funktion är nu thenable
async function getDataFromApi(){
    //wait säger att vi ska vänta på vårt promise
    const response = await fetch('https://fend-api.herokuapp.com/movies')
    //varje gång vi jobbar asynk måste vi använda await
    const json = await response.json()
    //Till slut kan vi returnera vår json
    return json;
}

getDataFromApi().then(data => console.log(data));

Bra video att kolla gällande detta:

Lösningsförslag

Todoapplikation via API

import React, { Component } from "react";

/**
 * Everthing is in the same component for better overview. I would 
 * recommend splitting it into smaller components! Bootstrap classes used.
 * Just add bootstrap CDN link in 'public/index.html'
 */

class App extends Component {
  /* state holds all todos from API and the input value */
  state = {
    allTodos: [],
    todoValue: ""
  };

  /* On page load, load all the todos */
  componentDidMount() {
    this.fetchAllTodos();
  }
  fetchAllTodos = () => {
    fetch(`http://fend-api.herokuapp.com/notes`)
      .then(res => res.json())
      .then(json => {
        this.setState({ allTodos: json });
      });
  };

  /* Send the ID of the todo to be removed as an argument. Then pass it 
   * Along to fetch, just add the ID at the end of the url. Method: DELETE
   * to remove, headers must be added. */
  deleteTodo = (id) => {
    fetch(`https://fend-api.herokuapp.com/notes/${id}`, {
      method: "DELETE",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      }
    }).then(() => {
      /* On success, filter allTodos using the ID-argument from before. And
       * then set the state again with a new array without the todo */
      const newTodoList = this.state.allTodos.filter(todo => todo.id !== id);
      this.setState({ allTodos: newTodoList });
    });
  };

  addTodo = (e) => {
    /* onSubmit, prevent from submitting form */
    e.preventDefault();
    /* Create new todo based on value from state, default completed is false */
    const todo = {
      text: this.state.todoValue,
      completed: false
    };
    /* Make a post request to the same URL, but with method: POST
     * here we supply a body: a stringified version of our todo variable above */
    fetch("https://fend-api.herokuapp.com/notes", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body: JSON.stringify(todo)
    })
      .then(res => res.json())
      .then(addedTodo => {
        /* On success, the API returns the created todo, it contains
         * id, text and completet. So we just have to set the state again
         * add the new todo to the current state, create a new array, and then
         * use setState() again */
        const newTodoList = [...this.state.allTodos, addedTodo];
        this.setState({ allTodos: newTodoList });
      });
  };

  onChange = (e) => this.setState({ [e.target.name]: e.target.value });

  render() {
    /* Create array of todos, the text, and a button that deletes the todo */
    const todoList = this.state.allTodos.map(todo => {
      return (
        <li className="list-group-item justify-content-between" key={todo.id}>
          <span> {todo.text} </span>
          { /* Use the id of the todo as argument, pass it to deleteTodo */}
          <button
            className="btn btn-outline-danger"
            onClick={() => this.deleteTodo(todo.id)}
          > x </button>
        </li>
      );
    });
    /* Below is mostly bootstrap stuff, a form with a controlled input field 
     * and the list we created above */
    return (
      <div className="App">
        <main className="container" style={ {maxWidth: "400px", marginTop: 60}}>
          <section className="row justify-content-center align-items-center">
          <form onSubmit={this.addTodo} className="form-inline">
              <input
                type="text"
                name="todoValue"
                className="form-control"
                value={this.state.todoValue}
                onChange={this.onChange}
              />
            <input type="submit" value="Add Todo" className="btn btn-primary m-3"/>
          </form>
          </section>
          <ul className="list-group">
            {todoList}
          </ul>
        </main>
      </div>
    );
  }
}

export default App;