Pitfall

השימוש ב-cloneElement אינו שכיח ועלול להוביל לקוד שביר. ראה חלופות נפוצות.

cloneElement מאפשר לך ליצור אלמנט React חדש באמצעות אלמנט אחר כנקודת התחלה.

const clonedElement = cloneElement(element, props, ...children)

הפניה

cloneElement(element, props, ...children)

התקשר ל-cloneElement כדי ליצור אלמנט React המבוסס על element, אך עם props וchildren שונים:

import { cloneElement } from 'react';

// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);

console.log(clonedElement); // <Row title="Cabbage" isHighlighted={true}>Goodbye</Row>

ראה דוגמאות נוספות למטה.

פרמטרים

  • element: הארגומנט element חייב להיות אלמנט React חוקי. לדוגמה, זה יכול להיות צומת JSX כמו <Something />, תוצאה של קריאה ל-createElement, או תוצאה של קריאה אחרת של cloneElement.

  • props: הארגומנט props חייב להיות אובייקט או null. אם תעבור את null, האלמנט המשובט ישמור על כל ה-element.props המקורי. אחרת, עבור כל אביזר באובייקט props, האלמנט המוחזר “יעדיף” את הערך מ-props על פני הערך מ-element.props. שאר ה-props יתמלא מה-element.props המקורי. אם תעבור את props.key או props.ref, הם יחליפו את המקוריים.

  • אופציונלי ...children: אפס צמתים צאצאים או יותר. הם יכולים להיות כל צמתים React, כולל אלמנטים React, מחרוזות, מספרים, פורטלים, צמתים ריקים (null, undefined, true ו-false), ומערכים של nodes.9___ אם לא תעביר שום ארגומנט ...children, ה-element.props.children המקורי יישמר.

מחזירה

cloneElement מחזיר אובייקט אלמנט React עם כמה מאפיינים:

  • type: זהה ל-element.type.
  • props: התוצאה של מיזוג רדוד של element.props עם ה-props המכריע שעברת.
  • ref: ה-element.ref המקורי, אלא אם כן הוא נדחק על ידי props.ref.
  • key: ה-element.key המקורי, אלא אם כן הוא נדחק על ידי props.key.

בדרך כלל, אתה תחזיר את הרכיב מהרכיב שלך או תהפוך אותו לילד של אלמנט אחר. למרות שאתה יכול לקרוא את המאפיינים של האלמנט, עדיף להתייחס לכל אלמנט כאטום לאחר יצירתו, ולעבד אותו רק.

אזהרות

  • שיבוט של אלמנט אינו משנה את האלמנט המקורי.

  • עליך להעביר ילדים כארגומנטים מרובים ל-cloneElement רק אם כולם ידועים סטטית, כמו cloneElement(element, null, child1, child2, child3). אם הילדים שלכם דינמיים, העבר את כל המערך כארגומנט השלישי: cloneElement(element, null, listItems). זה מבטיח ש-React תזהיר אותך על חסרים keys עבור כל רשימות דינמיות. עבור רשימות סטטיות זה לא הכרחי כיuse הם אף פעם לא מסדרים מחדש.

  • cloneElement מקשה על המעקב אחר זרימת הנתונים, אז נסה את חלופות במקום זאת.


שימוש

עקיפת props של אלמנט

כדי לעקוף את props של רכיב React כלשהו, ​​העבר אותו ל-cloneElement עם props שברצונך לעקוף:

import { cloneElement } from 'react';

// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);

כאן, הרכיב המשובט שנוצר יהיה <Row title="Cabbage" isHighlighted={true} />.

בואו נעבור על דוגמה כדי לראות מתי היא מלאה use.

תארו לעצמכם רכיב List שמעבד את children שלו כרשימה של שורות לבחירה עם כפתור “הבא” שמשנה איזו שורה נבחרה. הרכיב List צריך לעבד את ה-Row שנבחר בצורה שונה, אז הוא משכפל כל ילד <Row> שהוא קיבל, ומוסיף isHighlighted: true או isHighlighted: false תוספת נוספת:

export default function List({ children }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{Children.map(children, (child, index) =>
cloneElement(child, {
isHighlighted: index === selectedIndex
})
)}

נניח שה-JSX המקורי שהתקבל על-ידי List נראה כך:

<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>

על ידי שיבוט ילדיו, ה-List יכול להעביר מידע נוסף לכל Row בפנים. התוצאה נראית כך:

<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>

שימו לב כיצד לחיצה על “הבא” מעדכנת את ה-state של ה-List, ומדגישה שורה אחרת:

import { Children, cloneElement, useState } from 'react';

export default function List({ children }) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  return (
    <div className="List">
      {Children.map(children, (child, index) =>
        cloneElement(child, {
          isHighlighted: index === selectedIndex 
        })
      )}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % Children.count(children)
        );
      }}>
        Next
      </button>
    </div>
  );
}

לסיכום, ה-List שיבט את האלמנטים <Row /> שקיבל והוסיף להם אבזר נוסף.

Pitfall

שיבוט ילדים מקשה לדעת כיצד הנתונים זורמים דרך האפליקציה שלך. נסה אחת מהחלופות.


חלופות

העברת נתונים עם אבזר עיבוד

במקום להשתמש ב-cloneElement, שקול לקבל מאפיין עיבוד כמו renderItem. כאן, List מקבל renderItem בתור אביזר. List קורא ל-renderItem עבור כל פריט ומעביר את isHighlighted כטיעון:

export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return renderItem(item, isHighlighted);
})}

הפרופס של renderItem נקרא “רנדור פרופס” מכיוון שuse זה אבזר שמציין כיצד לרנדר משהו. לדוגמה, אתה יכול להעביר יישום renderItem המציג <Row> עם הערך הנתון isHighlighted:

<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>

התוצאה הסופית זהה לזו של cloneElement:

<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>

עם זאת, אתה יכול לעקוב בבירור מאיפה מגיע הערך isHighlighted.

import { useState } from 'react';

export default function List({ items, renderItem }) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  return (
    <div className="List">
      {items.map((item, index) => {
        const isHighlighted = index === selectedIndex;
        return renderItem(item, isHighlighted);
      })}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % items.length
        );
      }}>
        Next
      </button>
    </div>
  );
}

תבנית זו מועדפת על cloneElement כי use היא מפורשת יותר.


העברת נתונים דרך ההקשר

חלופה נוספת ל-cloneElement היא להעביר נתונים דרך הקשר.

לדוגמה, אתה יכול לקרוא ל-createContext כדי להגדיר HighlightContext:

export const HighlightContext = createContext(false);

רכיב List שלך יכול לעטוף כל פריט שהוא מעבד לספק HighlightContext:

export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return (
<HighlightContext.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}

בגישה זו, Row אינו צריך לקבל אבזר isHighlighted כלל. במקום זאת, הוא קורא את ההקשר:

export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...

זה מאפשר לרכיב הקורא לא לדעת או לדאוג לגבי העברת isHighlighted ל-<Row>:

<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>

במקום זאת, List ו-Row מתאמים את היגיון ההדגשה באמצעות הקשר.

import { useState } from 'react';
import { HighlightContext } from './HighlightContext.js';

export default function List({ items, renderItem }) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  return (
    <div className="List">
      {items.map((item, index) => {
        const isHighlighted = index === selectedIndex;
        return (
          <HighlightContext.Provider
            key={item.id}
            value={isHighlighted}
          >
            {renderItem(item)}
          </HighlightContext.Provider>
        );
      })}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % items.length
        );
      }}>
        Next
      </button>
    </div>
  );
}

למידע נוסף על העברת נתונים דרך הקשר.


חילוץ לוגיקה לתוך Hook מותאם אישית

גישה נוספת שאתה יכול לנסות היא לחלץ את ההיגיון ה”לא חזותי” לתוך Hook משלך, וuse את המידע המוחזר על ידי Hook שלך כדי להחליט מה לעבד. לדוגמה, אתה יכול לכתוב useList מותאם אישית Hook כך:

import { useState } from 'react';

export default function useList(items) {
const [selectedIndex, setSelectedIndex] = useState(0);

function onNext() {
setSelectedIndex(i =>
(i + 1) % items.length
);
}

const selected = items[selectedIndex];
return [selected, onNext];
}

אז אתה יכול use את זה ככה:

export default function App() {
const [selected, onNext] = useList(products);
return (
<div className="List">
{products.map(product =>
<Row
key={product.id}
title={product.title}
isHighlighted={selected === product}
/>
)}
<hr />
<button onClick={onNext}>
Next
</button>
</div>
);
}

זרימת הנתונים מפורשת, אבל ה-state נמצא בתוך useList המותאם אישית Hook שתוכלו use מכל רכיב:

import Row from './Row.js';
import useList from './useList.js';
import { products } from './data.js';

export default function App() {
  const [selected, onNext] = useList(products);
  return (
    <div className="List">
      {products.map(product =>
        <Row
          key={product.id}
          title={product.title}
          isHighlighted={selected === product}
        />
      )}
      <hr />
      <button onClick={onNext}>
        Next
      </button>
    </div>
  );
}

גישה זו היא useמלאה במיוחד אם ברצונך לשנותuse את ההיגיון הזה בין רכיבים שונים.