cloneElement

Tuzak

cloneElement kullanımı yaygın değildir ve kırılgan kodlara yol açabilir. Yaygın alternatiflere göz atın.

cloneElement, başka bir elementi başlangıç noktası olarak kullanarak yeni bir React elementi oluşturmanıza olanak tanır.

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

Referans

cloneElement(element, props, ...children)

element temel alınarak ancak farklı props ve children ile bir React elementi oluşturmak için cloneElement fonksiyonunu çağırın:

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>

Daha fazla örneği aşağıda inceleyin.

Parametreler

  • element: element argümanı geçerli bir React elementi olmalıdır. Örneğin <Something /> gibi bir JSX node’u, createElement çağrısının sonucu veya başka bir cloneElement çağrısının sonucu olabilir.

  • props: props argümanı bir obje veya null olmalıdır. Eğer null geçirirseniz, clone edilen element orijinal element.props değerlerini korur. Aksi takdirde, props objesindeki her prop için dönen element, element.props yerine props içindeki değeri “tercih eder”. Geri kalan prop’lar orijinal element.props’tan doldurulur. Eğer props.key veya props.ref geçirirseniz, bunlar orijinal değerlerin yerini alır.

  • opsiyonel ...children: Sıfır veya daha fazla child node. Bunlar React element’leri, string’ler, sayılar, portals, boş node’lar (null, undefined, true, false) ve React node dizileri dahil olmak üzere herhangi bir React node olabilir. Eğer herhangi bir ...children argümanı geçmezseniz, orijinal element.props.children korunur.

Returns

cloneElement, birkaç özelliğe sahip bir React element objesi döndürür:

  • type: element.type ile aynıdır.
  • props: element.props ile verdiğiniz override props’un shallow merge edilmesi sonucu oluşur.
  • ref: props.ref ile override edilmediyse, orijinal element.ref.
  • key: props.key ile override edilmediyse, orijinal element.key.

Genellikle, bu elementi component’inizden döndürür veya başka bir elementin child’ı olarak kullanırsınız. Element’in özelliklerini okuyabilseniz de, oluşturulduktan sonra her elementi opaque (iç yapısı bilinmeyen) olarak ele almak ve sadece render etmek en iyisidir.

Uyarılar

  • Bir elementi clone etmek orijinal elementi değiştirmez.

  • children’ları cloneElement’e yalnızca tamamı statik olarak biliniyorsa çoklu argümanlar şeklinde geçmelisiniz, örneğin cloneElement(element, null, child1, child2, child3). Eğer children dinamik ise, tüm diziyi üçüncü argüman olarak geçin: cloneElement(element, null, listItems). Bu, React’in dinamik listeler için eksik key uyarısı vermesini sağlar. Statik listeler için bu gerekli değildir çünkü yeniden sıralanmazlar.

  • cloneElement, veri akışını takip etmeyi zorlaştırır, bu yüzden bunun yerine alternatifleri kullanmayı deneyin.


Kullanım

Bir öğenin özelliklerini geçersiz kılma

Bazı React element prop’larını override etmek için, onu cloneElement’e override etmek istediğiniz props ile geçirin:

import { cloneElement } from 'react';

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

Burada, ortaya çıkan cloned element <Row title="Cabbage" isHighlighted={true} /> olacaktır.

Ne zaman kullanışlı olduğunu görmek için bir örnek üzerinden gidelim.

Bir List component’ini düşünün; bu component children öğelerini, seçilebilir satırlar listesi olarak render eder ve hangi satırın seçili olduğunu değiştiren bir “Next” butonu vardır. List component’i seçili Row’u farklı render etmesi gerektiğinden, aldığı her <Row> child’ını clone eder ve ekstra bir isHighlighted: true veya isHighlighted: false prop’u ekler:

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

Diyelim ki List’in aldığı orijinal JSX şöyle görünüyor:

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

Children’larını clone ederek, List her bir Row’a ekstra bilgi geçirebilir. Sonuç şöyle görünür:

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

“Next” butonuna basıldığında List’in state’inin nasıl güncellendiğine ve farklı bir satırın highlight edildiğine dikkat edin:

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

Özetle, List aldığı <Row /> elementlerini clone etti ve bunlara ekstra bir prop ekledi.

Tuzak

Children’ları clone etmek, veri akışının uygulamanızda nasıl ilerlediğini anlamayı zorlaştırır. Alternatiflerden birini denemeyi deneyin.


Alternatifler

Render prop ile veri aktarımı

cloneElement kullanmak yerine, renderItem gibi bir render prop kabul etmeyi düşünebilirsiniz. Burada List, renderItem’ı bir prop olarak alır. List, her item için renderItem’ı çağırır ve isHighlighted’ı bir argüman olarak geçirir:

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 prop’una “render prop” denir çünkü bir şeyin nasıl render edileceğini belirten bir prop’tur. Örneğin, verilen isHighlighted değeri ile bir <Row> render eden bir renderItem implementasyonu geçebilirsiniz:

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

Sonuç, cloneElement kullanımıyla elde edilen ile aynıdır:

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

Ancak, isHighlighted değerinin nereden geldiğini açıkça takip edebilirsiniz.

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

Bu desen, cloneElement’e göre tercih edilir çünkü daha açıktır.


Veriyi context üzerinden geçirmek

cloneElement’e bir diğer alternatif ise veriyi context üzerinden geçirmektir.

Örneğin, bir HighlightContext tanımlamak için createContext çağırabilirsiniz:

export const HighlightContext = createContext(false);

List bileşeniniz, render ettiği her bir öğeyi bir HighlightContext sağlayıcısı içine sarabilir:

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 key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext>
);
})}

Bu yaklaşımla, Row bileşeninin artık bir isHighlighted prop’u almasına gerek yoktur. Bunun yerine, context’i okur:

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

Bu, çağıran bileşenin <Row> bileşenine isHighlighted prop’unu geçmek zorunda kalmamasını ve bununla ilgilenmemesini sağlar:

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

Bunun yerine, List ve Row, highlight (vurgulama) mantığını context üzerinden koordine eder:

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
            key={item.id}
            value={isHighlighted}
          >
            {renderItem(item)}
          </HighlightContext>
        );
      })}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % items.length
        );
      }}>
        Next
      </button>
    </div>
  );
}

Context aracılığıyla veri aktarma hakkında daha fazla bilgi edinin.


Mantığı özel bir Hook içine çıkarma

Deneyebileceğiniz bir diğer yaklaşım, “görsel olmayan” mantığı kendi Hook’unuza çıkarmak ve Hook’unuzun döndürdüğü bilgilere göre neyin render edileceğine karar vermektir. Örneğin, aşağıdaki gibi bir useList özel Hook’u yazabilirsiniz:

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

Ardından bunu şu şekilde kullanabilirsiniz:

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

Veri akışı nettir (explicit), ancak state useList custom Hook’unun içindedir ve bunu herhangi bir bileşenden kullanabilirsiniz:

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

Bu yaklaşım, bu mantığı farklı bileşenler arasında yeniden kullanmak istediğiniz durumlarda özellikle faydalıdır.