Tämä on 3. periodilla pidetyn, jo päättyneen kurssin sisältö.

Jos haluat suorittaa kurssin nyt, mene osoitteeseen https://fullstackopen-2019.github.io/

Toteutetaan seuraavaksi React-sovellus, joka käyttää toteuttamaamme GraphQL-palvelinta.

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

fullstack content

Kommunikointi tapahtuu siis osoitteeseen http://localhosto: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 kirjoitettu 17.2.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 results sisältää GraphQL-kyselyn tuloksen.

Tuloksella eli parametrissa results 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={allPersons}>
      {(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={allPersons}>
          {(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 }) => {
  if (result.loading) {
    return <div>loading...</div>
  }

  const [person, setPerson] = useState(null)
  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 mielenkinntoisen asian. Kysely backendiin tapahtuu ainoastaan tietojen ensimmäisellä katsomiskerralla, tämän jälkeen siitäkin huomlimatta, että koodi tekee saman kysely 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            addUser={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.addUser({
      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älimuiti 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
            addUser={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={createPerson} 
        refetchQueries={[{ query: ALL_PERSONS }]}      >
        {(addPerson) =>
          <PersonForm
            addUser={addPerson}
          />
        }
      </Mutation>
    </div>
  )
}

Edut ja haitat tällä ratkaisulla ovat melkeimpä päinvastauset 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
            addUser={addPerson}
          />
        }
      </Mutation>
    </div>
  )
}

Poikkeuksesta tiedotetaan nyt käyttäjää 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 PersonForm = (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 -periaatten 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 kuukauden 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.

react-apollo-hooks

Jo tällä hetkellä on olemassa kirjasto react-apollo-hooks, joka mahdollistaa Apollo clientin käytön hookien avulla. Asennetaan kirjasto

npm install --save react-apollo-hooks

Otetaan nyt apollo-hookit käyttöön sovelluksessa. Muutetaan index.js ensin muotoon, joka mahdollistaa yhtäaikaisen hookien ja Query- sekä Mutation-komponenttien käytön:

import ApolloClient from 'apollo-boost'
import { ApolloProvider } from 'react-apollo'
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'
const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql'
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <ApolloHooksProvider client={client}>      <App />
    </ApolloHooksProvider>  </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 'react-apollo-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 'react-apollo-hooks'
const App = () => {
  const result = useQuery(ALL_PERSONS)
  // ...

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

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

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

import { useQuery, useMutation } from 'react-apollo-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>
  )
}

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

Apollo-tiimi on lupaillut että suora hook-tuki ilmestyy kevään aikana. Ennen suoran tuen toteuttamista voi jo melko turvallisin mielin käyttää kirjastoa react-apollo-hooks.

Koska render props -komponenteista on päästy kokonaan eroon, yksinkertaistuu inder.js seuraavasti

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

import ApolloClient from 'apollo-boost'
import { ApolloProvider } from 'react-apollo-hooks'

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

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

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