اذهب إلى المحتوى

بناء تطبيق React باستعمال خادم GraphQL


ابراهيم الخضور

سننجز في المرحلة القادمة تطبيق React باستخدام خادم GraphQL الذي أنشأناه سابقًا.

يمكنك أن تجد شيفرة الخادم ضمن الفرع part8-3 في المستودع المخصص على Github.

يمكن نظريًا استخدام GraphQL مع طلبات HTTP-POST. تظهر الصورة التالية مثالًا باستخدام Postman.

graphql_http_post_with_postman_01.png

يعمل الاتصال بإرسال طلبات إلى العنوان، وسيكون الاستعلام نصًا مرسلًا كقيمة للمفتاح query. كما يمكن التعامل مع الاتصال بين React-app و GraphQL باستخدام Axios. لكن لا يبدو هذا الخيار منطقيًا في معظم الأحيان. فمن الأفضل استخدام مكتبة ذات إمكانيات أعلى قادرة على إزالة التفاصيل غير الضرورية من الاتصال. ستجد حاليًا خيارين جيدين، أولهما المكتبة Relay من Facebook والمكتبة Apollo Client. وتُعَدّ Apollo الأكثر شعبية بينهما بلا منازع، لذلك سنستخدمها نحن أيضًا.

المكتبة Apollo client

سنستخدم في منهاجنا النسخة 3.0 بيتا التجريبية من هذه المكتبة. وتعتبر النسخة 3 حتى هذا التاريخ (12.12.2020) هي آخر نسخة رسمية. لذلك تذكر عند قراءة التوثيق أن تختار توثيق النسخة 3:

apollo_client_v3_beta_02.png

أنشئ تطبيق React-app جديد وثبّت الاعتماديات التي تتطلبها المكتبة Apollo client. ويمكن القيام بذلك كالتالي:

npm install @apollo/client graphql

سنبدأ تطبيقنا بكتابة الشيفرة التالية:

import React from 'react'
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، ليُستخدم بعد ذلك لإرسال الاستعلام إلى الخادم:

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

تُطبع استجابة الخادم على الطرفية كالتالي:

server_response_console_03.png

يمكن للتطبيق أن يتواصل مع خادم GraphQL بالاستفادة من الكائن client. يمكن أن نجعل هذا الكائن متاحًا لجميع مكوِّنات التطبيق بتغليف المكوِّن الرئيسي App ضمن المكوِّن ApolloProvider.

import React from 'react'
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')
)

إنشاء الاستعلامات

نحن جاهزون الآن لإنجاز واجهة العرض الرئيسية للتطبيق، والتي تُظهر قائمة بأرقام الهواتف. تقدم المكتبة Apollo Client عدة بدائل لإنشاء الاستعلامات. وتُعَدّ الممارسة الأفضل حاليًا هي استخدام دالة الخطاف useQuery.

تُظهر الشيفرة التالية الاستعلام الذي ينشئه المكون App:

import React from 'react'
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

ينفّذ الخطاف hook الذي يُدعى useQuery، عندما يُستدعى، الاستعلام الذي يُمرَّر إليه كمعامل، ويعيد كائنا مؤلفًا من عدة حقول. سيحمل الحقل loading القيمة true إن لم يتلقى الاستعلام استجابة بعد. ثم ستُصيَّر الشيفرة التالية:

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

عندما يتلقى الاستعلام allPersons الاستجابة، يمكن الحصول على النتيجة من الحقل data، كما يمكن تصيير قائمة الأسماء على الشاشة:

<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}/>
  )
}

الاستعلامات والمتغيرات المسماة

لنضف وظيفة إظهار تفاصيل عنوان الشخص. سيفي الاستعلام 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 معاملًا. كما يمكن أن ننجز الاستعلامات مستخدمين أرضية عمل GraphQL. تُعطى المعاملات داخل متغيّرات الاستعلام:

query_variables_04.png

يلائم استخدام الخطاف useQuery الحالات التي يُنجز فيها الاستعلام عندما يُصيَّر المكوِّن. لكننا سننفذ الاستعلام الآن عندما يريد المستخدم أن يرى تفاصيل شخص معين فقط، أي سيُنفّذ الاستعلام عند الحاجة. في هذه الحالة ستكون دالة الخطاف useLazyQuery خيارًا جيدًا.

سيصبح المكوّن Persons كالتالي:

const FIND_PERSON = gql`
query findPersonByName($nameToSearch: String!)
{
findPerson(name: $nameToSearch)
{
name
phone 
id
address
{ 
street 
city
}
}
}`
const Persons = ({ persons }) => {
  const [getPerson, result] = useLazyQuery(FIND_PERSON)
  const [person, setPerson] = useState(null)
  const showPerson = (name) => {
      getPerson({ variables:
                 { nameToSearch: name }
                })
  }
  useEffect(() => {
      if (result.data) {
          setPerson(result.data.findPerson)
      }
  }, [result])
  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>
      {persons.map(p =>
        <div key={p.name}>
          {p.name} {p.phone}
          <button onClick={() => showPerson(p.name)} >
              show address
                  </button>
</div>  
      )}
    </div>
  )
}

export default Persons

لقد تغيّرت الشيفرة، ولا تبدو معظم التغيّرات واضحة.

فعندما ننقر على زر show address بجوار الشخص، سيُنفَّذ معالج الحدث showPerson الذي ينفِّذ بدوره استعلامًا لإحضار تفاصيل الأشخاص:

const [getPerson, result] = useLazyQuery(FIND_PERSON) 

// ...

const showPerson = (name) => {
  getPerson({ variables: { nameToSearch: name } })
}

يتلقى المتغيّر nameToSearch العائد للاستعلام قيمةً عندما يُنفَّذ هذا الاستعلام، وتُخزّن الاستجابة عليه في المتغيّر result، كما تُخزن قيمته في حالة المكوّن person في خطاف التأثير useEffect.

useEffect(() => {
  if (result.data) {
    setPerson(result.data.findPerson)
  }
}, [result])

يتلقى الخطاف المعامل الثاني result، وبالتالي ستُنفّذ الدالة التي تمرر إلى الخطاف كمعامل ثانٍ في كل مرة يحضر فيها الاستعلام تفاصيل شخص مختلف. ولو لم نتعامل مع التحديث بطريقة قابلة للإدارة باستخدام الخطاف كالسابق، ستسبب العودة من عرض شخص واحد إلى قائمة الأشخاص الكثير من المشاكل.

إن احتوت الحالة person قيمة، فستظهر على الشاشة تفاصيل شخص واحد بدلًا من قائمة الأشخاص:

state_value_exists_problem_05.png

عندما يريد المستخدم العودة إلى قائمة الأشخاص، يجب ضبط قيمة الحالة person على null.ولا يعتبر هذا الحل هو الأفضل، لكنه جيد بما يكفي في حالتنا.

يمكنك إيجاد التطبيق بوضعه الحالي ضمن الفرع part8-1 في المستودع المخصص للتطبيق على Github.

الذاكرة المؤقتة

إن نفذّنا عدة استعلامات مثل الاستعلام عن تفاصيل عنوان Arto Hella، فسنلاحظ أمرًا مهمًا: سيُرسل الاستعلام إلى الواجهة الخلفية مرة واحدة فقط. بعد ذلك، وعلى الرغم من تنفيذ الاستعلام مرات عدة من قبل الشيفرة، فلن يُرسل الاستعلام إلى الواجهة الخلفية:

same_qurey_sent_once_06.png

تُخزَّن المكتبة الاستجابات على الاستعلامات في الذاكرة المؤقتة cache. ولاستمثال الأداء، لن ترسل المكتبة الاستعلام إن وجدت استجابة مسبقة عليه ضمن الذاكرة المؤقتة.

يمكن تثبيت الأداة Apollo Client devtools على المتصفح Chrome لمتابعة حالة الذاكرة المؤقتة:

cache_state_devtools_07.png

تُنظّم البيانات داخل الذاكرة المؤقتة عن طريق الاستعلام. وطالما أن الكائن Person سيمتلك حقلًا يحتوي على معرّف فريد id من النوع "ID"، فإن أعيد نفس الكائن كاستجابة لعدة استعلامات، يمكن أن تدمجها المكتبة Apollo ضمن كائن واحد. وهكذا فإن تنفيذ الاستعلام findPerson للحصول على تفاصيل "Arto Hellas" سيٌحدّث تفاصيل العنوان حتى من أجل الاستعلام allPersons.

تنفيذ الطفرات

سنضيف الآن وظيفة إنشاء أشخاص جدد.

لقد كتبنا في الفصل السابق المعاملات من أجل الطفرات mutations يدويًا. سنحتاج الآن نسخة من الطفرة addPeson تستخدم المتغيرات:

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 React, { 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. سيعيد الخطاف مصفوفة يحوي عنصرها الأول الدالة التي ستسبب الطفرة.

const [ createPerson ] = useMutation(CREATE_PERSON)

سيتلقى متغير الاستعلام قيمًا عند تنفيذ الاستعلام:

createPerson({  variables: { name, phone, street, city } })

سيُضاف الشخص الجديد كما هو مطلوب، لكن الشاشة لن تُحدّث ما يعرض عليها. والسبب في ذلك أن Apollo client لن تحدّث تلقائيًا الذاكرة المؤقتة للتطبيق، وستبقى محتفظة بالحالة التي خزنتها الطفرة السابقة. يمكن تحديث الشاشة بإعادة تحميل الصفحة، حيث يتم تفريغ الذاكرة المؤقتة عندما نفعل ذلك. لكن لابدّ من وجود طريقة أفضل لتحديث الشاشة.

تحديث محتويات الذاكرة المؤقتة

هناك حلول عدة لإنجاز ذلك. تتمثل إحدى الحلول بحثِّ 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

إن الحل المقدم بسيط، وسيظهر أي شخص جديد يضاف إلى القائمة على الشاشة التي تعرض كل الأشخاص مباشرة. لكن تتمثل الناحية السلبية لهذا الحل بانتقال بيانات بلا فائدة عبر الشبكة.

الطريقة السهلة الأخرى لإبقاء الذاكرة المؤقتة متزامنة مع التحديثات التي تجري هي استخدام المعامل refetchQueries لدالة الخطاف useMutation والذي يحدد أنّ الاستعلام سيحضر تفاصيل كل الأشخاص من جديد في كل مرة يُضاف فيها شخص جديد.

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

const PersonForm = (props) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    refetchQueries: [ { query: ALL_PERSONS } ]  })

إن حسنات وسيئات هذا الحل هي على نقيض الحل السابق، إذ لا يوجد هنا انتقال بيانات زائد عبر الشبكة، فلا تُنفَّذ الاستعلامات بشكل دوري لالتقاط أي تغيير في حال حدوثه. لكن إن حدَّث مستخدم حالة الخادم، فلن تظهر التغييرات مباشرة لبقية المستخدمين.

لا تزال هناك طرق أخرى لتحديث الذاكرة المؤقتة وسنستعرضها لاحقًا في هذا القسم.

تتواجد حاليًا في تطبيقنا شيفرة الاستعلامات وشيفرة المكوّنات في نفس المكان، لنفصل إذًا شيفرة الاستعلامات ونضعها في الملف الجديد "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)
  // ...
}

ستجد الشيفرة الحالية للتطبيق ضمن الفرع part8-2 في المستودع المخصص للتطبيق على Github.

التعامل مع أخطاء الطفرات

ستسبب محاولة إنشاء شخص جديد ببيانات غير صحيحة خطأً، وسينهار التطبيق ككل:

app_fail_invalid_data_08.png

علينا اصطياد هذا الخطأ ومنع وقوعه. يمكن تعريف دالة معالج خطأ للطفرة باستخدام الخيار onError للخطاف useMutaion.

لنعرّف معالج خطأ للطفرة يستخدم الدالة setEroor التي تُمرّر إليه كمعامل لتهيئة رسالة خطأ:

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>  )}

يُبلّغ المستخدم الآن عن الخطأ المرتكب من خلال تنبيه بسيط.

error_notification_09.png

ستجد الشيفرة الحالية للتطبيق ضمن الفرع part8-3 في المستودع المخصص للتطبيق على Github.

تحديث رقم الهاتف

لنضف إمكانية تغيير رقم هاتف الشخص إلى التطبيق. سيكون الحل مطابقًا تقريبًا لعملية إضافة شخص جديد.

تحتاج الطفرة مرة أخرى إلى معاملات.

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

إن آلية عمل المكوّن PhoneForm المسؤول عن التغيير مباشرة. إذ يمتلك النموذج حقولًا لاسم الشخص ورقم الهاتف الجديد ويستدعي بعدها الدالة changeNumber. تُنفَّذ الدالة باستخدام الخطاف useMutaion.

import React, { 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

لا يبدو المظهر مرضيًا لكن التطبيق يعمل:

person_number_change_10.png

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

ستجد الشيفرة الحالية للتطبيق ضمن الفرع part8-4 في المستودع المخصص للتطبيق على Github.

تبقى هناك ثغرة صغيرة في التطبيق. فلو أردنا تغيير رقم الهاتف لشخص غير موجود، يبدو أن لا شيء سيحدث. وذلك لأن شخصًا باسم غير موجود سيجعل الطفرة تعيد القيمة "null".

mutation_responded_null_11.png

لا يعتبر هذا الأمر خطأً بالنسبة للمكتبة GraphQL، وبالتالي لا فائدة من تعريف معالج خطأ في هذه الحالة.

يمكن الاستفادة من الحقل 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، سيستخدم المكوِّن دالة الاستدعاء التي يتلقاها كخاصية لإنشاء رسالة خطأ مناسبة. نريد في التطبيق أن نضبط رسالة الخطأ عندما تتغير نتيجة الطفرة result.data فقط. لذلك نستخدم خطاف التأثير useEffect للتحكم بإنشاء رسالة الخطأ.

سيسبب استخدام الخطاف useEffect تحذيرًا يطلقه المدقق ESlint:

eslint_useeffect_warning_12.png

لا فائدة حقيقية من هذا التحذير و سيكون الحل الأفضل هو تجاهل قاعدة المدقق ESlint في السطر الذي سيولد التحذير:

useEffect(() => {
  if (result.data && !result.data.editNumber) {
    setError('name not found')
  }
}, [result.data])  // eslint-disable-line 

يمكننا أيضًا التخلص من هذا التحذير بإضافة الدالة setError إلى مصفوفة المعامل الثاني للخطاف useEffect:

useEffect(() => {
  if (result.data && !result.data.editNumber) {
    setError('name not found')
  }
}, [result.data, setError])

لن يعمل هذا الحل بالطبع إن كانت الدالة notify غير مضمنة داخل دالة الخطاف useCallback. وإن لم تكن كذلك ستكون النتيجة حلقة فارغة لانهائية. عندما يُصيَّر المكوِن App بعد إزالة التنبيه، سينشأ تنبيه جديد مسببًا إعادة تنفيذ دالة خطاف التأثير، والتي ستولد بدورها تنبيهًا جديدًا وهكذا تستمر الحلقة.

ستجد الشيفرة الحالية للتطبيق ضمن الفرع part8-5 في المستودع المخصص للتطبيق على Github.

حالة التطبيق عند استخدام المكتبة Apollo client

لاحظنا في التطبيق السابق، أنّ إدارة الحالة كانت مسؤولية المكتبة Apollo client. وهذا حل نموذجي لتطبيقات GraphQL. وقد استخدم تطبيقنا حالة مكوِّنات React لإدارة حالة النماذج فقط ولإظهار تنبيهات الأخطاء. فعند استخدامك للمكتبة GraphQL، لا توجد مبررات مقبولة لنقل إدارة حالة التطبيقات إلى Redux إطلاقًا.

وعند الحاجة ستخزِّن المكتبة Apollo حالة التطبيق المحليّة في ذاكرتها المؤقتة.

التمرينات

سننجز خلال التمرينات القادمة واجهة أمامية للمكتبة GraphQl. استخدم المشروع المتعلق بالتمارين كنقطة انطلاق لتطبيقك.

يمكنك إنجاز المطلوب باستخدام المكونين Query وMutation القابلين للتصيير في المكتبة Apollo client، كما يمكن استخدام الخطافات التي تؤمنها النسخة Beta 3.0.

1. واجهة عرض للمؤلفين

نفّذ واجهة تعرض تفاصيل جميع المؤلفين على الصفحة كما في الشكل التالي:

authors_view_13.png

2. واجهة عرض للكتب

نفِّذ واجهة تعرض على الصفحة كل التفاصيل الأخرى للكتب ما عدا نوعها.

books-view_14.png

3. إضافة كتاب

أنجز آلية لإضافة كتب جديدة إلى تطبيقك. يمكن أن تبدو هذه الوظيفة بالشكل التالي:

add_new _book_15.png

تأكد من أنّ واجهتي عرض المؤلفين والكتب ستبقيان مُحدَّثتين بعد إضافة كتاب جديد.

تحقق عبر طرفية التطوير من استجابة الخادم في حال واجهتك المشاكل عند استخدام الاستعلامات أو الطفرات.

developers_console_server_response_check_16.png

4. عام ولادة المؤلف

أنجز آلية لإضافة عام ولادة المؤلف. يمكنك إنشاء واجهة عرض جديدة لضبط عام الميلاد، أو يمكنك عرضها ضمن واجهة عرض المؤلفين:

set_authors_birth_year_17.png

تأكد من تحديث واجهة عرض المؤلفين بعد إضافة عام ولادة مؤلف.

5. أسلوب متقدم لإضافة عام ولادة مؤلف

عدّل نموذج إضافة عام الميلاد بحيث لا يمكن إضافته إلا لمؤلف موجود مسبقًا. استخدم المكتبتين select-tag، وreact-select أو أية آليات مناسبة أخرى.

سيبدو الحل الذي يستخدم React-select مشابهًا للشكل التالي:

birthyear_using_react_select_18.png

ترجمة -وبتصرف- للفصل React and GraphQL من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...