This course is now available in English.Click here to change the language.

Toteutetaan seuraavaksi React-sovellus, joka käyttää toteuttamaamme GraphQL-palvelinta. Palvelimen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-3.

GraphQL:ää on periaatteessa mahdollista käyttää HTTP POST -pyyntöjen avulla. Seuraavassa esimerkki Postmanilla tehdystä kyselystä.

fullstack content

Kommunikointi tapahtuu siis osoitteeseen http://localhost:4000/graphql kohdistuvina POST-pyyntöinä, ja itse kysely lähetetään pyynnön mukana merkkijonona avaimen query arvona.

Voisimmekin hoitaa React-sovelluksen ja GraphQL:n kommunikoinnin Axiosilla. Tämä ei kuitenkaan ole useimmiten järkevää ja on parempi idea käyttää korkeamman tason kirjastoa, joka pystyy abstrahoimaan kommunikoinnin turhia detaljeja. Tällä hetkellä järkeviä vaihtoehtoja on kaksi: Facebookin Relay ja Apollo Client. Näistä Apollo on ylivoimaisesti suositumpi ja myös meidän valintamme.

Apollo client

Luodaan uusi React-sovellus ja asennetaan siihen Apollo clientin vaatimat riippuvuudet.

npm install apollo-boost react-apollo graphql --save

Aloitetaan seuraavalla ohjelmarungolla.

import React from 'react'
import ReactDOM from 'react-dom'

import ApolloClient, { gql } from 'apollo-boost'

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql'
})

const query = gql`
{
  allPersons  {
    name,
    phone,
    address {
      street,
      city
    }
    id
  }
}
`

client.query({ query })
  .then((response) => {
    console.log(response.data)
  })

const App = () => {
  return <div>
    test
  </div>
}

ReactDOM.render(<App />, document.getElementById('root'))

Koodi aloittaa luomalla client-olion, jonka avulla se lähettää kyselyn palvelimelle:

client.query({ query })
  .then((response) => {
    console.log(response.data)
  })

Palvelimen palauttama vastaus tulostuu konsoliin:

fullstack content

Sovellus pystyy siis kommunikoimaan GraphQL-palvelimen kanssa olion client välityksellä. Client saadaan sovelluksen kaikkien komponenttien saataville käärimällä komponenttti App komponentin ApolloProvider lapseksi:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'import ApolloClient, { gql } from 'apollo-boost'
import { ApolloProvider } from 'react-apollo'
const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql'
})

ReactDOM.render(
  <ApolloProvider client={client} >    <App />  </ApolloProvider>,   document.getElementById('root')
)

Query-komponentti

Olemme valmiina toteuttamaan sovelluksen päänäkymän, joka listaa kaikkien henkilöiden puhelinnumerot.

Apollo Client tarjoaa muutaman vaihtoehtoisen tavan kyselyjen tekemiselle. Tämän hetken (tämä osa on editoitu viimeksi 22.6.2019) vallitseva käytäntö on komponentin Query käyttäminen.

Kyselyn tekevän komponentin App koodi näyttää seuraavalta:

import React from 'react'
import { Query } from 'react-apollo'
import { gql } from 'apollo-boost'

const ALL_PERSONS = gql`
{
  allPersons  {
    name
    phone
    id
  }
}
`

const App = () => {
  return <Query query={ALL_PERSONS}>
    {(result) => { 
      if ( result.loading ) {
        return <div>loading...</div>
      }
      return (
        <div>
          {result.data.allPersons.map(p => p.name).join(', ')}
        </div>
      )
    }}
  </Query>
}

export default App

Koodi vaikuttaa hieman sekavalta. Koodin ytimessä on komponentti Query, joka saa parametrina query suoritettavan kyselyn joka on muuttujassa ALL_PERSONS. Komponentin Query tagien sisällä on funktio, joka palauttaa varsinaisen renderöitävän JSX:n. Funktion parametri result sisältää GraphQL-kyselyn tuloksen.

Tuloksella eli parametrissa result olevalla oliolla on useita kenttiä. Kenttä loading on arvoltaan tosi, jos kyselyyn ei ole saatu vielä vastausta. Tässä tilanteessa renderöitävä koodi on

if ( result.loading ) {
  return <div>loading...</div>
}

Kun tulos on valmis, otetaan tuloksen kentästä data kyselyn allPersons vastaus ja renderöidään luettelossa olevat nimet ruudulle.

<div>
  {result.data.allPersons.map(p => p.name).join(', ')}
</div>

Saadaksemme ratkaisua hieman siistimmäksi, eriytetään henkilöiden näyttäminen omaan komponenttiin Persons. Komponentti App muuttuu seuraavasti:

const App = () => {
  return (
    <Query query={ALL_PERSONS}>
      {(result) => <Persons result={result} />}
    </Query>
  )
}

Eli App välittää kyselyn tuloksen komponentille Persons propsina:

const Persons = ({ result }) => {
  if (result.loading) {
    return <div>loading...</div>
  }

  const persons = result.data.allPersons 

  return (
    <div>
      <h2>Persons</h2>
      {persons.map(p =>
        <div key={p.name}>
          {p.name} {p.phone}
        </div>  
      )}
    </div>
  )
}

Nimetyt kyselyt ja muuttujat

Toteutetaan sovellukseen ominaisuus, jonka avulla on mahdollisuus nähdä yksittäisen henkilön osoitetiedot. Palvelimen tarjoama kysely findPerson sopii hyvin tarkoitukseen.

Edellisessä luvussa tekemissämme kyselyissä parametri oli kovakoodattuna kyselyyn:

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
    id
  }
}

Kun teemme kyselyjä ohjelmallisesti, on kyselyn parametrit pystyttävä antamaan dynaamisesti.

Tähän tarkoitukseen sopivat GraphQL:n muuttujat. Muuttujia käyttääksemme on kysely myös nimettävä.

Sopiva muoto kyselylle on seuraava:

query findPersonByName($nameToSearch: String!) {
  findPerson(name: $nameToSearch) {
    name
    phone 
    address {
      street
      city
    }
  }
}

Kyselyn nimenä on findPersonByName, ja se saa yhden merkkijonomuotoisen parametrin $nameToSearch.

Myös GraphQL Playground mahdollistaa muuttujia sisältävän kyselyjen tekemisen. Tällöin muuttujille on annettava arvot kohdassa Query variables:

fullstack content

Äsken käyttämämme komponentti Query ei sovellu optimaalisella tavalla tarkoitukseen, sillä haluaisimme tehdä kyselyn vasta siinä vaiheessa kun käyttäjä haluaa nähdä jonkin henkilön tiedot.

Eräs tapa on käyttää suoraan client -olion metodia query. Sovelluksen komponentit pääsevät käsiksi query-olioon komponentin ApolloConsumer avulla.

Muutetaan komponenttia App siten, että se hakee ApolloConsumerin avulla viitteen query-olioon ja välittää sen komponentille Persons.

import { Query, ApolloConsumer } from 'react-apollo'
// ...

const App = () => {
  return (
    <ApolloConsumer>
      {(client => 
        <Query query={ALL_PERSONS}>
          {(result) => 
            <Persons result={result} client={client} /> 
          }
        </Query> 
      )}
    </ApolloConsumer>
  )
}

Komponentti Persons muuttuu seuraavasti:

const FIND_PERSON = gql`  query findPersonByName($nameToSearch: String!) {    findPerson(name: $nameToSearch) {      name      phone       id      address {        street        city      }    }  }`
const Persons = ({ result, client }) => {
  const [person, setPerson] = useState(null)  
  if (result.loading) {
    return <div>loading...</div>
  }

  const showPerson = async (name) => {    const { data } = await client.query({      query: FIND_PERSON,      variables: { nameToSearch: name }    })    setPerson(data.findPerson)  }
  if (person) {    return(      <div>        <h2>{person.name}</h2>        <div>{person.address.street} {person.address.city}</div>        <div>{person.phone}</div>        <button onClick={() => setPerson(null)}>close</button>      </div>    )  }
  return (
    <div>
      <h2>Persons</h2>
      {result.data.allPersons.map(p =>
        <div key={p.name}>
          {p.name} {p.phone}
          <button onClick={() => showPerson(p.name)} >            show address          </button>         </div>  
      )}
    </div>
  )
}

Jos henkilön yhteydessä olevaa nappia painetaan, tekee komponentti GraphQL-kyselyn henkilön tiedoista ja tallettaa vastauksen komponentin tilaan person:

const showPerson = async (name) => {
  const { data } = await client.query({
    query: FIND_PERSON,
    variables: { nameToSearch: name }
  })

  setPerson(data.findPerson)
}

Jos tilalla person on arvo, näytetään kaikkien henkilöiden sijaan yhden henkilön tarkemmat tiedot:

fullstack content

Ratkaisu ei ole siistein mahdollinen mutta saa kelvata meille.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-1.

Välimuisti

Kun haemme monta kertaa esim. Arto Hellaksen tiedot, huomaamme mielenkiintoisen asian: kysely backendiin tapahtuu ainoastaan tietojen ensimmäisellä katsomiskerralla. Tämän jälkeen, siitäkin huolimatta, että koodi tekee saman kyselyn uudelleen, ei kyselyä lähetetä backendille:

fullstack content

Apollo client tallettaa kyselyjen tulokset cacheen eli välimuistiin ja optimoi suoritusta siten, että jos kyselyn vastaus on jo välimuistissa, ei kyselyä lähetetä ollenkaan palvelimelle.

Chromeen on mahdollista asentaa lisäosa Apollo Client devtools, jonka avulla voidaan tarkastella mm. välimuistin tilaa

fullstack content

Tieto on organisoitu välimuistiin kyselykohtaisesti. Koska Person-tyypin olioilla on identifioiva kenttä id, jonka tyypiksi on määritelty ID, osaa Apollo yhdistää kahden eri kyselyn palauttaman saman olion. Tämän ansiosta Arto Hellaksen osoitetietojen hakeminen kyselyllä findPerson on päivittänyt välimuistia Arton osoitetietojen osalta myös kyselyn allPersons alta.

Mutation-komponentti

Toteutetaan sovellukseen mahdollisuus uusien henkilöiden lisäämiseen. Sopivan toiminnallisuuden tarjoaa komponentti mutation. Edellisessä luvussa kovakoodasimme mutaatioiden parametrit. Tarvitsemme nyt muuttujia käyttävän version henkilön lisäävästä mutaatiosta:

const CREATE_PERSON = gql`
mutation createPerson($name: String!, $street: String!, $city: String!, $phone: String) {
  addPerson(
    name: $name,
    street: $street,
    city: $city,
    phone: $phone
  ) {
    name
    phone
    id
    address {
      street
      city
    }
  }
}
`

Komponentti App muuttuu seuraavasti:

const App = () => {
  return (
    <div>
      <ApolloConsumer>
        {(client) => 
          <Query query={ALL_PERSONS}>
            {(result) => 
              <Persons result={result} client={client} />
            }
          </Query> 
        }
      </ApolloConsumer>
      <h2>create new</h2>      <Mutation mutation={CREATE_PERSON}>        {(addPerson) =>          <PersonForm            addPerson={addPerson}          />        }      </Mutation>    </div>
  )
}

Komponentin Mutation tagien sisällä on funktio, joka palauttaa varsinaisen renderöitävän lomakkeen muodostaman komponentin PersonForm. Parametrina tuleva addPerson on funktio, jota kutsumalla mutaatio suoritetaan.

Lomakkeen muodostama komponentti ei sisällä mitään ihmeellistä.

const PersonForm = (props) => {
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')
  const [street, setStreet] = useState('')
  const [city, setCity] = useState('')

  const submit = async (e) => {
    e.preventDefault()
    await props.addPerson({
      variables: { name, phone, street, city }
    })

    setName('')
    setPhone('')
    setStreet('')
    setCity('')
  }

  return (
    <div>
      <form onSubmit={submit}>
        <div>
          name <input
            value={name}
            onChange={({ target }) => setName(target.value)}
          />
        </div>
        <div>
          phone <input
            value={phone}
            onChange={({ target }) => setPhone(target.value)}
          />
        </div>
        <div>
          street <input
            value={street}
            onChange={({ target }) => setStreet(target.value)}
          />
        </div>
        <div>
          city <input
            value={city}
            onChange={({ target }) => setCity(target.value)}
          />
        </div>
        <button type='submit'>add!</button>
      </form>
    </div>
  )
}

Lisäys kyllä toimii, mutta sovelluksen näkymä ei päivity. Syynä tälle on se, että Apollo Client ei osaa automaattisesti päivittää sovelluksen välimuistia, se sisältää edelleen ennen lisäystä olevan tilanteen. Saisimme kyllä uuden käyttäjän näkyviin uudelleenlataamalla selaimen, sillä Apollon välimuisti nollautuu uudelleenlatauksen yhteydessä. Tilanteeseen on kuitenkin pakko löytää joku järkevämpi ratkaisu.

Välimuistin päivitys

Ongelma voidaan ratkaista muutamallakin eri tavalla. Eräs tapa on määritellä kaikki henkilöt hakeva kysely pollaamaan palvelinta, eli suorittamaan kysely palvelimelle toistuvasti tietyin väliajoin.

Muutos on pieni, määritellään pollausväliksi kaksi sekuntia:

const App = () => {
  return (
    <div>
      <ApolloConsumer>
        {(client) => 
          <Query query={ALL_PERSONS} pollInterval={2000}>            {(result) =>
              <Persons result={result} client={client} />
            }
          </Query> 
        }
      </ApolloConsumer>

      <h2>create new</h2>
      <Mutation mutation={createPerson} >
        {(addPerson) =>
          <PersonForm
            addPerson={addPerson}
          />
        }
      </Mutation>
    </div>
  )
}

Yksinkertaisuuden lisäksi ratkaisun hyvä puoli on se, että aina kun joku käyttäjä lisää palvelimelle uuden henkilön, se ilmestyy pollauksen ansiosta heti kaikkien sovelluksen käyttäjien selaimeen.

Ikävänä puolena pollauksessa on tietenkin sen aiheuttama turha verkkoliikenne.

Toinen helppo tapa välimuistin synkronoimiseen on määritellä Mutation-komponentin refetchQueries-propsin avulla että kysely ALL_PERSONS tulee suorittaa uudelleen henkilön lisäyksen yhteydessä:

const App = () => {
  return (
    <div>
      <ApolloConsumer>
        {(client) => 
          <Query query={allPersons}>
            {(result) =>
              <Persons result={result} client={client} 
            />}
          </Query> 
        }
      </ApolloConsumer>

      <h2>create new</h2>
      <Mutation
        mutation={CREATE_PERSON} 
        refetchQueries={[{ query: ALL_PERSONS }]}      >
        {(addPerson) =>
          <PersonForm
            addPerson={addPerson}
          />
        }
      </Mutation>
    </div>
  )
}

Edut ja haitat tällä ratkaisulla ovat melkeinpä päinvastaiset pollaukseen. Verkkoliikennettä ei synny kuin tarpeen vaatiessa, eli kyselyjä ei tehdä varalta. Jos joku muu käyttäjä päivittää palvelimen tilaa, muutokset eivät kuitenkaan siirry nyt kaikille käyttäjille.

Muitakin tapoja välimuistin tilan päivittämiseksi on, niistä lisää myöhemmin tässä osassa.

HUOM Apollo Client devtools vaikuttaa olevan hieman buginen, se lopettaa jossain vaiheessa välimuistin tilan päivittämisen. Jos törmäät ongelmaan, avaa sovellus uudessa välilehdessä.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-2.

Mutaatioiden virheiden käsittely

Jos yritämme luoda epävalidia henkilöä, seurauksena on poikkeus.

fullstack content

Poikkeus on syytä käsitellä. Eräs tapa poikkeusten käsittelyyn on rekisteröidä mutaatiolle poikkeuksenkäsittelijä onError-propsin avulla:

const App = () => {
  const [errorMessage, setErrorMessage] = useState(null)  const handleError = (error) => {    setErrorMessage(error.graphQLErrors[0].message)    setTimeout(() => {      setErrorMessage(null)    }, 10000)  }
  return (
    <div>
      {errorMessage &&        <div style={{color: 'red'}}>          {errorMessage}        </div>      }      <ApolloConsumer>
        // ...
      </ApolloConsumer>

      <h2>create new</h2>
      <Mutation
        mutation={createPerson} 
        refetchQueries={[{ query: allPersons }]}
        onError={handleError}      >
        {(addPerson) =>
          <PersonForm
            addPerson={addPerson}
          />
        }
      </Mutation>
    </div>
  )
}

Poikkeuksesta tiedotetaan nyt käyttäjälle yksinkertaisella notifikaatiolla.

fullstack content

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-3.

Puhelinnumeron päivitys

Tehdään sovellukseen mahdollisuus vaihtaa henkilöiden puhelinnumeroita. Ratkaisu on lähes samanlainen kuin uuden henkilön lisäykseen käytetty.

Mutaatio edellyttää jälleen muuttujien käyttöä.

const EDIT_NUMBER = gql`
mutation editNumber($name: String!, $phone: String!) {
  editNumber(name: $name, phone: $phone)  {
    name
    phone
    address {
      street
      city
    }
    id
  }
}
`

Tehdään lisäys App-komponenttiin:

const App = () => {
  // ...
  return (
    <div>
      {errorMessage && ... }
      <ApolloConsumer>
        // ...
      </ApolloConsumer>
      
      <h2>create new</h2>
      <Mutation mutation={CREATE_PERSON}>
        // ...
      </Mutation>

      <h2>change number</h2>      <Mutation        mutation={EDIT_NUMBER}      >        {(editNumber) =>          <PhoneForm            editNumber={editNumber}          />        }      </Mutation>       </div>
  )
}

Muutoksen suorittava komponentti PhoneForm on suoraviivainen, se kysyy lomakkeen avulla henkilön nimeä ja uutta puhelinnumeroa, ja kutsuu mutaation tekevää funktiota editNumber:

const PhoneForm = (props) => {
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')

  const submit = async (e) => {
    e.preventDefault()

    await props.editNumber({
      variables: { name, phone }
    })

    setName('')
    setPhone('')
  }

  return (
    <div>
      <form onSubmit={submit}>
        <div>
          name <input
            value={name}
            onChange={({ target }) => setName(target.value)}
          />
        </div>
        <div>
          phone <input
            value={phone}
            onChange={({ target }) => setPhone(target.value)}
          />
        </div>
        <button type='submit'>change number</button>
      </form>
    </div>
  )
}

Ulkoasu on karu mutta toimiva:

fullstack content

Kun numero muutetaan, päivittyy se hieman yllättäen automaattisesti komponentin Persons renderöimään nimien ja numeroiden listaan. Tämä johtuu kahdesta seikasta. Ensinnäkin koska henkilöillä on identifioiva, tyyppiä ID oleva kenttä, päivittyy henkilö välimuistissa uusilla tiedoilla päivitysoperaation yhteydessä. Toinen syy näkymän päivittymiselle on se, että komponentin Query avulla tehdyn kyselyn palauttama data huomaa välimuistiin tulleet muutokset ja päivittää itsensä automaattisesti. Tämä koskee ainoastaan kyselyn alunperin palauttamia olioita, ei välimuistiin lisättäviä kokonaan uusia olioita, jotka uudelleen tehtävä kysely palauttaisi.

Jos yritämme vaihtaa olemattomaan nimeen liittyvän puhelinnumeron, ei mitään näytä tapahtuvan. Syynä tälle on se, että jos nimeä vastaavaa henkilöä ei löydy, vastataan kyselyyn null:

fullstack content

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-4.

Apollo Client ja sovelluksen tila

Esimerkissämme sovelluksen tilan käsittely on siirtynyt suurimmaksi osaksi Apollo Clientin vastuulle. Tämä onkin melko tyypillinen ratkaisu GraphQL-sovelluksissa. Esimerkkimme käyttää Reactin komponenttien tilaa ainoastaan lomakkeen tilan hallintaan sekä virhetilanteesta kertovan notifikaation näyttämiseen. GraphQL:ää käytettäessä voikin olla, että ei ole enää kovin perusteltuja syitä siirtää sovelluksen tilaa ollenkaan Reduxiin.

Apollo mahdollistaa tarvittaessa myös sovelluksen paikallisen tilan tallettamisen Apollon välimuistiin.

Render props

GraphQL:n Query, Mutation ja ApolloConsumer komponentit noudattavat periaatetta, joka kulkee nimellä render props. Periaatetta noudattava komponentti saa propsina tai tagiensa välissä lapsina (joka on teknisesti ottaen myös props) funktion, joka määrittelee miten komponentin renderöinti tapahtuu. Render props -periaatteen avulla on mahdollista siirtää renderöinnistä huolehtivalle komponentille joko dataa tai funktioviitteitä.

Render props -periaate on ollut viime aikoina melko suosittu, mm. osassa 7 käsittelemämme react router käyttää sitä. React routerin komponentin Route avulla määritellään mitä sovellus renderöi selaimen ollessa tietyssä urlissa. Seuraavassa määritellään, että jos selaimen url on /notes, renderöidään komponentti Notes, jos taas selaimen url on esim. /notes/10, renderöidään komponentti Note, joka saa propsina muistiinpano-olion, jonka id on 10.

<Router>
  <div>
    // ...
    <Route exact path='/notes' render={() => 
      <Notes notes={notes} />
    } />    
    <Route exact path='/notes/:id' render={({ match }) =>
      <Note note={noteById(match.params.id)} />
    } />
  </div>
</Router>

Urleja vastaavat komponentit on määritelty render propseina. Render props -funktion avulla renderöitävälle komponentille on mahdollista välittää tietoa, esim. yksittäisen muistiinpanon sivu saa propsina urliaan vastaavan muistiinpanon.

Itse en ole suuri render propsien fani. React routerin yhteydessä ne vielä menettelevät, mutta erityisesti GraphQL:n yhteydessä niiden käyttö tuntuu erittäin ikävältä.

Joudumme esimerkissämme käärimään komponentin Persons ikävästi kahden render props -komponentin sisälle:

<ApolloConsumer>
  {(client) => 
    <Query query={allPersons}>
      {(result) => <Persons result={result} client={client} />}
    </Query> 
  }
</ApolloConsumer>

Muutaman viikon kuluessa asiaan on kuitenkin odotettavissa muutoksia ja Apollo Clientiin tullaan lisäämään rajapinta, jonka avulla kyselyjä ja mutaatioita on mahdollista tehdä hookien avulla.

Yleisemminkin trendinä on se, että hookeilla tullaan useissa tapauksissa korvaamaan tarve render propsien käyttöön.

Apollon hookit

Jo nyt Apollo Client 3.0:sta on olemassa beta-julkaisu. Kokeillaan sitä nyt. Asennetaan kirjastosta oikea versio:

npm install --save react-apollo@3.0.0-beta.2

Tällä hetkellä (28.6.2019) ei ole olemassa juuri mitään dokumentaatiota Apollon hookien käytöstä. Tämä blogi on eräs ainoista googlen löytämistä ohjeista.

Tiedostoon index.js tarvitaan vielä pieni muutos kirjaston asennuksen jälkeen:

import React from 'react'
import ReactDOM from 'react-dom'
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from "@apollo/react-hooks"
import App from './App'

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql'
})

ReactDOM.render(
  <ApolloProvider client={client} >
    <App />
  </ApolloProvider>, 
  document.getElementById('root')
)

Muutetaan komponenttia Persons siten, että se käyttää useApolloClient-hookia.

import React,  { useState } from 'react'
import { gql } from 'apollo-boost'
import { useApolloClient } from '@apollo/react-hooks'
// ...

const Persons = ({ result }) => {  const client = useApolloClient()  // ...
}

Komponentti App yksinkertaistuu, render props -komponentti ApolloConsumer voidaan poistaa:

const App = () => {

  return(
    <div>
      {errorMessage &&
        <div style={{ color: 'red' }}>
          {errorMessage}
        </div>
      }
      <Query query={ALL_PERSONS}>        {(result) => <Persons result={result} />}      </Query>       // ...
    </div>
  )
}

Hankkiudutaan seuraavaksi eroon komponentista Query hookin useQuery avulla. Komponentti App yksinkertaistuu edelleen:

import { useQuery } from '@apollo/react-hooks'
const App = () => {
  const persons = useQuery(ALL_PERSONS)
  // ...

  return (
    <div>
      {errorMessage &&
        <div style={{ color: 'red' }}>
          {errorMessage}
        </div>
      }

      <Persons result={persons} />
      <Mutation
        mutation={createPerson} 
        refetchQueries={[{ query: allPersons }]}
        onError={handleError}
      >
        {(addPerson) =>
          <PersonForm
            addPerson={addPerson}
          />
        }
      </Mutation>
      // ...
    </div>
  )
}

Mutation-komponentit saadaan korvattua hookin useMutation avulla. Komponentin App lopullinen muoto on seuraava:

import { useQuery, useMutation } from '@apollo/react-hooks'
const App = () => {
  const result = useQuery(ALL_PERSONS)

  const [errorMessage, setErrorMessage] = useState(null)

  const handleError = (error) => {
    // ...
  }

  const [addPerson] = useMutation(CREATE_PERSON, {    onError: handleError,    refetchQueries: [{ query: ALL_PERSONS }]  })
  const [editNumber] = useMutation(EDIT_NUMBER)
  return (
    <div>
      {errorMessage &&
        <div style={{ color: 'red' }}>
          {errorMessage}
        </div>
      }
      <Persons result={result} />

      <h2>create new</h2>
      <PersonForm addPerson={addPerson} />
      <h2>change number</h2>
      <PhoneForm editNumber={editNumber} />    </div>
  )
}

Huomaa, että useMutation palauttaa taulukon, jonka ensimmäinen elementti on funktio, jonka avulla mutaatio tehdään. Taulukon toinen elementti on olio, jonka avulla mutaation loading- ja error-tiloja voidaan tarkkailla. Me emme kuitenkaan näitä tarvitse.

Lopputulos on todellakin monin verroin selkeämpi kuin render props -komponentteja käyttävä sotku. Voimme yhtyä Ryan Florencen React Confissa 2018 esittämään mielipiteeseen 90% Cleaner React With Hooks.

Sovelluksen kirjastoa react-apollo-hooks käyttävä koodi on kokonaisuudessaan githubissa, branchissa part8-5.