d
Kirjautuminen ja välimuistin päivitys
Sovelluksen frontend toimii puhelinluettelon näyttämisen osalta päivitetyn palvelimen kanssa. Jotta luetteloon voitaisiin lisätä henkilöitä, tulee frontendiin toteuttaa kirjautuminen.
Käyttäjän kirjautuminen
Määritellään ensin kirjautumisen suorittava mutaatio tiedostossa src/queries.js:
export const LOGIN = gql`
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password) {
value
}
}
`Määritellään kirjautumisesta huolehtiva komponentti LoginForm tiedostossa src/components/LoginForm.jsx. Se toimii melko samalla tavalla kuin aiemmat mutaatioista huolehtivat komponentit. Mielenkiintoiset rivit on korostettu koodissa:
import { useState } from 'react'
import { useMutation } from '@apollo/client/react'
import { LOGIN } from '../queries'
const LoginForm = ({ setError, setToken }) => { const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [ login ] = useMutation(LOGIN, { onCompleted: (data) => { const token = data.login.value setToken(token) localStorage.setItem('phonebook-user-token', token) }, onError: (error) => { setError(error.message) } })
const submit = (event) => { event.preventDefault() login({ variables: { username, password } }) }
return (
<div>
<form onSubmit={submit}>
<div>
username <input
value={username}
onChange={({ target }) => setUsername(target.value)}
/>
</div>
<div>
password <input
type='password'
value={password}
onChange={({ target }) => setPassword(target.value)}
/>
</div>
<button type='submit'>login</button>
</form>
</div>
)
}
export default LoginFormKomponentti saa propsina funktiot setError ja setToken, joilla voidaan muuttaa ohjelman tilaa. Tilanhallinnan määrittely on jätetty komponentin App vastuulle.
Kirjautumisen toteuttavalle useMutation-funktiolle on määritelty takaisinkutsufunktio onCompleted, jota kutsutaan, kun mutaatio on suoritettu onnistuneesti. Takaisinkutsufunktiossa tokenin arvo haetaan vastauksen datasta, ja tallennetaan sen jälkeen ohjelman tilaan ja selaimen localStorageen.
Otetaan uusi LoginForm-komponentti käyttöön tiedostossa App.jsx. Lisätään sovelluksen tilaan muuttuja token, joka tallettaa tokenin siinä vaiheessa kun käyttäjä on kirjautunut. Jos token ei ole määritelty, näytetään ainoastaan kirjautumislomake:
import LoginForm from './components/LoginForm'// ...
const App = () => {
const [token, setToken] = useState(localStorage.getItem('phonebook-user-token')) const [errorMessage, setErrorMessage] = useState(null)
const result = useQuery(ALL_PERSONS)
if (result.loading) {
return <div>loading...</div>
}
const notify = (message) => {
setErrorMessage(message)
setTimeout(() => {
setErrorMessage(null)
}, 10000)
}
if (!token) { return ( <div> <Notify errorMessage={errorMessage} /> <h2>Login</h2> <LoginForm setToken={setToken} setError={notify} /> </div> ) }
return (
// ...
)
}Token alustetaan nyt localStoragesta mahdollisesti löytyvällä tokenin arvolla:
const [token, setToken] = useState(localStorage.getItem('phonebook-user-token'))Näin token haetaan sovellukseen myös sivun uudelleenlatauksen yhteydessä, ja käyttäjä pysyy kirjautuneena. Jos localStoragesta ei löydy arvoa avaimelle phonebook-user-token, tokenin arvoksi tulee null.
Lisätään sovellukselle myös nappi, jonka avulla kirjautunut käyttäjä voi kirjautua ulos. Napin klikkauskäsittelijässä asetetaan token tilaan null, poistetaan token local storagesta ja resetoidaan Apollo clientin välimuisti:
import { useApolloClient, useQuery } from '@apollo/client/react'//...
const App = () => {
const [token, setToken] = useState(null)
const [errorMessage, setErrorMessage] = useState(null)
const result = useQuery(ALL_PERSONS)
const client = useApolloClient()
if (result.loading) {
return <div>loading...</div>
}
const onLogout = () => { setToken(null) localStorage.clear() client.resetStore() }
// ...
return (
<>
<Notify errorMessage={errorMessage} />
<button onClick={onLogout}>logout</button> <Persons persons={result.data.allPersons} />
<PersonForm setError={notify} />
<PhoneForm setError={notify} />
</>
)
}Välimuistin nollaaminen tapahtuu Apollon client-objektin metodilla resetStore, clientiin taas päästään käsiksi hookilla useApolloClient. Välimuistin tyhjentäminen on tärkeää, sillä joissain kyselyissä välimuistiin on saatettu hakea dataa, johon vain kirjaantuneella käyttäjällä on oikeus päästä käsiksi.
Tokenin lisääminen headeriin
Backendin muutosten jälkeen uusien henkilöiden lisäys puhelinluetteloon vaatii sen, että käyttäjän token lähetetään pyynnön mukana. Tämä edellyttää muutoksia tiedostossa main.jsx olevaan ApolloClient-olion konfiguraatioon:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { ApolloProvider } from '@apollo/client/react'
import { SetContextLink } from '@apollo/client/link/context'
const authLink = new SetContextLink(({ headers }) => { const token = localStorage.getItem('phonebook-user-token') return { headers: { ...headers, authorization: token ? `Bearer ${token}` : null, } }})
const httpLink = new HttpLink({ uri: 'http://localhost:4000' })
const client = new ApolloClient({ cache: new InMemoryCache(), link: authLink.concat(httpLink)})
createRoot(document.getElementById('root')).render(
<StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</StrictMode>,
)Palvelimen URL kääritään HttpLink-konstruktorin avulla sopivaksi httpLink-olioksi kuten aiemminkin. Nyt sitä muokataan kuitenkin authLink-olion määrittelemän kontekstin avulla siten, että pyyntöjen mukaan asetetaan headerille authorization arvoksi localStoragessa mahdollisesti oleva token.
Uusien henkilöiden lisäys ja numeroiden muuttaminen toimii taas.
Validaatioiden korjaaminen
Sovelluksessa pitäisi pystyä lisäämään henkilö, jolla ei ole puhelinnumeroa. Nyt kuitenkin jos yritämme lisätä puhelinnumerotonta henkilöä, se ei onnistu:

Validointi epäonnistuu, sillä frontend lähettää kentän phone arvona tyhjän merkkijonon.
Muutetaan uuden henkilön luovaa funktiota siten, että se asettaa kentälle phone arvon undefined, jos käyttäjä ei ole syöttänyt kenttään mitään:
const PersonForm = ({ setError }) => {
// ...
const submit = async (event) => {
event.preventDefault()
createPerson({ variables: { name, street, city, phone: phone.length > 0 ? phone : undefined, }, })
setName('')
setPhone('')
setStreet('')
setCity('')
}
// ...
}Nyt backendin ja tietokannan näkökulmasta phone-attribuutilla ei ole arvoa, jos käyttäjä jättää kentän tyhjäksi. Henkilön lisääminen ilman puhelinnumeroa onnistuu jälleen.
Myös numeron muuttamistoiminnallisuudessa on eräs ongelma. Tietokannan validaatiot vaativat, että puhelinnumeron tulee olla vähintään 5 merkkiä pitkä, mutta jos yritämme päivittää olemassa olevan henkilön puhelinnumeroksi liian lyhyen numeron, mitään ei näytä tapahtuvan. Henkilön puhelinnumero ei päivity, mutta toisaalta myöskään mitään virheilmoitusta ei näytetä.
Konsolin Network-välilehdeltä näemme, että pyyntöön vastataan virheilmoituksella:

Muokataan sovellusta siten, että validaatiovirheistä näytetään virheilmoitus myös puhelinnumeroa muutettaessa:
const PhoneForm = ({ setError }) => {
// ...
const submit = async (event) => {
event.preventDefault()
try { await changeNumber({ variables: { name, phone } }) } catch (error) { setError(error.message) }
setName('')
setPhone('')
}
// ...
}Numeron päivittävä pyyntö changeNumber tehdään nyt try-lohkon sisällä. Jos tietokannan validaatiot eivät mene läpi, päädytään catch-lohkoon, jossa asetetaan sovellukseen asianmukainen virheilmoitus käyttäen setError-funktiota:

Välimuistin päivitys revisited
Uusien henkilöiden lisäyksen yhteydessä on siis päivitettävä Apollo clientin välimuisti. Päivitys tapahtuu määrittelemällä mutaation yhteydessä option refetchQueries avulla, että kysely ALL_PERSONS on suoritettava uudelleen:
const PersonForm = ({ setError }) => {
// ...
const [createPerson] = useMutation(CREATE_PERSON, {
onError: (error) => setError(error.message),
refetchQueries: [{ query: ALL_PERSONS }], })
// ...
}Lähestymistapa on kohtuullisen toimiva, ikävänä puolena on toki se, että päivityksen yhteydessä suoritetaan aina myös kysely.
Ratkaisua on mahdollista optimoida hoitamalla välimuistin päivitys itse. Tämä tapahtuu määrittelemällä mutaatiolle refetchQueries-attribuutin sijaan sopiva update-callback, jonka Apollo suorittaa mutaation päätteeksi:
const PersonForm = ({ setError }) => {
// ...
const [createPerson] = useMutation(CREATE_PERSON, {
onError: (error) => setError(error.message),
update: (cache, response) => { cache.updateQuery({ query: ALL_PERSONS }, ({ allPersons }) => { return { allPersons: allPersons.concat(response.data.addPerson), } }) }, })
// ..
} Callback-funktio saa parametriksi viitteen välimuistiin sekä mutaation mukana palautetun datan, eli esimerkkimme tapauksessa lisätyn käyttäjän.
Koodi päivittää funktion updateQuery avulla kyselyn ALL_PERSONS välimuistiin talletetun tilan lisäämällä henkilöiden joukkoon (jonka se saa parametrinsa välityksellä) mutaation lisäämän henkilön.
On myös olemassa tilanteita, joissa ainoa järkevä tapa saada välimuisti pidettyä ajantasaisena on update-callbackillä tehtävä päivitys.
Tarvittaessa välimuisti on mahdollista kytkeä pois päältä joko koko sovelluksesta tai yksittäisiltä kyselyiltä määrittelemällä välimuistin käyttöä kontrolloivalle fetchPolicy:lle arvo no-cache.
Välimuistin kanssa kannattaa olla tarkkana. Välimuistissa oleva epäajantasainen data voi aiheuttaa vaikeasti havaittavia bugeja. Kuten tunnettua, välimuistin ajantasalla pitäminen on erittäin haastavaa. Koodareiden joukossa kulkevan kansanviisauden mukaan
There are only two hard things in Computer Science: cache invalidation and naming things. Katso lisää täältä.
Sovelluksen tämän vaiheen koodi GitHubissa, branchissa part8-5.




