javascript - Bagaimana saya bisa menggunakan beberapa refs untuk array elemen dengan hooks?
Sejauh yang saya pahami, saya bisa menggunakan refs untuk satu elemen seperti ini:
const { useRef, useState, useEffect } = React;
const App = () => {
const elRef = useRef();
const [elWidth, setElWidth] = useState();
useEffect(() => {
setElWidth(elRef.current.offsetWidth);
}, []);
return (
<div>
<div ref={elRef} style={{ width: "100px" }}>
Width is: {elWidth}
</div>
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById("root")
);<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>Bagaimana saya bisa menerapkan ini untuk sebuah array elemen? Jelas bukan seperti itu: (Saya tahu itu bahkan saya tidak mencobanya:)
const { useRef, useState, useEffect } = React;
const App = () => {
const elRef = useRef();
const [elWidth, setElWidth] = useState();
useEffect(() => {
setElWidth(elRef.current.offsetWidth);
}, []);
return (
<div>
{[1, 2, 3].map(el => (
<div ref={elRef} style={{ width: `${el * 100}px` }}>
Width is: {elWidth}
</div>
))}
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById("root")
);<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>Saya telah melihat ini dan oleh karena itu ini. Tapi, saya masih bingung tentang bagaimana menerapkan saran itu untuk kasus sederhana ini.
Jawaban #1
Karena Anda tidak dapat menggunakan hooks di dalam loop, berikut adalah solusi agar dapat bekerja ketika array berubah seiring waktu.
Saya kira array itu berasal dari props :
const App = props => {
const itemsRef = useRef([]);
// you can access the elements with itemsRef.current[n]
useEffect(() => {
itemsRef.current = itemsRef.current.slice(0, props.items.length);
}, [props.items]);
return props.items.map((item, i) => (
<div
key={i}
ref={el => itemsRef.current[i] = el}
style={{ width: `${(i + 1) * 100}px` }}>
...
</div>
));
}
Jawaban #2
Sebuah ref pada awalnya hanyalah objek { current: null }. useRef menjaga referensi ke objek ini di antara render komponen. Nilai current terutama ditujukan untuk ref komponen tetapi bisa menampung apa saja.
Seharusnya ada sebuah array dari refs pada suatu titik. Jika panjang array dapat bervariasi antara render, array tersebut seharusnya menyesuaikan skalanya:
const arrLength = arr.length;
const [elRefs, setElRefs] = React.useState([]);
React.useEffect(() => {
// add or remove refs
setElRefs((elRefs) =>
Array(arrLength)
.fill()
.map((_, i) => elRefs[i] || createRef()),
);
}, [arrLength]);
return (
<div>
{arr.map((el, i) => (
<div ref={elRefs[i]} style={...}>
...
</div>
))}
</div>
);
Potongan kode ini dapat dioptimalkan dengan membuka useEffect dan mengganti useState dengan useRef namun perlu dicatat bahwa melakukan efek samping dalam fungsi render umumnya dianggap sebagai praktik yang buruk:
const arrLength = arr.length;
const elRefs = React.useRef([]);
if (elRefs.current.length !== arrLength) {
// add or remove refs
elRefs.current = Array(arrLength)
.fill()
.map((_, i) => elRefs.current[i] || createRef());
}
return (
<div>
{arr.map((el, i) => (
<div ref={elRefs.current[i]} style={...}>
...
</div>
))}
</div>
);
Jawaban #3
Perbarui
Dokumentasi React baru menunjukkan cara yang disarankan dengan menggunakan map.
Periksa versi Beta di sini (Des, 2022)
Ada dua cara
- gunakan satu ref dengan beberapa elemen current
const inputRef = useRef([]);
inputRef.current[idx].focus();
<input
ref={el => inputRef.current[idx] = el}
/>
const {useRef} = React;
const App = () => {
const list = [...Array(8).keys()];
const inputRef = useRef([]);
const handler = idx => e => {
const next = inputRef.current[idx + 1];
if (next) {
next.focus()
}
};
return (
<div className="App">
<div className="input_boxes">
{list.map(x => (
<div>
<input
key={x}
ref={el => inputRef.current[x] = el}
onChange={handler(x)}
type="number"
className="otp_box"
/>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>gunakan Array dari ref
Seperti yang dikatakan oleh postingan di atas, ini tidak disarankan karena panduan resmi (dan pemeriksaan lint internal) tidak akan membiarkannya lolos.
Jangan memanggil Hooks di dalam loop, kondisi, atau fungsi bersarang. Sebagai gantinya, selalu gunakan Hooks di tingkat atas fungsi React Anda. Dengan mengikuti aturan ini, Anda memastikan bahwa Hooks dipanggil dalam urutan yang sama setiap kali komponen dirender.
Namun, karena ini bukan kasus kita saat ini, demo di bawah ini masih berfungsi, hanya saja tidak disarankan.
const inputRef = list.map(x => useRef(null));
inputRef[idx].current.focus();
<input
ref={inputRef[idx]}
/>
const {useRef} = React;
const App = () => {
const list = [...Array(8).keys()];
const inputRef = list.map(x => useRef(null));
const handler = idx => () => {
const next = inputRef[idx + 1];
if (next) {
next.current.focus();
}
};
return (
<div className="App">
<div className="input_boxes">
{list.map(x => (
<div>
<input
key={x}
ref={inputRef[x]}
onChange={handler(x)}
type="number"
className="otp_box"
/>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>Jawaban #4
Semua opsi lain di atas bergantung pada Array, tetapi itu membuat segala sesuatunya sangat rapuh, karena elemen mungkin diurutkan ulang dan kemudian kita tidak melacak referensi mana yang milik elemen mana.
React menggunakan properti key untuk melacak item. Oleh karena itu jika Anda menyimpan refs Anda berdasarkan key tidak akan ada masalah :
const useRefs = () => {
const refsByKey = useRef<Record<string,HTMLElement | null>>({})
const setRef = (element: HTMLElement | null, key: string) => {
refsByKey.current[key] = element;
}
return {refsByKey: refsByKey.current, setRef};
}
const Comp = ({ items }) => {
const {refsByKey, setRef} = useRefs()
const refs = Object.values(refsByKey).filter(Boolean) // your array of refs here
return (
<div>
{items.map(item => (
<div key={item.id} ref={element => setRef(element, item.id)}/>
)}
</div>
)
}
Perlu diingat bahwa React, saat menghapus sebuah item, akan memanggil fungsi yang diberikan dengan null, yang akan mengatur entri key yang sesuai menjadi null dalam objek, sehingga semuanya akan terbarui.
Jawaban #5
Cara yang paling sederhana dan efektif adalah dengan tidak menggunakan useRef sama sekali. Cukup gunakan callback ref yang membuat array baru dari ref pada setiap render.
function useArrayRef() {
const refs = []
return [refs, el => el && refs.push(el)]
}
Demo
<div id="root"></div>
<script type="text/babel" defer>
const { useEffect, useState } = React
function useArrayRef() {
const refs = []
return [refs, el => el && refs.push(el)]
}
const App = () => {
const [elements, ref] = useArrayRef()
const [third, setThird] = useState(false)
useEffect(() => {
console.log(elements)
}, [third])
return (
<div>
<div ref={ref}>
<button ref={ref} onClick={() => setThird(!third)}>toggle third div</button>
</div>
<div ref={ref}>another div</div>
{ third && <div ref={ref}>third div</div>}
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>Perbarui :
Berikut adalah versi Typescript dari hook yang membuat callback ref yang dimemoisasi dan mendukung penerusan kunci khusus ke setiap ref elemen.
import { MutableRefObject, useRef } from 'react'
type UseElementsRefResult<T extends HTMLElement = HTMLElement> = [
/**
* Map of DOM node refs identified by an auto-created key or custom one.
*/
MutableRefObject<{ readonly [K: string | number]: T }>,
/**
* Creates a memoized callback ref for a DOM node.
* If you expect the rendering order of nodes to change, use a custom key.
*
* @param customKey Key used to set/unset the node ref.
*/
(customKey?: string | number) => (el?: T) => void,
]
/**
* Hook that creates callback refs to manage DOM node refs of multiple elements.
* Ensures refs remain stable even with re-renders. Use custom keys if the rendering order may change.
*/
export const useElementsRef = <T extends HTMLElement = HTMLElement>(): UseElementsRefResult<T> => {
const elementRefs = useRef({
current: {} as { [K: string | number]: T },
callbacks: {} as Record<string | number, (el?: T) => void>,
})
// Resets index on every render; stable refs as long as the render order remains unchanged
let currentIndex = 0
return [
elementRefs.current,
(customKey) => {
const { current: elements, callbacks } = elementRefs.current
const autoKey = currentIndex++
const key = customKey ?? autoKey
// Memoize callback for stable ref assignment
if (!callbacks[key]) {
callbacks[key] = (el) => {
// React is setting the ref for this node
if (el != null) {
elements[key] = el
return
}
// React is clearing the node ref
delete elements[key]
}
}
return callbacks[key]
},
]
}
Demo
const { render } = ReactDOM
const { useEffect, useRef, useState } = React
const useElementsRef = () => {
const elementRefs = useRef({
current: {},
callbacks: {},
})
let currentIndex = 0
return [
elementRefs.current,
(customKey) => {
const { current: elements, callbacks } = elementRefs.current
const autoKey = currentIndex++
const key = customKey ?? autoKey
// Memoize callback for stable ref assignment
if (!callbacks[key]) {
callbacks[key] = (el) => {
// React is setting the ref for this node
if (el != null) {
elements[key] = el
return
}
// React is clearing the node ref
delete elements[key]
}
}
return callbacks[key]
},
]
}
const App = () => {
const [elementsRef, createElementRef] = useElementsRef()
const [third, setThird] = useState(false)
useEffect(() => {
console.log(elementsRef.current)
}, [third])
return (
<div>
<div ref={createElementRef()}>
<button ref={createElementRef()} onClick={() => setThird(!third)}>toggle third div</button>
</div>
<div ref={createElementRef('custom-key')}>another div</div>
{ third && <div ref={createElementRef('third-div')}>third div</div>}
</div>
);
}
render(<App />, document.getElementById("root"))<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>Jawaban #6
Perlu dicatat bahwa Anda tidak sebaiknya menggunakan useRef di dalam loop karena alasan sederhana: urutan hook yang digunakan itu penting!
Dokumentasi mengatakan
Jangan memanggil Hooks di dalam loop, kondisi, atau fungsi bersarang. Sebagai gantinya, selalu gunakan Hooks di tingkat atas dari fungsi React Anda. Dengan mengikuti aturan ini, Anda memastikan bahwa Hooks dipanggil dalam urutan yang sama setiap kali sebuah komponen dirender. Itulah yang memungkinkan React untuk mempertahankan status Hooks dengan benar antara beberapa pemanggilan useState dan useEffect. (Jika Anda penasaran, kami akan menjelaskan ini secara mendalam di bawah.)
Tapi pertimbangkan bahwa ini jelas berlaku untuk array dinamis... tapi jika kamu menggunakan array statis (kamu SELALU merender jumlah komponen yang sama) jangan terlalu khawatir tentang itu, sadarilah apa yang kamu lakukan dan manfaatkan itu 😉
