Pitfall

Menggunakan cloneElement adalah hal yang jarang terjadi dan dapat menyebabkan kode yang rentan. Lihat alternatif yang umum.

cloneElement memungkinkan Anda untuk membuat elemen React baru dengan menggunakan elemen lain sebagai titik awal.

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

Referensi

cloneElement(element, props, ...children)

Panggil cloneElement untuk membuat elemen React berdasarkan element, tetapi dengan props dan children yang berbeda:

import { cloneElement } from 'react';

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

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

Lihat lebih banyak contoh di bawah ini.

Parameter

  • element: Argumen element harus merupakan elemen React yang valid. Misalnya, dapat berupa simpul JSX seperti <Something />, hasil dari pemanggilan createElement, atau hasil dari pemanggilan cloneElement lainnya.

  • props: Argumen props harus berupa objek atau null. Jika Anda mengoper null, elemen yang di-kloning akan mempertahankan semua element.props yang orisinal. Sebaliknya, untuk setiap prop di objek props, elemen yang dikembalikan akan “memilih” nilai dari props daripada nilai dari element.props. Sisa props lainnya akan diisi dari element.props yang orisinal. Jika Anda mengoper props.key atau props.ref, mereka akan menggantikan yang orisinal.

  • opsional ...children: Nol atau lebih simpul anak. Bisa dari simpul React apa pun, termasuk elemen React, string, dan number. portal, simpul kosong (null, undefined, true, dan false), dan senarai dari simpul-simpul React. Jika Anda tidak mengoper argumen ...children apa pun, element.props.children yang orisinal akan tetap dipertahankan.

Kembalian

cloneElement mengembalikan objek elemen React dengan beberapa properti:

  • type: Sama seperti element.type.
  • props: Hasil dari penggabungan dangkal antara element.props dengan props yang Anda oper untuk menimpanya.
  • ref: element.ref yang orisinal, kecuali telah ditimpa oleh props.ref.
  • key: element.key, yang orisinal, kecuali telah ditimpa oleh props.key.

Biasanya, Anda akan mengembalikan elemen dari sebuah komponen atau membuatnya sebagai anak dari elemen lain. Meskipun Anda mungkin membaca properti elemen tersebut, sebaiknya Anda memperlakukan setiap elemen sebagai objek tersembunyi setelah dibuat, dan hanya me-render-nya.

Catatan penting

  • Mengkloning sebuah elemen tidak mengubah elemen yang orisinal.

  • Sebaiknya Anda hanya mengoper children sebagai beberapa argumen ke cloneElement jika semuanya diketahui secara statis, seperti cloneElement(element, null, child1, child2, child3). Jika children Anda dinamis, oper seluruh senarai sebagai argumen ketiga: cloneElement(element, null, listItems). Ini memastikan bahwa React akan memperingatkan Anda tentang key yang hilang untuk setiap list dinamis. Untuk list statis hal tersebut tidak diperlukan karena tidak pernah diurutkan ulang.

  • cloneElement membuat pelacakan aliran data lebih sulit, jadi cobalah beberapa alternatif sebagai gantinya.


Penggunaan

Menimpa props dari suatu elemen

Untuk menimpa prop dari beberapa elemen React, oper ke cloneElement dengan props yang ingin Anda timpa:

import { cloneElement } from 'react';

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

Hasil dari elemen yang dikloning akan menjadi <Row title="Cabbage" isHighlighted={true} />.

Mari telusuri contoh untuk melihat kapan hal tersebut berguna.

Bayangkan komponen List yang me-render childrennya sebagai daftar baris yang dapat dipilih dengan tombol “Next” yang dapat merubah baris mana yang dipilih. Komponen List perlu me-render Row yang dipilih secara terpisah, lalu mengkloning setiap anak <Row> yang telah diterima, dan menambahkan prop isHighlighted: true atau 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
})
)}

Katakanlah JSX asli yang diterima oleh List terlihat seperti ini:

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

Dengan mengkloning anaknya, List dapat meneruskan informasi tambahan ke setiap Row di dalamnya. Hasilnya terlihat seperti ini:

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

Perhatikan saat menekan “Next” akan memperbarui state dari List, dan menyorot baris yang berbeda:

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

Ringkasnya, List mengkloning elemen <Row /> yang diterimanya dan menambahkan prop tambahan ke dalamnya.

Pitfall

Mengkloning children mempersulit untuk mengetahui bagaimana aliran data di aplikasi Anda. Coba salah satu alternatif.


Alternatif

Mengoper data dengan render prop

Daripada menggunakan cloneElement, pertimbangkan untuk menerima render prop seperti renderItem. Di sini, List menerima renderItem sebagai prop. List memanggil renderItem untuk setiap item dan mengoper isHighlighted sebagai argumen:

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

Prop renderItem disebut “render prop” karena merupakan prop yang menentukan cara me-render sesuatu. Misalnya, Anda dapat mengoper renderItem yang me-render <Row> dengan nilai isHighlighted yang diberikan:

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

Hasil akhirnya sama dengan cloneElement:

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

Namun, Anda dapat dengan mudah melacak dari mana nilai isHighlighted berasal.

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

Pola ini lebih anjurkan daripada cloneElement karena lebih eksplisit.


Mengoper data melalui context

Alternatif lain untuk cloneElement adalah mengoper data melalui context.

Sebagai contoh, Anda dapat memanggil createContext untuk mendefinisikan HighlightContext:

export const HighlightContext = createContext(false);

Komponen List dapat menggabungkan setiap item yang di-render ke dalam provider 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>
);
})}

Dengan pendekatan ini, Row tidak perlu menerima prop isHighlighted sama sekali. Sebaliknya, dengan membaca context-nya:

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

Hal ini memungkinkan komponen pemanggil untuk tidak mengetahui atau peduli tentang pengoperan isHighlighted ke <Row>:

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

Sebagai gantinya, List dan Row mengoordinasikan logika penyorotan melalui context.

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

Pelajari lebih lanjut tentang mengoper data melalui context.


Mengekstraksi logika ke dalam Hook kustom

Pendekatan lain yang dapat Anda coba adalah mengekstrak logika “non-visual” ke dalam Hook Anda sendiri, dan menggunakan informasi yang dikembalikan oleh Hook Anda untuk memutuskan apa yang akan di-render. Misalnya, Anda dapat menulis Hook kustom useList seperti ini:

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];
}

Lalu Anda dapat menggunakannya seperti ini:

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

Aliran datanya eksplisit, tetapi state ada di dalam Hook kustom useList yang dapat Anda gunakan dari komponen apa pun:

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

Pendekatan ini sangat berguna jika Anda ingin menggunakan kembali logika ini di komponen yang berbeda.