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

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

في هذه الصفحة

  • Overview
  • الحالة مرتبطة بموضع في شجرة العرض
  • نفس المكون في نفس الموضع يحفظ الحالة
  • مكونات مختلفة في نفس الموضع تعيد تعيين الحالة
  • إعادة تعيين الحالة في نفس الموضع
  • الخيار 1: عرض مكون في مواضع مختلفة
  • الخيار 2: إعادة تعيين الحالة باستخدام key
  • إعادة تعيين نموذج باستخدام key
  • 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

الحفاظ على الحالة وإعادة تعيينها

الحالة معزولة بين المكونات. يتتبع React أي حالة تنتمي إلى أي مكون بناءً على مكانهم في شجرة واجهة المستخدم. يمكنك التحكم في متى يتم الحفاظ على الحالة ومتى يتم إعادة تعيينها بين عمليات إعادة العرض.

You will learn

  • متى يختار React الحفاظ على الحالة أو إعادة تعيينها
  • كيفية إجبار React على إعادة تعيين حالة المكون
  • كيف تؤثر المفاتيح والأنواع على ما إذا كان سيتم الحفاظ على الحالة

الحالة مرتبطة بموضع في شجرة العرض

يبني React أشجار العرض لبنية المكونات في واجهة المستخدم الخاصة بك.

عندما تعطي مكونًا حالة، قد تعتقد أن الحالة “تعيش” داخل المكون. لكن الحالة في الواقع محفوظة داخل React. يربط React كل جزء من الحالة التي يحتفظ بها مع المكون الصحيح حسب مكان جلوس ذلك المكون في شجرة العرض.

هنا، يوجد وسم <Counter /> JSX واحد فقط، لكنه يتم عرضه في موضعين مختلفين:

import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

إليك كيف تبدو هذه كشجرة:

Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.

شجرة React

هذه عدّادان منفصلان لأن كل واحد يتم عرضه في موضعه الخاص في الشجرة. عادةً لا تحتاج إلى التفكير في هذه المواضع لاستخدام React، لكن قد يكون من المفيد فهم كيفية عملها.

في React، كل مكون على الشاشة له حالة معزولة تمامًا. على سبيل المثال، إذا عرضت مكونَي Counter جنبًا إلى جنب، فإن كلًا منهما سيحصل على حالاته الخاصة والمستقلة score و hover.

جرب النقر على كلا العدّادين ولاحظ أنهما لا يؤثران على بعضهما البعض:

import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

As you can see, when one counter is updated, only the state for that component is updated:

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.

تحديث الحالة

سيحتفظ React بالحالة طالما أنك تعرض نفس المكون في نفس الموضع في الشجرة. لرؤية ذلك، قم بزيادة كلا العدّادين، ثم أزل المكون الثاني بإلغاء تحديد مربع الاختيار “Render the second counter”، ثم أضفه مرة أخرى بتحديده مرة أخرى:

import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Render the second counter </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

لاحظ كيف أنه في اللحظة التي تتوقف فيها عن عرض العدّاد الثاني، تختفي حالته تمامًا. ذلك لأنه عندما يزيل React مكونًا، فإنه يدمر حالته.

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.

حذف مكون

عندما تحدد “Render the second counter”، يتم تهيئة Counter ثانٍ وحالته من الصفر (score = 0) وإضافته إلى DOM.

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.

إضافة مكون

يحفظ React حالة المكون طالما يتم عرضه في موضعه في شجرة واجهة المستخدم. إذا تمت إزالته، أو إذا تم عرض مكون مختلف في نفس الموضع، فإن React يتجاهل حالته.

نفس المكون في نفس الموضع يحفظ الحالة

في هذا المثال، يوجد وسمان <Counter /> مختلفان:

import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

عندما تحدد أو تلغي تحديد مربع الاختيار، لا يتم إعادة تعيين حالة العدّاد. سواء كان isFancy يساوي true أو false، فلديك دائمًا <Counter /> كأول ابن للـ div المُرجع من مكون App الجذر:

Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.
Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.

تحديث حالة App لا يعيد تعيين Counter لأن Counter يبقى في نفس الموضع

إنه نفس المكون في نفس الموضع، لذلك من منظور React، إنه نفس العدّاد.

مأزق

تذكر أن الموضع في شجرة واجهة المستخدم—وليس في ترميز JSX—هو ما يهم React! هذا المكون لديه جملتا return مع وسوم <Counter /> JSX مختلفة داخل وخارج if:

import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); if (isFancy) { return ( <div> <Counter isFancy={true} /> <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } return ( <div> <Counter isFancy={false} /> <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

قد تتوقع أن يتم إعادة تعيين الحالة عند تحديد مربع الاختيار، لكن ذلك لا يحدث! هذا لأن كلا وسمَي <Counter /> هذين يتم عرضهما في نفس الموضع. React لا يعرف أين تضع الشروط في دالتك. كل ما “يراه” هو الشجرة التي تُرجعها.

في كلتا الحالتين، يُرجع مكون App عنصر <div> مع <Counter /> كابن أول. بالنسبة لـ React، لهذين العدّادين نفس “العنوان”: الابن الأول للابن الأول للجذر. هذه هي الطريقة التي يطابق بها React بينهما بين العرض السابق والتالي، بغض النظر عن كيفية هيكلة منطقك.

مكونات مختلفة في نفس الموضع تعيد تعيين الحالة

في هذا المثال، سيؤدي تحديد مربع الاختيار إلى استبدال <Counter> بـ <p>:

import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

هنا، تبدل بين أنواع مكونات مختلفة في نفس الموضع. في البداية، احتوى الابن الأول للـ <div> على Counter. لكن عندما بدّلته بـ p، أزال React الـ Counter من شجرة واجهة المستخدم ودمر حالته.

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.

عندما يتغير Counter إلى p، يُحذف Counter ويُضاف p

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.

عند التبديل مرة أخرى، يُحذف p ويُضاف Counter

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

import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

The counter state gets reset when you click the checkbox. Although you render a Counter, the first child of the div changes from a div to a section. When the child div was removed from the DOM, the whole tree below it (including the Counter and its state) was destroyed as well.

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

When section changes to div, the section is deleted and the new div is added

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

When switching back, the div is deleted and the new section is added

كقاعدة عامة، إذا كنت تريد الحفاظ على الحالة بين عمليات إعادة العرض، فإن بنية شجرتك يجب أن “تتطابق” من عرض إلى آخر. إذا كانت البنية مختلفة، يتم تدمير الحالة لأن React يدمر الحالة عندما يزيل مكونًا من الشجرة.

مأزق

هذا هو السبب في أنه يجب عدم تداخل تعريفات دوال المكونات.

هنا، يتم تعريف دالة مكون MyTextField داخل MyComponent:

import { useState } from 'react'; export default function MyComponent() { const [counter, setCounter] = useState(0); function MyTextField() { const [text, setText] = useState(''); return ( <input value={text} onChange={e => setText(e.target.value)} /> ); } return ( <> <MyTextField /> <button onClick={() => { setCounter(counter + 1) }}>Clicked {counter} times</button> </> ); }

في كل مرة تنقر فيها على الزر، تختفي حالة الإدخال! هذا لأنه يتم إنشاء دالة MyTextField مختلفة لكل عرض من MyComponent. أنت تعرض مكونًا مختلفًا في نفس الموضع، لذلك يعيد React تعيين كل الحالة أدناه. يؤدي هذا إلى أخطاء ومشاكل في الأداء. لتجنب هذه المشكلة، قم دائمًا بالإعلان عن دوال المكونات في المستوى الأعلى، ولا تداخل تعريفاتها.

إعادة تعيين الحالة في نفس الموضع

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

import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

حاليًا، عندما تغيّر اللاعب، يتم الحفاظ على النتيجة. يظهر Counterان في نفس الموضع، لذلك يراهما React على أنهما نفس Counter الذي تغير prop person الخاص به.

لكن من الناحية المفاهيمية، في هذا التطبيق يجب أن يكونا عدّادين منفصلين. قد يظهران في نفس المكان في واجهة المستخدم، لكن أحدهما عدّاد لـ Taylor، والآخر عدّاد لـ Sarah.

هناك طريقتان لإعادة تعيين الحالة عند التبديل بينهما:

  1. عرض المكونات في مواضع مختلفة
  2. إعطاء كل مكون هوية صريحة باستخدام key

الخيار 1: عرض مكون في مواضع مختلفة

إذا كنت تريد أن يكون هذان Counterان مستقلين، يمكنك عرضهما في موضعين مختلفين:

import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
  • Initially, isPlayerA is true. So the first position contains Counter state, and the second one is empty.
  • When you click the “Next player” button the first position clears but the second one now contains a Counter.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.

الحالة الأولية

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.

النقر على “next”

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.

النقر على “next” مرة أخرى

يتم تدمير حالة كل Counter في كل مرة تتم إزالته من DOM. هذا هو السبب في أنهم يعيدون التعيين في كل مرة تنقر فيها على الزر.

هذا الحل مناسب عندما يكون لديك عدد قليل فقط من المكونات المستقلة المعروضة في نفس المكان. في هذا المثال، لديك اثنان فقط، لذلك ليس من المزعج عرض كليهما بشكل منفصل في JSX.

الخيار 2: إعادة تعيين الحالة باستخدام key

هناك أيضًا طريقة أخرى أكثر عمومية لإعادة تعيين حالة المكون.

ربما رأيت keys عند عرض القوائم. المفاتيح ليست للقوائم فقط! يمكنك استخدام المفاتيح لجعل React يميز بين أي مكونات. بشكل افتراضي، يستخدم React الترتيب داخل الأب (“العدّاد الأول”، “العدّاد الثاني”) للتمييز بين المكونات. لكن المفاتيح تتيح لك إخبار React أن هذا ليس مجرد عدّاد أول، أو عدّاد ثانٍ، بل عدّاد محدد—على سبيل المثال، عدّاد Taylor. بهذه الطريقة، سيعرف React عدّاد Taylor أينما ظهر في الشجرة!

في هذا المثال، <Counter />ان لا يشتركان في الحالة رغم أنهما يظهران في نفس المكان في JSX:

import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }

Switching between Taylor and Sarah does not preserve the state. This is because you gave them different keys:

{isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )}

تحديد key يخبر React باستخدام key نفسه كجزء من الموضع، بدلاً من ترتيبه داخل الأب. هذا هو السبب في أنه على الرغم من أنك تعرضهما في نفس المكان في JSX، يراهما React كعدّادين مختلفين، وبالتالي لن يشتركا أبدًا في الحالة. في كل مرة يظهر فيها عدّاد على الشاشة، يتم إنشاء حالته. في كل مرة تتم إزالته، يتم تدمير حالته. التبديل بينهما يعيد تعيين حالتهما مرارًا وتكرارًا.

ملاحظة

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

إعادة تعيين نموذج باستخدام key

إعادة تعيين الحالة باستخدام key مفيدة بشكل خاص عند التعامل مع النماذج.

في تطبيق الدردشة هذا، يحتوي مكون <Chat> على حالة إدخال النص:

import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];

جرب إدخال شيء ما في الإدخال، ثم اضغط على “Alice” أو “Bob” لاختيار مستلم مختلف. ستلاحظ أن حالة الإدخال محفوظة لأن <Chat> يتم عرضه في نفس الموضع في الشجرة.

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

<Chat key={to.id} contact={to} />

هذا يضمن أنه عندما تحدد مستلمًا مختلفًا، سيتم إعادة إنشاء مكون Chat من الصفر، بما في ذلك أي حالة في الشجرة أدناه. سيعيد React أيضًا إنشاء عناصر DOM بدلاً من إعادة استخدامها.

الآن يؤدي تبديل المستلم دائمًا إلى مسح حقل النص:

import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
غوص عميق

الحفاظ على الحالة للمكونات المُزالة

في تطبيق دردشة حقيقي، من المحتمل أن ترغب في استرداد حالة الإدخال عندما يحدد المستخدم المستلم السابق مرة أخرى. هناك عدة طرق للحفاظ على الحالة “حية” لمكون لم يعد مرئيًا:

  • يمكنك عرض جميع المحادثات بدلاً من المحادثة الحالية فقط، لكن إخفاء جميع المحادثات الأخرى باستخدام CSS. لن تتم إزالة المحادثات من الشجرة، لذلك سيتم الحفاظ على حالتها المحلية. يعمل هذا الحل بشكل رائع لواجهات المستخدم البسيطة. لكن يمكن أن يصبح بطيئًا جدًا إذا كانت الأشجار المخفية كبيرة وتحتوي على الكثير من عقد DOM.
  • يمكنك رفع الحالة والاحتفاظ بالرسالة المعلقة لكل مستلم في المكون الأب. بهذه الطريقة، عندما تتم إزالة المكونات الأبناء، لا يهم، لأن الأب هو الذي يحتفظ بالمعلومات المهمة. هذا هو الحل الأكثر شيوعًا.
  • يمكنك أيضًا استخدام مصدر مختلف بالإضافة إلى حالة React. على سبيل المثال، من المحتمل أن ترغب في استمرار مسودة الرسالة حتى لو أغلق المستخدم الصفحة عن طريق الخطأ. لتنفيذ ذلك، يمكنك جعل مكون Chat يهيئ حالته بالقراءة من localStorage، وحفظ المسودات هناك أيضًا.

بغض النظر عن الاستراتيجية التي تختارها، فإن محادثة مع Alice تختلف مفاهيميًا عن محادثة مع Bob، لذا من المنطقي إعطاء key لشجرة <Chat> بناءً على المستلم الحالي.

خلاصة

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

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

تحدي 1 من 5:
أصلح نص الإدخال المختفي

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

import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }
السابقمشاركة State بين Components
التالياستخراج منطق State إلى Reducer

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

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Render the second counter
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Use fancy styling
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>See you later!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Take a break
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

Fork
import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Fork
import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Fork
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

<Chat key={to.id} contact={to} />
Fork
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Fork
import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Hint: Your favorite city?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Hide hint</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Show hint</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}