logo by @sawaratsuki1004
React
v19.2
تعلم
مرجع
المجتمع
المدونة

هل هذه الصفحة مفيدة؟

في هذه الصفحة

  • Overview
  • مبادئ بُنية state
  • اجمع state المرتبطة
  • تجنب التناقضات في state
  • تجنب state الزائدة
  • تجنب التكرار في state
  • تجنب state المتداخلة بعمق
  • Recap
  • Challenges

    البدأ

  • بداية سريعة
    • شرح تطبيقي: لعبة تيك تاك تو
    • التفكير على طريقة React
  • التثبيت
    • إنشاء تطبيق React
    • بناء تطبيق React من الصفر
    • إضافة React إلى مشروع موجود بالفعل
  • الإعداد
    • تجهيز المحرر
    • استخدام TypeScript
    • أدوات مطوري React
  • React Compiler
    • مقدمة
    • التثبيت
    • التبني التدريجي
    • تصحيح الأخطاء واستكشاف المشاكل
  • تعلم React

  • وصف واجهة المستخدم (UI)
    • مكونك الأول (Component)
    • استيراد وتصدير المكونات (Components)
    • كتابة ترميز البناء بـ JSX
    • JavaScript في JSX باستخدام الأقواس المنحنية
    • تمرير الخصائص (Props) إلى مكون
    • التصيير الشرطي (Conditional Rendering)
    • تصيير القوائم (Rendering Lists)
    • الحفاظ على نقاء المكونات (Pure Components)
    • واجهتك المستخدم كشجرة (UI Tree)
  • إضافة التفاعلية (Interactivity)
    • الاستجابة للأحداث (Events)
    • الحالة (State): ذاكرة المُكَوِّن
    • التصيير والالتزام (Render and Commit)
    • الحالة (State) كلقطة
    • إضافة سلسلة من تحديثات الحالة (State) إلى قائمة انتظار
    • تحديث الكائنات (Objects) في الحالة
    • تحديث المصفوفات (Arrays) في الحالة
  • إدارة State
    • التفاعل مع Input باستخدام State
    • اختيار بنية State
    • مشاركة State بين Components
    • الحفاظ على State وإعادة ضبطها
    • استخراج منطق State إلى Reducer
    • تمرير البيانات بشكل عميق باستخدام Context
    • التوسع باستخدام Reducer و Context
  • مخارج الطوارئ (Escape Hatches)
    • الإشارة إلى القيم باستخدام Refs
    • التلاعب بـ DOM باستخدام Refs
    • التزامن مع Effects
    • قد لا تحتاج إلى Effect
    • دورة حياة Reactive Effects
    • فصل Events عن Effects
    • إزالة اعتماديات Effect
    • إعادة استخدام المنطق باستخدام Custom Hooks
تعلم React
إدارة State

اختيار بُنية State

بُنية state الجيدة يمكن أن تُحدث فرقًا بين مكون يسهل تعديله وتنقيح أخطائه، ومكون يكون مصدرًا دائمًا للأخطاء. إليك بعض النصائح التي يجب عليك مراعاتها عند بُنية state.

You will learn

  • متى تستخدم متغير state واحد مقابل متغيرات state متعددة
  • ما يجب تجنبه عند تنظيم state
  • كيفية إصلاح المشاكل الشائعة مع بُنية state

مبادئ بُنية state

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

  1. اجمع state المرتبطة. إذا كنت تُحدّث دائمًا متغيري state أو أكثر في نفس الوقت، ففكر في دمجهما في متغير state واحد.
  2. تجنب التناقضات في state. عندما يتم بُنية state بطريقة قد تتناقض فيها عدة أجزاء من state وتكون “غير متفقة” مع بعضها البعض، فإنك تترك مجالًا للأخطاء. حاول تجنب ذلك.
  3. تجنب state الزائدة. إذا كان بإمكانك حساب بعض المعلومات من props المكون أو من متغيرات state الموجودة أثناء rendering، فلا يجب عليك وضع تلك المعلومات في state ذلك المكون.
  4. تجنب التكرار في state. عندما يتم تكرار نفس البيانات بين متغيرات state متعددة، أو ضمن objects متداخلة، يكون من الصعب الحفاظ على تزامنها. قلل من التكرار عندما تستطيع.
  5. تجنب state المتداخلة بعمق. state الهرمية العميقة ليست مريحة جدًا للتحديث. عندما يكون ذلك ممكنًا، فضّل بُنية state بطريقة مسطحة.

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

الآن دعنا نرى كيف تنطبق هذه المبادئ عمليًا.

اجمع state المرتبطة

قد تكون في بعض الأحيان غير متأكد بين استخدام متغير state واحد أو متغيرات state متعددة.

هل يجب عليك فعل هذا؟

const [x, setX] = useState(0); const [y, setY] = useState(0);

أم هذا؟

const [position, setPosition] = useState({ x: 0, y: 0 });

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

import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) }

حالة أخرى حيث ستجمع البيانات في object أو array هي عندما لا تعرف عدد أجزاء state التي ستحتاجها. على سبيل المثال، يكون ذلك مفيدًا عندما يكون لديك نموذج حيث يمكن للمستخدم إضافة حقول مخصصة.

مأزق

إذا كان متغير state الخاص بك هو object، تذكر أنه لا يمكنك تحديث حقل واحد فقط فيه دون نسخ الحقول الأخرى صراحةً. على سبيل المثال، لا يمكنك فعل setPosition({ x: 100 }) في المثال أعلاه لأنه لن يكون له خاصية y على الإطلاق! بدلاً من ذلك، إذا كنت تريد تعيين x بمفرده، فستقوم إما بعمل setPosition({ ...position, x: 100 })، أو تقسيمهما إلى متغيري state وفعل setX(100).

تجنب التناقضات في state

إليك نموذج ملاحظات فندق مع متغيري state isSending و isSent:

import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Thanks for feedback!</h1> } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Send </button> {isSending && <p>Sending...</p>} </form> ); } // Pretend to send a message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }

While this code works, it leaves the door open for “impossible” states. For example, if you forget to call setIsSent and setIsSending together, you may end up in a situation where both isSending and isSent are true at the same time. The more complex your component is, the harder it is to understand what happened.

على الرغم من أن هذا الكود يعمل، إلا أنه يترك الباب مفتوحًا لحالات “مستحيلة”. على سبيل المثال، إذا نسيت استدعاء setIsSent و setIsSending معًا، فقد تنتهي في موقف حيث يكون كل من isSending و isSent true في نفس الوقت. كلما كان مكونك أكثر تعقيدًا، كان من الصعب فهم ما حدث.

Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable that may take one of three valid states: 'typing' (initial), 'sending', and 'sent':

نظرًا لأن isSending و isSent يجب ألا يكونا true في نفس الوقت أبدًا، فمن الأفضل استبدالهما بمتغير state واحد status يمكن أن يأخذ واحدة من ثلاث حالات صالحة: 'typing' (الأولية)، 'sending'، و 'sent':

import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>Thanks for feedback!</h1> } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Send </button> {isSending && <p>Sending...</p>} </form> ); } // Pretend to send a message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }

You can still declare some constants for readability:

const isSending = status === 'sending'; const isSent = status === 'sent';

But they’re not state variables, so you don’t need to worry about them getting out of sync with each other.

لا يزال بإمكانك الإعلان عن بعض الثوابت من أجل قابلية القراءة:

const isSending = status === 'sending'; const isSent = status === 'sent';

لكنها ليست متغيرات state، لذا لا داعي للقلق بشأن خروجها عن التزامن مع بعضها البعض.

تجنب state الزائدة

إذا كان بإمكانك حساب بعض المعلومات من props المكون أو من متغيرات state الموجودة أثناء rendering، فـيجب ألا تضع تلك المعلومات في state ذلك المكون.

على سبيل المثال، خذ هذا النموذج. إنه يعمل، ولكن هل يمكنك العثور على أي state زائدة فيه؟

import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }

This form has three state variables: firstName, lastName, and fullName. However, fullName is redundant. You can always calculate fullName from firstName and lastName during render, so remove it from state.

This is how you can do it:

هذا النموذج يحتوي على ثلاثة متغيرات state: firstName، lastName، و fullName. ومع ذلك، fullName زائدة. يمكنك دائمًا حساب fullName من firstName و lastName أثناء render، لذا أزله من state.

هكذا يمكنك فعل ذلك:

import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }

Here, fullName is not a state variable. Instead, it’s calculated during render:

const fullName = firstName + ' ' + lastName;

As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName or setLastName, you trigger a re-render, and then the next fullName will be calculated from the fresh data.

هنا، fullName ليس متغير state. بدلاً من ذلك، يتم حسابه أثناء render:

const fullName = firstName + ' ' + lastName;

ونتيجة لذلك، لا تحتاج معالجات التغيير إلى فعل أي شيء خاص لتحديثه. عندما تستدعي setFirstName أو setLastName، فإنك تشغل re-render، وبعد ذلك سيتم حساب fullName التالي من البيانات الجديدة.

غوص عميق

لا تنسخ props في state

مثال شائع على state الزائدة هو كود مثل هذا:

function Message({ messageColor }) { const [color, setColor] = useState(messageColor);

Here, a color state variable is initialized to the messageColor prop. The problem is that if the parent component passes a different value of messageColor later (for example, 'red' instead of 'blue'), the color state variable would not be updated! The state is only initialized during the first render.

This is why “mirroring” some prop in a state variable can lead to confusion. Instead, use the messageColor prop directly in your code. If you want to give it a shorter name, use a constant:

هنا، يتم تهيئة متغير state color بـ prop messageColor. المشكلة هي أنه إذا مرر المكون الأصلي قيمة مختلفة لـ messageColor لاحقًا (على سبيل المثال، 'red' بدلاً من 'blue')، فلن يتم تحديث متغير state color! يتم تهيئة state فقط أثناء render الأول.

هذا هو السبب في أن “نسخ” prop معين في متغير state يمكن أن يؤدي إلى الارتباك. بدلاً من ذلك، استخدم prop messageColor مباشرة في الكود الخاص بك. إذا كنت تريد إعطائه اسمًا أقصر، فاستخدم ثابتًا:

function Message({ messageColor }) { const color = messageColor;

This way it won’t get out of sync with the prop passed from the parent component.

”Mirroring” props into state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with initial or default to clarify that its new values are ignored:

بهذه الطريقة لن يخرج عن التزامن مع prop الممرر من المكون الأصلي.

”نسخ” props إلى state له معنى فقط عندما تريد تجاهل كل التحديثات لـ prop معين. بحسب الاتفاقية، ابدأ اسم prop بـ initial أو default لتوضيح أن قيمه الجديدة تم تجاهلها:

function Message({ initialColor }) { // The `color` state variable holds the *first* value of `initialColor`. // Further changes to the `initialColor` prop are ignored. const [color, setColor] = useState(initialColor);

تجنب التكرار في state

تتيح قائمة القائمة هذه اختيار وجبة خفيفة واحدة من عدة خيارات للسفر:

import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); return ( <> <h2>What's your travel snack?</h2> <ul> {items.map(item => ( <li key={item.id}> {item.title} {' '} <button onClick={() => { setSelectedItem(item); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }

Currently, it stores the selected item as an object in the selectedItem state variable. However, this is not great: the contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places.

Why is this a problem? Let’s make each item editable:

حاليًا، تخزن العنصر المحدد ككائن في متغير state selectedItem. ومع ذلك، هذا ليس جيدًا: محتويات selectedItem هي نفس الكائن كأحد العناصر داخل قائمة items. هذا يعني أن المعلومات حول العنصر نفسه مكررة في مكانين.

لماذا هذه مشكلة؟ دعنا نجعل كل عنصر قابلاً للتحرير:

import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedItem(item); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }

Notice how if you first click “Choose” on an item and then edit it, the input updates but the label at the bottom does not reflect the edits. This is because you have duplicated state, and you forgot to update selectedItem.

Although you could update selectedItem too, an easier fix is to remove duplication. In this example, instead of a selectedItem object (which creates a duplication with objects inside items), you hold the selectedId in state, and then get the selectedItem by searching the items array for an item with that ID:

لاحظ كيف أنه إذا نقرت أولاً على “Choose” على عنصر ثم قمت بتحريره، فإن الإدخال يتحدث لكن التسمية في الأسفل لا تعكس التعديلات. هذا لأن لديك state مكررة، ونسيت تحديث selectedItem.

على الرغم من أنه يمكنك تحديث selectedItem أيضًا، إلا أن الإصلاح الأسهل هو إزالة التكرار. في هذا المثال، بدلاً من كائن selectedItem (الذي ينشئ تكرارًا مع الكائنات داخل items)، فإنك تحتفظ بـ selectedId في state، ثم تحصل على selectedItem بالبحث في مصفوفة items عن عنصر بذلك ID:

import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find(item => item.id === selectedId ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedId(item.id); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }

The state used to be duplicated like this:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

But after the change it’s like this:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

The duplication is gone, and you only keep the essential state!

Now if you edit the selected item, the message below will update immediately. This is because setItems triggers a re-render, and items.find(...) would find the item with the updated title. You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render.

كانت state مكررة مثل هذا:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

لكن بعد التغيير أصبحت مثل هذا:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

التكرار اختفى، وأنت تحتفظ فقط بـ state الأساسية!

الآن إذا قمت بتحرير العنصر المحدد، ستتحدث الرسالة أدناه فورًا. هذا لأن setItems تشغل re-render، و items.find(...) ستجد العنصر بالعنوان المحدث. لم تكن بحاجة للاحتفاظ بـالعنصر المحدد في state، لأن فقط ID المحدد أساسي. الباقي يمكن حسابه أثناء render.

تجنب state المتداخلة بعمق

تخيل خطة سفر تتكون من كواكب وقارات ودول. قد تميل إلى بُنية state باستخدام objects ومصفوفات متداخلة، كما في هذا المثال:

export const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [{ id: 1, title: 'Earth', childPlaces: [{ id: 2, title: 'Africa', childPlaces: [{ id: 3, title: 'Botswana', childPlaces: [] }, { id: 4, title: 'Egypt', childPlaces: [] }, { id: 5, title: 'Kenya', childPlaces: [] }, { id: 6, title: 'Madagascar', childPlaces: [] }, { id: 7, title: 'Morocco', childPlaces: [] }, { id: 8, title: 'Nigeria', childPlaces: [] }, { id: 9, title: 'South Africa', childPlaces: [] }] }, { id: 10, title: 'Americas', childPlaces: [{ id: 11, title: 'Argentina', childPlaces: [] }, { id: 12, title: 'Brazil', childPlaces: [] }, { id: 13, title: 'Barbados', childPlaces: [] }, { id: 14, title: 'Canada', childPlaces: [] }, { id: 15, title: 'Jamaica', childPlaces: [] }, { id: 16, title: 'Mexico', childPlaces: [] }, { id: 17, title: 'Trinidad and Tobago', childPlaces: [] }, { id: 18, title: 'Venezuela', childPlaces: [] }] }, { id: 19, title: 'Asia', childPlaces: [{ id: 20, title: 'China', childPlaces: [] }, { id: 21, title: 'India', childPlaces: [] }, { id: 22, title: 'Singapore', childPlaces: [] }, { id: 23, title: 'South Korea', childPlaces: [] }, { id: 24, title: 'Thailand', childPlaces: [] }, { id: 25, title: 'Vietnam', childPlaces: [] }] }, { id: 26, title: 'Europe', childPlaces: [{ id: 27, title: 'Croatia', childPlaces: [], }, { id: 28, title: 'France', childPlaces: [], }, { id: 29, title: 'Germany', childPlaces: [], }, { id: 30, title: 'Italy', childPlaces: [], }, { id: 31, title: 'Portugal', childPlaces: [], }, { id: 32, title: 'Spain', childPlaces: [], }, { id: 33, title: 'Turkey', childPlaces: [], }] }, { id: 34, title: 'Oceania', childPlaces: [{ id: 35, title: 'Australia', childPlaces: [], }, { id: 36, title: 'Bora Bora (French Polynesia)', childPlaces: [], }, { id: 37, title: 'Easter Island (Chile)', childPlaces: [], }, { id: 38, title: 'Fiji', childPlaces: [], }, { id: 39, title: 'Hawaii (the USA)', childPlaces: [], }, { id: 40, title: 'New Zealand', childPlaces: [], }, { id: 41, title: 'Vanuatu', childPlaces: [], }] }] }, { id: 42, title: 'Moon', childPlaces: [{ id: 43, title: 'Rheita', childPlaces: [] }, { id: 44, title: 'Piccolomini', childPlaces: [] }, { id: 45, title: 'Tycho', childPlaces: [] }] }, { id: 46, title: 'Mars', childPlaces: [{ id: 47, title: 'Corn Town', childPlaces: [] }, { id: 48, title: 'Green Hill', childPlaces: [] }] }] };

Now let’s say you want to add a button to delete a place you’ve already visited. How would you go about it? Updating nested state involves making copies of objects all the way up from the part that changed. Deleting a deeply nested place would involve copying its entire parent place chain. Such code can be very verbose.

If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each place has an array of its child places, you can have each place hold an array of its child place IDs. Then store a mapping from each place ID to the corresponding place.

This data restructuring might remind you of seeing a database table:

الآن لنفترض أنك تريد إضافة زر لحذف مكان قد زرته بالفعل. كيف ستفعل ذلك؟ تحديث state المتداخلة يتضمن عمل نسخ من objects طوال الطريق من الجزء الذي تغير. حذف مكان متداخل بعمق سيتضمن نسخ سلسلة الأماكن الأصلية بأكملها. يمكن أن يكون مثل هذا الكود طويلاً جدًا.

إذا كانت state متداخلة جدًا للتحديث بسهولة، ففكر في جعلها “مسطحة”. إليك طريقة واحدة يمكنك إعادة تنظيم هذه البيانات بها. بدلاً من بُنية شبيهة بالشجرة حيث كل place له مصفوفة من أماكنه الفرعية، يمكنك جعل كل مكان يحتوي على مصفوفة من IDs أماكنه الفرعية. ثم احفظ تعيينًا من كل ID مكان إلى المكان المقابل.

قد تذكرك إعادة هيكلة البيانات هذه برؤية جدول قاعدة بيانات:

export const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 42, 46], }, 1: { id: 1, title: 'Earth', childIds: [2, 10, 19, 26, 34] }, 2: { id: 2, title: 'Africa', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'Botswana', childIds: [] }, 4: { id: 4, title: 'Egypt', childIds: [] }, 5: { id: 5, title: 'Kenya', childIds: [] }, 6: { id: 6, title: 'Madagascar', childIds: [] }, 7: { id: 7, title: 'Morocco', childIds: [] }, 8: { id: 8, title: 'Nigeria', childIds: [] }, 9: { id: 9, title: 'South Africa', childIds: [] }, 10: { id: 10, title: 'Americas', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'Argentina', childIds: [] }, 12: { id: 12, title: 'Brazil', childIds: [] }, 13: { id: 13, title: 'Barbados', childIds: [] }, 14: { id: 14, title: 'Canada', childIds: [] }, 15: { id: 15, title: 'Jamaica', childIds: [] }, 16: { id: 16, title: 'Mexico', childIds: [] }, 17: { id: 17, title: 'Trinidad and Tobago', childIds: [] }, 18: { id: 18, title: 'Venezuela', childIds: [] }, 19: { id: 19, title: 'Asia', childIds: [20, 21, 22, 23, 24, 25], }, 20: { id: 20, title: 'China', childIds: [] }, 21: { id: 21, title: 'India', childIds: [] }, 22: { id: 22, title: 'Singapore', childIds: [] }, 23: { id: 23, title: 'South Korea', childIds: [] }, 24: { id: 24, title: 'Thailand', childIds: [] }, 25: { id: 25, title: 'Vietnam', childIds: [] }, 26: { id: 26, title: 'Europe', childIds: [27, 28, 29, 30, 31, 32, 33], }, 27: { id: 27, title: 'Croatia', childIds: [] }, 28: { id: 28, title: 'France', childIds: [] }, 29: { id: 29, title: 'Germany', childIds: [] }, 30: { id: 30, title: 'Italy', childIds: [] }, 31: { id: 31, title: 'Portugal', childIds: [] }, 32: { id: 32, title: 'Spain', childIds: [] }, 33: { id: 33, title: 'Turkey', childIds: [] }, 34: { id: 34, title: 'Oceania', childIds: [35, 36, 37, 38, 39, 40, 41], }, 35: { id: 35, title: 'Australia', childIds: [] }, 36: { id: 36, title: 'Bora Bora (French Polynesia)', childIds: [] }, 37: { id: 37, title: 'Easter Island (Chile)', childIds: [] }, 38: { id: 38, title: 'Fiji', childIds: [] }, 39: { id: 40, title: 'Hawaii (the USA)', childIds: [] }, 40: { id: 40, title: 'New Zealand', childIds: [] }, 41: { id: 41, title: 'Vanuatu', childIds: [] }, 42: { id: 42, title: 'Moon', childIds: [43, 44, 45] }, 43: { id: 43, title: 'Rheita', childIds: [] }, 44: { id: 44, title: 'Piccolomini', childIds: [] }, 45: { id: 45, title: 'Tycho', childIds: [] }, 46: { id: 46, title: 'Mars', childIds: [47, 48] }, 47: { id: 47, title: 'Corn Town', childIds: [] }, 48: { id: 48, title: 'Green Hill', childIds: [] } };

Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier.

In order to remove a place now, you only need to update two levels of state:

  • The updated version of its parent place should exclude the removed ID from its childIds array.
  • The updated version of the root “table” object should include the updated version of the parent place.

Here is an example of how you could go about it:

الآن بعد أن أصبحت state “مسطحة” (تُعرف أيضًا باسم “منظمة”)، يصبح تحديث العناصر المتداخلة أسهل.

لإزالة مكان الآن، تحتاج فقط إلى تحديث مستويين من state:

  • النسخة المحدثة من مكان الأصل يجب أن تستبعد ID المحذوف من مصفوفة childIds الخاصة به.
  • النسخة المحدثة من كائن “الجدول” الجذر يجب أن تتضمن النسخة المحدثة من المكان الأصل.

إليك مثال على كيفية القيام بذلك:

import { useState } from 'react'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // Create a new version of the parent place // that doesn't include this child ID. const nextParent = { ...parent, childIds: parent.childIds .filter(id => id !== childId) }; // Update the root state object... setPlan({ ...plan, // ...so that it has the updated parent. [parentId]: nextParent }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Complete </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }

You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.

غوص عميق

تحسين استخدام الذاكرة

بشكل مثالي، يجب عليك أيضًا إزالة العناصر المحذوفة (وأطفالها!) من كائن “الجدول” لتحسين استخدام الذاكرة. هذه النسخة تفعل ذلك. كما أنها تستخدم Immer لجعل منطق التحديث أكثر اختصارًا.

import { useImmer } from 'use-immer'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, updatePlan] = useImmer(initialTravelPlan); function handleComplete(parentId, childId) { updatePlan(draft => { // Remove from the parent place's child IDs. const parent = draft[parentId]; parent.childIds = parent.childIds .filter(id => id !== childId); // Forget this place and all its subtree. deleteAllChildren(childId); function deleteAllChildren(id) { const place = draft[id]; place.childIds.forEach(deleteAllChildren); delete draft[id]; } }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Complete </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }

Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered.

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

خلاصة

  • إذا كان متغيران state يتحدثان دائمًا معًا، ففكر في دمجهما في متغير واحد.
  • اختر متغيرات state الخاصة بك بعناية لتجنب إنشاء حالات “مستحيلة”.
  • قم ببُنية state الخاصة بك بطريقة تقلل من فرص ارتكاب خطأ في تحديثها.
  • تجنب state الزائدة والمكررة حتى لا تضطر إلى الحفاظ على تزامنها.
  • لا تضع props في state إلا إذا كنت تريد على وجه التحديد منع التحديثات.
  • لنماذج واجهة المستخدم مثل التحديد، احفظ ID أو الفهرس في state بدلاً من الكائن نفسه.
  • إذا كان تحديث state المتداخلة بعمق معقدًا، فحاول تسطيحها.

جرّب بعض التحديات

تحدي 1 من 4:
أصلح مكونًا لا يتحدث

يستقبل مكون Clock هذا prop: color و time. عندما تحدد لونًا مختلفًا في مربع التحديد، يستقبل مكون Clock prop color مختلفة من مكونه الأصل. ومع ذلك، لسبب ما، لا يتحدث اللون المعروض. لماذا؟ أصلح المشكلة.

import { useState } from 'react'; export default function Clock(props) { const [color, setColor] = useState(props.color); return ( <h1 style={{ color: color }}> {props.time} </h1> ); }
السابقالتفاعل مع Input باستخدام State
التاليمشاركة State بين Components

Copyright © Meta Platforms, Inc
no uwu plz
uwu?
Logo by@sawaratsuki1004
تعلم React
بداية سريعة
التثبيت
وصف واجهة المستخدم (UI)
إضافة التفاعلية
إدارة State
مخارج الطوارئ
مرجع API
React APIs
React DOM APIs
المجتمع
ميثاق السلوك
تعرف على الفريق
المساهمون في التوثيق
شكر وتقدير
المزيد
المدونة
React Native
الخصوصية
الشروط
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0 });
Fork
import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

Fork
import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Fork
import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

const isSending = status === 'sending';
const isSent = status === 'sent';
const isSending = status === 'sending';
const isSent = status === 'sent';
Fork
import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Fork
import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

const fullName = firstName + ' ' + lastName;
const fullName = firstName + ' ' + lastName;
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
function Message({ messageColor }) {
const color = messageColor;
function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);
Fork
import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Fork
import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Fork
import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Fork
export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

Fork
export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

Fork
import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Create a new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Update the root state object...
    setPlan({
      ...plan,
      // ...so that it has the updated parent.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Fork
{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}
Fork
import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}