b
React 与 GraphQL
我们接下来将实现一个React应用,它使用我们创建的GraphQL服务器。
服务器的当前代码可以在GitHub上找到,分支part8-3。
理论上,我们可以使用GraphQL与HTTP POST请求。下面是一个使用Postman的例子。
通信的工作方式是向http://localhost:4000/graphql 发送HTTP POST请求。查询本身是一个字符串,作为键query的值发送。
我们可以通过使用Axios来处理React应用和GraphQL之间的通信。然而,在大多数情况下,这样做是不太明智的。使用一个能够抽象出不必要的通信细节的高阶库是一个更好的主意。
目前,有两个不错的选择。Facebook的Relay和Apollo Client,它是我们在上一节使用的同一个库的客户端。Apollo绝对是这两个中最受欢迎的,我们在这一节也会使用它。
Apollo client
让我们创建一个新的React-app。在写这篇文章的时候(2022年4月21日),Apollo客户端不能很好地与React 18版本一起工作。所以我们通过改变文件package.json将项目降级到以前的版本,如下所示。
{
"dependencies": {
"react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
},
// ...
}
更改后,通过运行以下程序重新安装依赖关系
npm install
现在我们可以继续安装Apollo客户端所需的依赖项。
npm install @apollo/client graphql
我们将用以下代码开始我们的应用。
import ReactDOM from 'react-dom'
import App from './App'
import { ApolloClient, HttpLink, InMemoryCache, gql } from '@apollo/client'
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://localhost:4000',
})
})
const query = gql`
query {
allPersons {
name,
phone,
address {
street,
city
}
id
}
}
`
client.query({ query })
.then((response) => {
console.log(response.data)
})
ReactDOM.render(<App />, document.getElementById('root'))
代码的开头创建了一个新的客户端对象,然后用它向服务器发送一个查询。
client.query({ query })
.then((response) => {
console.log(response.data)
})
服务器的响应被打印到控制台。
应用可以使用client对象与GraphQL服务器通信。通过用ApolloProvider包装App组件,可以使应用的所有组件都能访问该客户端。
import ReactDOM from 'react-dom'
import App from './App'
import {
ApolloClient, ApolloProvider, HttpLink, InMemoryCache} from '@apollo/client'
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://localhost:4000',
})
})
ReactDOM.render(
<ApolloProvider client={client}> <App />
</ApolloProvider>, document.getElementById('root')
)
Making queries
我们准备实现应用的主视图,它显示一个电话号码列表。
Apollo客户端提供了一些替代方案来进行查询。
目前,使用钩子函数useQuery是最主要的做法。
查询是由App组件进行的,其代码如下。
import { gql, useQuery } from '@apollo/client'
const ALL_PERSONS = gql`
query {
allPersons {
name
phone
id
}
}
`
const App = () => {
const result = useQuery(ALL_PERSONS)
if (result.loading) {
return <div>loading...</div>
}
return (
<div>
{result.data.allPersons.map(p => p.name).join(', ')}
</div>
)
}
export default App
当被调用时,useQuery做出它作为参数收到的查询。
它返回一个有多个[字段]的对象(https://www.apollographql.com/docs/react/api/react/hooks/#result)。
如果查询还没有收到响应,字段loading为真。
然后下面的代码会被渲染。
if (result.loading) {
return <div>loading...</div>
}
当收到响应时,可以从data字段中找到allPersons查询的结果,我们可以将名字的列表渲染在屏幕上。
<div>
{result.data.allPersons.map(p => p.name).join(', ')}
</div>
让我们把显示人名列表分离成自己的组件。
const Persons = ({ persons }) => {
return (
<div>
<h2>Persons</h2>
{persons.map(p =>
<div key={p.name}>
{p.name} {p.phone}
</div>
)}
</div>
)
}
App组件仍然进行查询,并将结果传递给新的组件进行渲染。
const App = () => {
const result = useQuery(ALL_PERSONS)
if (result.loading) {
return <div>loading...</div>
}
return (
<Persons persons={result.data.allPersons}/>
)
}
Named queries and variables
我们来实现查看一个人的地址细节的功能。findPerson查询很适合于此。
我们在上一章所做的查询将参数硬编码到查询中。
query {
findPerson(name: "Arto Hellas") {
phone
city
street
id
}
}
当我们以编程方式进行查询时,我们必须能够动态地给他们参数。
GraphQL变量很适合于此。为了能够使用变量,我们也必须为我们的查询命名。
查询的一个好的格式是这样的。
query findPersonByName($nameToSearch: String!) {
findPerson(name: $nameToSearch) {
name
phone
address {
street
city
}
}
}
查询的名字是findPersonByName,它被赋予一个字符串$nameToSearch作为参数。
也可以用Apollo Explorer进行带参数的查询。参数在Variables中给出。
useQuery钩子非常适用于在组件渲染时进行查询的情况。 然而,我们现在想只在用户想看一个特定人的细节时才进行查询,所以查询只[按要求]进行(https://www.apollographql.com/docs/react/data/queries/#executing-queries-manually)。
这种情况的一个可能性是钩子函数useLazyQuery,它可以定义一个查询,当用户想看一个人的详细信息时才执行。
然而,在我们的案例中,我们可以坚持使用useQuery,并使用选项skip,这使得只有在设定的条件为真时才可以进行查询。
解决方案如下。
import { useState } from 'react'
import { gql, useQuery } from '@apollo/client'
const FIND_PERSON = gql`
query findPersonByName($nameToSearch: String!) {
findPerson(name: $nameToSearch) {
name
phone
id
address {
street
city
}
}
}
`
const Person = ({ person, onClose }) => {
return (
<div>
<h2>{person.name}</h2>
<div>
{person.address.street} {person.address.city}
</div>
<div>{person.phone}</div>
<button onClick={onClose}>close</button>
</div>
)
}
const Persons = ({ persons }) => {
const [nameToSearch, setNameToSearch] = useState(null) const result = useQuery(FIND_PERSON, { variables: { nameToSearch }, skip: !nameToSearch, })
if (nameToSearch && result.data) { return ( <Person person={result.data.findPerson} onClose={() => setNameToSearch(null)} /> ) }
return (
<div>
<h2>Persons</h2>
{persons.map((p) => (
<div key={p.name}>
{p.name} {p.phone}
<button onClick={() => setNameToSearch(p.name)}> show address </button> </div>
))}
</div>
)
}
export default Persons
代码有相当大的变化,所有的变化并不完全明显。
当按下一个人的按钮显示地址时,这个人的名字被设置为状态nameToSearch。
<button onClick={() => setNameToSearch(p.name)}>
show address
</button>
这导致组件重新渲染。在渲染时,获取用户详细信息的查询FIND_PERSON被执行,如果变量nameToSearch有一个值。
const result = useQuery(FIND_PERSON, {
variables: { nameToSearch },
skip: !nameToSearch,})
当用户对看到任何一个人的详细信息不感兴趣时,状态变量nameToSearch为空,查询不被执行。
如果状态变量nameToSearch有一个值,并且查询结果已经准备好了,组件Person会显示一个人的详细信息。
if (nameToSearch && result.data) {
return (
<Person
person={result.data.findPerson}
onClose={() => setNameToSearch(null)}
/>
)
}
一个单一的人的视图如下所示:
当用户想返回到人物列表时,nameToSearch状态被设置为null。
该应用的当前代码可以在GitHub分支part8-1找到。
Cache
当我们进行多次查询时,例如查询Arto Hellas的详细地址,我们注意到一些有趣的事情:向后端查询只在第一次进行。在这之后,尽管代码再次进行相同的查询,但查询不会被发送到后端。
Apollo客户端将查询的响应保存到cache。为了优化性能,如果查询的响应已经在缓存中,查询根本不会被发送到服务器。
缓存显示了查询findPerson后Arto Hellas的详细信息。
Doing mutations
我们来实现添加新人物的功能。
在上一章中,我们对改变的参数进行了硬编码。现在,我们需要一个使用变量的addPerson改变的版本。
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
}
}
}
`
钩子函数useMutation提供了制作改变的功能。
让我们创建一个新的组件,用于在目录中添加一个新的人。
import { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
const CREATE_PERSON = gql`
// ...
`
const PersonForm = () => {
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [street, setStreet] = useState('')
const [city, setCity] = useState('')
const [ createPerson ] = useMutation(CREATE_PERSON)
const submit = (event) => {
event.preventDefault()
createPerson({ variables: { name, phone, street, city } })
setName('')
setPhone('')
setStreet('')
setCity('')
}
return (
<div>
<h2>create new</h2>
<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>
)
}
export default PersonForm
这个表单的代码很简单,有趣的几行已经被强调了。
我们可以使用useMutation钩子来定义改变函数。
该钩子返回一个array,其中的第一个元素包含引起改变的函数。
const [ createPerson ] = useMutation(CREATE_PERSON)
查询变量在查询时接收数值。
createPerson({ variables: { name, phone, street, city } })
新的人员添加得很好,但屏幕没有更新。这是因为Apollo客户端不能自动更新应用的缓存,所以它仍然包含改变前的状态。
我们可以通过重新加载页面来更新屏幕,因为当页面被重新加载时,缓存被清空。然而,一定有更好的方法来做到这一点。
Updating the cache
对此有几个不同的解决方案。一种方法是对所有的人进行查询poll服务器,或者反复进行查询。
变化很小。让我们把查询设置为每两秒轮询一次。
const App = () => {
const result = useQuery(ALL_PERSONS, {
pollInterval: 2000 })
if (result.loading) {
return <div>loading...</div>
}
return (
<div>
<Persons persons = {result.data.allPersons}/>
<PersonForm />
</div>
)
}
export default App
解决方案很简单,每当一个用户添加一个新的人,它就会立即出现在所有用户的屏幕上。
这个解决方案的坏处是所有无意义的网络流量。
另一个保持缓存同步的简单方法是使用useMutation钩子的refetchQueries参数来定义,每当创建一个新的人,就重新进行获取所有人员的查询。
const ALL_PERSONS = gql`
query {
allPersons {
name
phone
id
}
}
`
const PersonForm = (props) => {
// ...
const [ createPerson ] = useMutation(CREATE_PERSON, {
refetchQueries: [ { query: ALL_PERSONS } ] })
这个解决方案的优点和缺点几乎与前一个解决方案相反。没有额外的网络流量,因为查询不是为了以防万一而进行的。 然而,如果一个用户现在更新了服务器的状态,这些变化不会立即显示给其他用户。
如果你想做多个查询,你可以在refetchQueries里面传递多个对象。这将允许你同时更新你的应用的不同部分。下面是一个例子。
const [ createPerson ] = useMutation(CREATE_PERSON, {
refetchQueries: [ { query: ALL_PERSONS }, { query: OTHER_QUERY }, { query: ... } ] // pass as many queries as you need
})
还有其他更新缓存的方法。在这一部分的后面会有更多的介绍。
目前,在我们的代码中,查询和组件被定义在同一个地方。
让我们把查询的定义分开到他们自己的文件queries.js。
import { gql } from '@apollo/client'
export const ALL_PERSONS = gql`
query {
// ...
}
`
export const FIND_PERSON = gql`
query findPersonByName($nameToSearch: String!) {
// ...
}
`
export const CREATE_PERSON = gql`
mutation createPerson($name: String!, $street: String!, $city: String!, $phone: String) {
// ...
}
`
然后每个组件导入它需要的查询。
import { ALL_PERSONS } from './queries'
const App = () => {
const result = useQuery(ALL_PERSONS)
// ...
}
该应用的当前代码可以在GitHub分支part8-2上找到。
Handling mutation errors
试图用无效的数据创建一个人,会导致一个错误。
我们应该处理这个异常。我们可以使用useMutation钩子的onError选项向改变注册一个错误处理函数。
让我们给改变注册一个错误处理函数,该函数使用setError。
它收到的函数作为参数来设置一个错误信息。
const PersonForm = ({ setError }) => {
// ...
const [ createPerson ] = useMutation(CREATE_PERSON, {
refetchQueries: [ {query: ALL_PERSONS } ],
onError: (error) => { setError(error.graphQLErrors[0].message) } })
// ...
}
然后我们就可以在屏幕上渲染必要的错误信息。
const App = () => {
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) }
return (
<div>
<Notify errorMessage={errorMessage} /> <Persons persons = {result.data.allPersons} />
<PersonForm setError={notify} /> </div>
)
}
const Notify = ({errorMessage}) => { if ( !errorMessage ) { return null } return ( <div style={{color: 'red'}}> {errorMessage} </div> )}
现在用户可以通过一个简单的通知来了解错误。
该应用的当前代码可以在GitHub分支part8-3上找到。
Updating a phone number
让我们在我们的应用中增加改变人的电话号码的可能性。这个解决方案与我们用于添加新人员的方案几乎相同。
同样,改变需要参数。
export const EDIT_NUMBER = gql`
mutation editNumber($name: String!, $phone: String!) {
editNumber(name: $name, phone: $phone) {
name
phone
address {
street
city
}
id
}
}
`
负责更改的PhoneForm组件很简单。这个表单有个人姓名和新电话号码的字段,并调用changeNumber函数。这个函数是用useMutation钩子完成的。
代码中有趣的行已经被突出显示。
import { useState } from 'react'
import { useMutation } from '@apollo/client'
import { EDIT_NUMBER } from '../queries'
const PhoneForm = () => {
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [ changeNumber ] = useMutation(EDIT_NUMBER)
const submit = (event) => {
event.preventDefault()
changeNumber({ variables: { name, phone } })
setName('')
setPhone('')
}
return (
<div>
<h2>change number</h2>
<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>
)
}
export default PhoneForm
它看起来很暗淡,但它能工作。
令人惊讶的是,当一个人的号码被改变时,新号码会自动出现在由Persons组件渲染的人员列表中。
发生这种情况是因为每个人都有一个ID类型的识别字段,所以当一个人的详细资料随着改变而改变时,保存在缓存中的资料会自动更新。
目前的应用代码可以在GitHub分支part8-4上找到。
我们的应用仍然有一个小缺陷。如果我们试图改变一个不存在的名字的电话号码,似乎什么都不会发生。
这种情况发生的原因是,如果找不到一个具有该名字的人。
改变的响应是null。
对于GraphQL,这不是一个错误,所以注册一个onError错误处理程序是没有用的。
我们可以使用由useMutation钩子返回的result字段作为其第二个参数来生成一个错误信息。
const PhoneForm = ({ setError }) => {
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [ changeNumber, result ] = useMutation(EDIT_NUMBER)
const submit = (event) => {
// ...
}
useEffect(() => { if (result.data && result.data.editNumber === null) { setError('person not found') } }, [result.data])
// ...
}
如果找不到一个人,或者result.data.editNumber是null,该组件使用它作为prop收到的回调函数来设置一个合适的错误信息。
我们希望只在改变的结果中设置错误信息
result.data发生变化,所以我们使用useEffect钩子来控制设置错误信息。
使用useEffect会导致一个ESLint警告。
这个警告毫无意义,最简单的解决办法是忽略该行的ESLint规则。
useEffect(() => {
if (result.data && !result.data.editNumber) {
setError('name not found')
}
}, [result.data]) // eslint-disable-line
我们可以尝试通过在useEffect's第二参数数组中加入setError函数来摆脱这个警告。
useEffect(() => {
if (result.data && !result.data.editNumber) {
setError('name not found')
}
}, [result.data, setError])
但是,如果notify函数没有被包裹到useCallback函数中,这个解决方案就不起作用。 如果不是这样,这将导致一个无休止的循环。当通知被删除后,App组件被重新渲染,一个新版本的notify被创建,导致效果函数被执行,从而导致一个新的通知,等等,等等......
该应用的当前代码可以在GitHub分支part8-5上找到。
Apollo Client and the applications state
在我们的例子中,应用的状态管理主要由Apollo客户端负责。这是一个非常典型的GraphQL应用的解决方案。
我们的例子只使用React组件的状态来管理表单的状态和显示错误通知。因此,在使用GraphQL时,可能没有合理的理由使用Redux来管理应用的状态。
必要时,Apollo可以将应用的本地状态保存到Apollo cache。