سننجز في المرحلة القادمة تطبيق React باستخدام خادم GraphQL الذي أنشأناه سابقًا.
يمكنك أن تجد شيفرة الخادم ضمن الفرع part8-3 في المستودع المخصص على Github.
يمكن نظريًا استخدام GraphQL مع طلبات HTTP-POST. تظهر الصورة التالية مثالًا باستخدام Postman.
يعمل الاتصال بإرسال طلبات إلى العنوان، وسيكون الاستعلام نصًا مرسلًا كقيمة للمفتاح query
. كما يمكن التعامل مع الاتصال بين React-app و GraphQL باستخدام Axios. لكن لا يبدو هذا الخيار منطقيًا في معظم الأحيان. فمن الأفضل استخدام مكتبة ذات إمكانيات أعلى قادرة على إزالة التفاصيل غير الضرورية من الاتصال. ستجد حاليًا خيارين جيدين، أولهما المكتبة Relay من Facebook والمكتبة Apollo Client. وتُعَدّ Apollo الأكثر شعبية بينهما بلا منازع، لذلك سنستخدمها نحن أيضًا.
المكتبة Apollo client
سنستخدم في منهاجنا النسخة 3.0 بيتا التجريبية من هذه المكتبة. وتعتبر النسخة 3 حتى هذا التاريخ (12.12.2020) هي آخر نسخة رسمية. لذلك تذكر عند قراءة التوثيق أن تختار توثيق النسخة 3:
أنشئ تطبيق 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) })
تُطبع استجابة الخادم على الطرفية كالتالي:
يمكن للتطبيق أن يتواصل مع خادم 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. تُعطى المعاملات داخل متغيّرات الاستعلام:
يلائم استخدام الخطاف 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
قيمة، فستظهر على الشاشة تفاصيل شخص واحد بدلًا من قائمة الأشخاص:
عندما يريد المستخدم العودة إلى قائمة الأشخاص، يجب ضبط قيمة الحالة person
على null.ولا يعتبر هذا الحل هو الأفضل، لكنه جيد بما يكفي في حالتنا.
يمكنك إيجاد التطبيق بوضعه الحالي ضمن الفرع part8-1 في المستودع المخصص للتطبيق على Github.
الذاكرة المؤقتة
إن نفذّنا عدة استعلامات مثل الاستعلام عن تفاصيل عنوان Arto Hella، فسنلاحظ أمرًا مهمًا: سيُرسل الاستعلام إلى الواجهة الخلفية مرة واحدة فقط. بعد ذلك، وعلى الرغم من تنفيذ الاستعلام مرات عدة من قبل الشيفرة، فلن يُرسل الاستعلام إلى الواجهة الخلفية:
تُخزَّن المكتبة الاستجابات على الاستعلامات في الذاكرة المؤقتة cache. ولاستمثال الأداء، لن ترسل المكتبة الاستعلام إن وجدت استجابة مسبقة عليه ضمن الذاكرة المؤقتة.
يمكن تثبيت الأداة Apollo Client devtools على المتصفح Chrome لمتابعة حالة الذاكرة المؤقتة:
تُنظّم البيانات داخل الذاكرة المؤقتة عن طريق الاستعلام. وطالما أن الكائن 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.
التعامل مع أخطاء الطفرات
ستسبب محاولة إنشاء شخص جديد ببيانات غير صحيحة خطأً، وسينهار التطبيق ككل:
علينا اصطياد هذا الخطأ ومنع وقوعه. يمكن تعريف دالة معالج خطأ للطفرة باستخدام الخيار 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> )}
يُبلّغ المستخدم الآن عن الخطأ المرتكب من خلال تنبيه بسيط.
ستجد الشيفرة الحالية للتطبيق ضمن الفرع 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
لا يبدو المظهر مرضيًا لكن التطبيق يعمل:
وبشكل مفاجئ سيظهر الرقم الجديد للشخص تلقائيًا عند تغييره ضمن قائمة الأشخاص التي يصيرها المكوّن Persons
. ويحدث هذا لأن كل شخص يمتلك حقلًا مُعرِّفًا فريدًا من النوع ID. وهكذا سوف تُحدَّث بيانات الشخص المخزّنة في الذاكرة المؤقتة تلقائيًا عندما تغيّرها الطفرة.
ستجد الشيفرة الحالية للتطبيق ضمن الفرع part8-4 في المستودع المخصص للتطبيق على Github.
تبقى هناك ثغرة صغيرة في التطبيق. فلو أردنا تغيير رقم الهاتف لشخص غير موجود، يبدو أن لا شيء سيحدث. وذلك لأن شخصًا باسم غير موجود سيجعل الطفرة تعيد القيمة "null".
لا يعتبر هذا الأمر خطأً بالنسبة للمكتبة 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(() => { 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. واجهة عرض للمؤلفين
نفّذ واجهة تعرض تفاصيل جميع المؤلفين على الصفحة كما في الشكل التالي:
2. واجهة عرض للكتب
نفِّذ واجهة تعرض على الصفحة كل التفاصيل الأخرى للكتب ما عدا نوعها.
3. إضافة كتاب
أنجز آلية لإضافة كتب جديدة إلى تطبيقك. يمكن أن تبدو هذه الوظيفة بالشكل التالي:
تأكد من أنّ واجهتي عرض المؤلفين والكتب ستبقيان مُحدَّثتين بعد إضافة كتاب جديد.
تحقق عبر طرفية التطوير من استجابة الخادم في حال واجهتك المشاكل عند استخدام الاستعلامات أو الطفرات.
4. عام ولادة المؤلف
أنجز آلية لإضافة عام ولادة المؤلف. يمكنك إنشاء واجهة عرض جديدة لضبط عام الميلاد، أو يمكنك عرضها ضمن واجهة عرض المؤلفين:
تأكد من تحديث واجهة عرض المؤلفين بعد إضافة عام ولادة مؤلف.
5. أسلوب متقدم لإضافة عام ولادة مؤلف
عدّل نموذج إضافة عام الميلاد بحيث لا يمكن إضافته إلا لمؤلف موجود مسبقًا. استخدم المكتبتين select-tag، وreact-select أو أية آليات مناسبة أخرى.
سيبدو الحل الذي يستخدم React-select مشابهًا للشكل التالي:
ترجمة -وبتصرف- للفصل React and GraphQL من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.