React وGraphQL
سننفّذ بعد ذلك تطبيق React يستخدم خادم GraphQL الذي أنشأناه.
تجد الشيفرة الحالية للخادم على GitHub، في الفرع part8-3.
من حيث المبدأ، يمكننا استخدام GraphQL عبر طلبات HTTP من نوع POST. يُظهر ما يلي مثالاً على ذلك باستخدام Postman:

يتم التواصل عبر إرسال طلبات HTTP من نوع POST إلى http://localhost:4000/graphql. والاستعلام نفسه نص يُرسل كقيمة للمفتاح query.
يمكننا تولّي أمر التواصل بين تطبيق React وGraphQL باستخدام Axios. لكن في معظم الأحيان، ليس من الحكمة كثيراً فعل ذلك. من الأفضل استخدام مكتبة أعلى مستوى قادرة على تجريد التفاصيل غير الضرورية من التواصل.
حالياً، هناك خياران جيدان: Relay من Facebook و Apollo Client، وهو الجانب الخاص بالعميل من المكتبة نفسها التي استخدمناها في القسم السابق. Apollo هو بالتأكيد الأكثر شعبية بين الاثنين، وسنستخدمه في هذا القسم أيضاً.
عميل Apollo
لننشئ تطبيق React جديداً ونثبّت الاعتماديات اللازمة لـ عميل Apollo.
npm install @apollo/client graphql
استبدل المحتوى الافتراضي للملف main.jsx بهيكل البرنامج التالي:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import { ApolloClient, gql, HttpLink, InMemoryCache } from '@apollo/client'
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:4000',
}),
cache: new InMemoryCache(),
})
const query = gql`
query {
allPersons {
name
phone
address {
street
city
}
id
}
}
`
client.query({ query }).then((response) => {
console.log(response.data)
})
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
تُنشئ بداية الشيفرة كائن عميل جديداً، يُستخدم بعدها لإرسال استعلام إلى الخادم:
client.query({ query }).then((response) => {
console.log(response.data)
})
تُطبع استجابة الخادم في وحدة التحكم:

يُضاف وسم gql قبل القالب النصي الذي يشكّل الاستعلام، وهو مستورد من حزمة @apollo/client:
import { ApolloClient, gql, HttpLink, InMemoryCache } from '@apollo/client'
// ...
const query = gql`
query {
allPersons {
name
phone
address {
street
city
}
id
}
}
`
بفضل الوسم، تتعرّف إضافة GraphQL في VS Code والأدوات الأخرى على التعريف باعتباره GraphQL، ما يتيح ميزات مثل إبراز الصيغة في المحرر. أما في جهة الخادم، فقد حققنا الشيء نفسه بإضافة تعليق يحدد النوع قبل القالب النصي، لأن مكتبة @apollo/server المستخدمة في الخادم لا تتضمن وسم gql مقابلاً.
يمكن للتطبيق أن يتواصل مع خادم GraphQL باستخدام كائن client. ويمكن جعل العميل متاحاً لجميع مكوّنات التطبيق بتغليف مكوّن App بـ ApolloProvider.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import { ApolloClient, gql, HttpLink, InMemoryCache } from '@apollo/client'
import { ApolloProvider } from '@apollo/client/react'
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:4000',
}),
cache: new InMemoryCache(),
})
// ...
createRoot(document.getElementById('root')).render(
<StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</StrictMode>,
)
إجراء الاستعلامات
أصبحنا مستعدين لتنفيذ العرض الرئيسي للتطبيق، الذي يعرض قائمة باسم الشخص ورقم هاتفه.
يقدم Apollo Client بضعة بدائل لإجراء الاستعلامات. حالياً، يُعد استخدام دالة الخطاف useQuery هو الممارسة السائدة.
يُجري الاستعلام مكوّن App ، وشيفرته كالتالي:
import { gql } from '@apollo/client'
import { useQuery } from '@apollo/client/react'
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 الاستعلام الذي يستقبله كمعامل. وهو يعيد كائناً فيه عدة حقول. يكون الحقل loading قيمته true إذا لم يتلقَّ الاستعلام استجابة بعد. عندئذٍ تُعرض الشيفرة التالية:
if (result.loading) {
return <div>loading...</div>
}
عند استلام استجابة، يمكن العثور على نتيجة استعلام allPersons في الحقل data، ويمكننا عرض قائمة الأسماء على الشاشة.
<div>
{result.data.allPersons.map(p => p.name).join(', ')}
</div>
افصل عرض الأشخاص في مكوّن خاص به في الملف src/components/Persons.jsx:
const Persons = ({ persons }) => {
return (
<div>
<h2>Persons</h2>
{persons.map(p =>
<div key={p.id}>
{p.name} {p.phone}
</div>
)}
</div>
)
}
export default Persons
لا يزال مكوّن App يجري الاستعلام، ويمرر النتيجة إلى المكوّن الجديد ليُعرض:
import { gql } from '@apollo/client'
import { useQuery } from '@apollo/client/react'
import Persons from './components/Persons'
// ...
const App = () => {
const result = useQuery(ALL_PERSONS)
if (result.loading) {
return <div>loading...</div>
}
return <Persons persons={result.data.allPersons} />
}
الاستعلامات المسماة والمتغيرات
لننفّذ وظيفة لعرض تفاصيل عنوان شخص. استعلام 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 مناسب تماماً للحالات التي يُجرى فيها الاستعلام عند عرض المكوّن. لكننا نريد الآن إجراء الاستعلام فقط عندما يريد المستخدم رؤية تفاصيل شخص معيّن، لذا لا يُجرى الاستعلام إلا عند الحاجة.
أحد الاحتمالات لهذا النوع من الحالات هو دالة الخطاف useLazyQuery التي تجعل من الممكن تعريف استعلام يُنفَّذ عندما يريد المستخدم رؤية المعلومات التفصيلية لشخص.
لكن في حالتنا يمكننا الاكتفاء بـ useQuery واستخدام الخيار skip، الذي يجعل من الممكن إجراء الاستعلام فقط إذا تحقق شرط محدد.
بعد التغييرات، يصبح الملف Persons.jsx كالتالي:
import { useState } from 'react'
import { gql } from '@apollo/client'
import { useQuery } from '@apollo/client/react'
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.id}>
{p.name} {p.phone}
<button onClick={() => setNameToSearch(p.name)}>
show address
</button>
</div>
))}
</div>
)
}
export default Persons
تغيّرت الشيفرة كثيراً، وليست كل التغييرات ظاهرة تماماً.
عند الضغط على زر show address لشخص ما، يُضبط اسم الشخص في الحالة nameToSearch:
<button onClick={() => setNameToSearch(p.name)}>
show address
</button>
يتسبب هذا في إعادة عرض المكوّن لنفسه. وعند العرض يُنفَّذ استعلام FIND_PERSON الذي يجلب المعلومات التفصيلية لمستخدم إذا كانت للمتغير nameToSearch قيمة:
const result = useQuery(FIND_PERSON, {
variables: { nameToSearch },
skip: !nameToSearch,
})
عندما لا يهتم المستخدم برؤية المعلومات التفصيلية لأي شخص، يكون متغير الحالة nameToSearch قيمته null ولا يُنفَّذ الاستعلام.
إذا كانت للحالة nameToSearch قيمة وكانت نتيجة الاستعلام جاهزة، يعرض مكوّن Person المعلومات التفصيلية لشخص:
if (nameToSearch && result.data) {
return (
<Person
person={result.data.findPerson}
onClose={() => setNameToSearch(null)}
/>
)
}
يبدو عرض الشخص الواحد كالتالي:

عندما يريد المستخدم العودة إلى قائمة الأشخاص، تُضبط حالة nameToSearch على null.
تجد الشيفرة الحالية للتطبيق على GitHub في الفرع part8-1.
الذاكرة المؤقتة
عندما نُجري استعلامات متعددة، مثلاً بتفاصيل عنوان Arto Hellas، نلاحظ شيئاً مثيراً للاهتمام: لا يُجرى الاستعلام إلى الواجهة الخلفية إلا في المرة الأولى. بعد ذلك، ورغم إجراء الشيفرة للاستعلام نفسه مرة أخرى، لا يُرسل الاستعلام إلى الواجهة الخلفية.

يحفظ عميل Apollo استجابات الاستعلامات في الذاكرة المؤقتة. ولتحسين الأداء، إذا كانت استجابة الاستعلام موجودة أصلاً في الذاكرة المؤقتة، فلا يُرسل الاستعلام إلى الخادم إطلاقاً.

تُظهر الذاكرة المؤقتة المعلومات التفصيلية عن Arto Hellas بعد استعلام findPerson:

إجراء الـ mutations
لننفّذ وظيفة لإضافة أشخاص جدد.
في الفصل السابق، ضمّنّا المعاملات مباشرة في الـ mutations. الآن، نحتاج إلى نسخة من mutation الـ 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 وظيفة إجراء الـ mutations.
أنشئ مكوّناً جديداً PersonForm لإضافة شخص جديد إلى التطبيق. محتوى الملف src/components/PersonForm.jsx كالتالي:
import { useState } from 'react'
import { gql } from '@apollo/client'
import { useMutation } from '@apollo/client/react'
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
}
}
}
`
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
شيفرة النموذج مباشرة وقد أُبرزت الأسطر المهمة. يمكننا تعريف دوال الـ mutation باستخدام خطاف useMutation. يعيد الخطاف مصفوفة، يحتوي عنصرها الأول على الدالة التي تُحدث الـ mutation.
const [createPerson] = useMutation(CREATE_PERSON)
تتلقى متغيرات الاستعلام القيم عند إجراء الاستعلام:
createPerson({ variables: { name, phone, street, city } })
فعّل مكوّن PersonForm في الملف App.jsx:
import { gql } from '@apollo/client'
import { useQuery } from '@apollo/client/react'
import PersonForm from './components/PersonForm'
import Persons from './components/Persons'
// ...
const App = () => {
const result = useQuery(ALL_PERSONS)
if (result.loading) {
return <div>loading...</div>
}
return (
<div>
<Persons persons={result.data.allPersons} />
<PersonForm />
</div>
)
}
export default App
يُضاف الأشخاص الجدد بشكل جيد، لكن الشاشة لا تُحدَّث. السبب أن Apollo Client لا يستطيع تحديث الذاكرة المؤقتة للتطبيق تلقائياً، لذا لا تزال تحتوي على الحالة السابقة للـ mutation. يمكننا تحديث الشاشة بإعادة تحميل الصفحة، إذ تُفرَّغ الذاكرة المؤقتة عند إعادة تحميل الصفحة. لكن لا بد أن تكون هناك طريقة أفضل لفعل ذلك.
تحديث الذاكرة المؤقتة
توجد بضعة حلول مختلفة لهذا. إحدى الطرق هي جعل استعلام جميع الأشخاص يستطلع (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
الحل بسيط، وفي كل مرة يضيف فيها مستخدم شخصاً جديداً، يظهر فوراً على شاشات جميع المستخدمين.
الجانب السلبي في الاستطلاع هو بطبيعة الحال حركة الشبكة غير الضرورية التي يسببها. إضافة إلى ذلك، قد تبدأ الصفحة بالوميض، لأن المكوّن يُعاد عرضه مع كل تحديث للاستعلام وتكون result.loading قيمتها true للحظة وجيزة—فيتوهّج نص loading... على الشاشة لجزء من الثانية.
طريقة سهلة أخرى للحفاظ على تزامن الذاكرة المؤقتة هي استخدام معامل refetchQueries في خطاف useMutation لتعريف أنه كلما أُنشئ شخص جديد، يُعاد إجراء الاستعلام الذي يجلب جميع الأشخاص.
// ...
const ALL_PERSONS = gql`
query {
allPersons {
name
phone
id
}
}
`
const PersonForm = () => {
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [street, setStreet] = useState('')
const [city, setCity] = useState('')
const [createPerson] = useMutation(CREATE_PERSON, {
refetchQueries: [{ query: ALL_PERSONS }],
})
// ...
}
مزايا هذا الحل وعيوبه شبه معاكسة للحل السابق. لا توجد حركة ويب إضافية لأن الاستعلامات لا تُجرى احتياطاً. لكن إذا حدّث أحد المستخدمين الآن حالة الخادم، فلا تظهر التغييرات للمستخدمين الآخرين فوراً.
إذا أردت إجراء استعلامات متعددة، يمكنك تمرير عدة كائنات داخل refetchQueries. سيتيح لك ذلك تحديث أجزاء مختلفة من تطبيقك في الوقت نفسه. إليك مثالاً:
const [createPerson] = useMutation(CREATE_PERSON, {
refetchQueries: [
{ query: ALL_PERSONS },
{ query: OTHER_QUERY },
{ query: ANOTHER_QUERY },
], // مرّر ما تحتاج من استعلامات
})
توجد طرق أخرى لتحديث الذاكرة المؤقتة. المزيد عنها لاحقاً في هذا الجزء.
حالياً، تُعرَّف الاستعلامات والمكوّنات في المكان نفسه في شيفرتنا. لنفصل تعريفات الاستعلامات في ملفها الخاص src/queries.js:
import { gql } from '@apollo/client'
export const ALL_PERSONS = gql`
query {
allPersons {
name
phone
id
}
}
`
export const FIND_PERSON = gql`
query findPersonByName($nameToSearch: String!) {
findPerson(name: $nameToSearch) {
name
phone
id
address {
street
city
}
}
}
`
export 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
}
}
}
`
ثم يستورد كل مكوّن الاستعلامات التي يحتاجها:
import { ALL_PERSONS } from './queries'
const App = () => {
const result = useQuery(ALL_PERSONS)
// ...
}
تجد الشيفرة الحالية للتطبيق على GitHub في الفرع part8-2.
معالجة أخطاء الـ mutation
إذا حاولنا إنشاء شخص غير صالح، مثلاً باستخدام اسم موجود أصلاً في التطبيق، فلا يحدث شيء. لا يُضاف الشخص إلى التطبيق، لكننا لا نتلقى أيضاً أي رسالة خطأ.
في وقت سابق، عرّفنا فحصاً في الخادم يمنع إضافة شخص آخر بالاسم نفسه ويرمي خطأً في مثل هذه الحالة. لكن الخطأ لم يُعالج بعد في الواجهة الأمامية. باستخدام الخيار onError في خطاف useMutation، يمكن تسجيل دالة معالج أخطاء للـ mutations.
لنسجّل معالج أخطاء للـ mutation. يتلقى مكوّن PersonForm دالة setError كـ prop، تُستخدم لضبط رسالة تشير إلى الخطأ:
const PersonForm = ({ setError }) => {
// ...
const [ createPerson ] = useMutation(CREATE_PERSON, {
refetchQueries: [ {query: ALL_PERSONS } ],
onError: (error) => setError(error.message),
})
// ...
}
أنشئ مكوّناً منفصلاً للإشعار في الملف src/components/Notify.jsx:
const Notify = ({ errorMessage }) => {
if (!errorMessage) {
return null
}
return (
<div style={{ color: 'red' }}>
{errorMessage}
</div>
)
}
export default Notify
يتلقى المكوّن رسالة خطأ محتملة كـ prop. وإذا ضُبطت رسالة خطأ، تُعرض على الشاشة.
اعرض مكوّن Notify الذي يعرض رسالة الخطأ في الملف App.jsx:
import Notify from './components/Notify'
// ...
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>
)
}
الآن يُبلَّغ المستخدم بحدوث خطأ عبر إشعار بسيط.

تجد الشيفرة الحالية للتطبيق على GitHub في الفرع part8-3.
تحديث رقم هاتف
لنضف إلى تطبيقنا إمكانية تغيير أرقام هواتف الأشخاص. الحل شبه مطابق للحل الذي استخدمناه لإضافة أشخاص جدد.
تتطلب الـ mutation مرة أخرى استخدام المتغيرات. أضف الاستعلام التالي إلى الملف queries.js:
export const EDIT_NUMBER = gql`
mutation editNumber($name: String!, $phone: String!) {
editNumber(name: $name, phone: $phone) {
name
phone
address {
street
city
}
id
}
}
`
أنشئ مكوّناً جديداً PhoneForm في الملف src/components/PhoneForm.jsx لتحديث رقم هاتف. يضيف المكوّن نموذجاً إلى التطبيق حيث يمكنك إدخال رقم هاتف جديد لشخص محدد. الأجزاء المثيرة للاهتمام في الشيفرة مُبرَزة:
import { useState } from 'react'
import { useMutation } from '@apollo/client/react'
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
مكوّن PhoneForm مباشر: يطلب اسم الشخص ورقم هاتف جديداً عبر نموذج. وعند إرسال النموذج، يستدعي دالة changeNumber التي تتولى التحديث، والمُنشأة بخطاف useMutation.
فعّل المكوّن الجديد في الملف App.jsx:
import PhoneForm from './components/PhoneForm'
const App = () => {
// ...
return (
<div>
<Notify errorMessage={errorMessage} />
<Persons persons={result.data.allPersons} />
<PersonForm setError={notify} />
<PhoneForm setError={notify} />
</div>
)
}
يبدو قاتماً، لكنه يعمل:

من المثير للدهشة أنه عند تغيير رقم شخص، يظهر الرقم الجديد تلقائياً في قائمة الأشخاص التي يعرضها مكوّن Persons. يحدث هذا لأن لكل شخص حقلاً معرِّفاً من النوع ID، لذا تُحدَّث تفاصيل الشخص المحفوظة في الذاكرة المؤقتة تلقائياً عند تغييرها بالـ mutation.
لا يزال في تطبيقنا عيب صغير واحد. إذا حاولنا تغيير رقم هاتف لاسم غير موجود، لا يبدو أن شيئاً يحدث. يحدث هذا لأنه إذا تعذّر العثور على شخص بالاسم المعطى، تكون استجابة الـ mutation هي null:

بما أن GraphQL لا يعتبر هذا حالة خطأ، فلن يكون تسجيل معالج أخطاء onError مفيداً في هذه الحالة. لكن يمكننا إضافة دالة استدعاء راجعة onCompleted إلى خطاف useMutation، حيث يمكننا توليد رسالة خطأ محتملة:
const PhoneForm = ({ setError }) => {
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [changeNumber] = useMutation(EDIT_NUMBER, {
onCompleted: (data) => {
if (!data.editNumber) {
setError('person not found')
}
}
})
// ...
}
تُنفَّذ دالة الاستدعاء الراجعة onCompleted دائماً عند اكتمال الـ mutation بنجاح. وإذا لم يُعثر على الشخص—أي إذا كانت نتيجة الاستعلام data.editNumber قيمتها null—يستخدم المكوّن دالة الاستدعاء الراجعة setError التي تلقاها عبر props لضبط رسالة خطأ مناسبة.
تجد الشيفرة الحالية للتطبيق على GitHub في الفرع part8-4.
Apollo Client وحالة التطبيق
في مثالنا، أصبحت إدارة حالة التطبيق في معظمها من مسؤولية Apollo Client. هذا حل نموذجي تماماً لتطبيقات GraphQL. يستخدم مثالنا حالة مكوّنات React فقط لإدارة حالة نموذج ولعرض إشعارات الأخطاء. ونتيجة لذلك، قد لا توجد أسباب مبررة لاستخدام Redux لإدارة حالة التطبيق عند استخدام GraphQL.
عند الحاجة، يتيح Apollo حفظ الحالة المحلية للتطبيق في ذاكرة Apollo المؤقتة.
8. عرض المؤلفين
9. عرض الكتب
10. إضافة كتاب
11. سنة ميلاد المؤلفين
12. سنة ميلاد المؤلفين المتقدمة