How to dynamically push added inputs value to array? - javascript

I have React component where I can dynamically add new text inputs. So, I need to push the values from the inputs to array.
Can anyone help me how to do this?
Here is my code:
function FormPage({ setData }) {
const [item, setItem] = useState([]);
const [counter, setCounter] = useState(0);
const handleCounter = () => {
setCounter(counter + 1);
};
const addItem = (setItem) => setItem((ing) => [...ing, newItem]);
return (
{Array.from(Array(counter)).map((c, index) =>
<TextField
key={index}
label="Item"
onChange={() => setItem(i=> [...i, (this.value)])}
/>
)}
<Button onClick={handleCounter}>Add one more item</Button>
)
}
Here is example in sandbox:
https://codesandbox.io/s/solitary-sound-t2cfy?file=/src/App.js

Firstly, you are using two-way data binding with your TextField component, so you also need to pass a value prop.
Secondly, to get the current value of TextField, we don't use this.value. Rather, the callback to onChange takes an argument of type Event and you can access the current value as follows
<TextField
...
onChange={(e) => {
const value = e.target.value;
// Do something with value
}}
/>
You cannot return multiple children from a component without wrapping them by single component. You are simply returning multiple TextField components at the same level, which is also causing an error. Try wrapping them in React.Fragment as follows
...
return (
<React.Fragment>
{/* Here you can return multiple sibling components*/}
</React.Fragment>
);
You are mapping the TextField components using counter which is equal to the length of item array. In handleCounter, we'll add a placeholder string to accomodate the new TextField value.
...
const handleCounter = () => {
setCounter(prev => prev+1); // Increment the counter
setItem(prev => [...prev, ""]); // Add a new value placeholder for the newly added TextField
}
return (
<React.Fragment>
{ /* Render only when the value of counter and length of item array are the same */
counter === item.length && (Array.from(Array(counter).keys()).map((idx) => (
<TextField
key={idx}
value={item[idx]}
label="Item"
onChange={(e) => {
const val = e.target.value;
setItem(prev => {
const nprev = [...prev]
nprev[idx] = val;
return nprev;
})
}}
/>
)))}
<br />
<Button onClick={handleCounter}>Add one more item</Button>
</React.Fragment>
);
Here is the sandbox link

Try this:
import "./styles.css";
import React, { useState } from "react";
export default function App() {
// Changes made here
const [item, setItem] = useState({});
const [counter, setCounter] = useState(0);
console.log("item 1:", item[0], "item 2:", item[1],item);
const handleCounter = () => {
setCounter(counter + 1);
};
const addItem = (newItem) => setItem((ing) => [...ing, newItem]);
return (
<>
{Array.from(Array(counter)).map((c, index) => (
<input
type="text"
key={index}
//Changes made here
value={item[index]}
label="Item"
// Changes made here
onChange={(event) => setItem({...item, [index]:event.target.value })}
/>
))}
<button onClick={handleCounter}>Add one more item</button>
</>
);
}
Instead of using an array to store the input values I recommend using an object as it's more straight-forward.
If you wanted to use an array you can replace the onChange event with the following:
onChange={(event) => {
const clonedArray = item.slice()
clonedArray[index] = event.target.value
setItem(clonedArray)
}}
It's slightly more convoluted and probably slightly less optimal, hence why I recommend using an object.
If you want to loop through the object later you can just use Object.entries() like so:
[...Object.entries(item)].map(([key, value]) => {console.log(key, value)})
Here's the documentation for Object.entries().

codeSolution: https://codesandbox.io/s/snowy-cache-dlnku?file=/src/App.js
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [item, setItem] = useState(["a", "b"]);
const handleCounter = () => {
console.log(item, "item");
setItem([...item, ""]);
};
const setInput = (index) => (evt) => {
item.splice(index, 1, evt.target.value);
setItem([...item]);
};
return (
<>
{item.map((c, index) => {
return (
<input
type="text"
key={index}
label="Item"
value={c}
onChange={setInput(index)}
/>
);
})}
<button onClick={handleCounter}>Add one more item</button>
</>
);
}
I have solved for you . check if this works for you , if any issues tell me

Related

React Hooks: state variable having wrong value in event handlers, not able to type in input

Below, i am rendering <App/> component with children as <Input/> component array. I added few inputs using "add new" button. I am able to add input text components. But, when i am typing value in text, it is not displaying. i am not able to modify object in state array since index is showing as "-1" in setData function. Due to this, value is not showing when we type in text box. Please let me know why state is [] when i am accessing in setData function.
function Input(props)
{
return (
<div>
<label htmlFor='variable'>Name</label>
<input id='variable'
type='text'
value={props.value}
onChange={(e) => props.setData(props.id, e.target.value)} />
</div>
)
}
function App()
{
let [state, setState] = React.useState([])
let [inputs, setInputs] = React.useState([])
let setData = ((id, value) =>
{
console.log(state); // prints []
let index = state.findIndex(ele => ele.key === id);
console.log(index); // prints -1
if (!(index === -1))
{
setState(state =>
{
state[idx]["value"] = value;
})
}
})
let handleAdd = () =>
{
let idx = `${new Date().getTime()}`
let tempState = {
"key": idx,
"value": "",
}
setState(state => [...state, tempState])
let input = <Input key={tempState.key}
value={tempState.value}
id={tempState.key}
setData={setData} />
setInputs(inputs => [...inputs, input])
}
return (
<div>
<button onClick={handleAdd}>add new</button>
<div>
{inputs}
</div>
</div>
)
}
When you create an Input component inside handleAdd, it creates a closure and as a result setData gets the state that existed when the component was created, missing the newly added state.
In general, creating components and saving them to state is not a good approach. Instead it's better to only save the data onto state and render the components based on it.
Here's one way to do this, note how much simpler the component and its logic are.
function App() {
let [state, setState] = React.useState([]);
let setData = (id, value) => {
const newState = state.map((st) => {
if (st.key === id) {
st.value = value;
}
return st;
});
setState(newState);
};
const addInput = () => {
const idx = `${new Date().getTime()}`;
setState([...state, { key: idx, value: '' }]);
};
return (
<div>
<button onClick={addInput}>add new</button>
<div>
{state.map((st) => (
<Input value={st.value} key={st.key} setData={setData} id={st.key} />
))}
</div>
</div>
);
}

How can I create an array of states in React using hooks?

I have a number, n, that can be any value and I want to render n input fields while keeping track of each input's state but I'm having trouble figuring out how. For example, if n = 3, I want to render something like this:
<div>
<input onChange={(e) => setValue1(e.target.value)}/>
<input onChange={(e) => setValue2(e.target.value)}/>
<input onChange={(e) => setValue3(e.target.value)}/>
< /div>
In this example, I would manually need to create three states: value1, value2, value3. My goal is to have it dynamic so if in the future I change n to 4 or any other number, I don't have to manually create more states and mess with the component. Is there a good way to accomplish this using hooks?
You have to create a inputs state in order to track every input:
import React, { useState } from 'react';
import './style.css';
export default function App() {
const [inputs, setInputs] = useState(Array(10).fill(''));
const inputChangedHandler = (e, index) => {
const inputsUpdated = inputs.map((input, i) => {
if (i === index) {
return e.target.value;
} else {
return input;
}
});
setInputs(inputsUpdated);
};
return (
<div>
{inputs.map((input, i) => (
<input onChange={e => inputChangedHandler(e, i)} value={input} />
))}
</div>
);
}
You can check here, how things work:
https://stackblitz.com/edit/react-sdzoqh
You can create a new array with useState hook of size num that is passed from its parent and then using its index i.e. i you can change its input value using setValue function.
CODESANDBOX DEMO
Just for DEMO purpose and make the input come to a new line so I've wrapped it into div.
export default function App({ num }) {
const [arr, setValue] = useState(Array(num).fill(""));
console.log(arr);
function onInputChange(index, event) {
console.log(event.target.value);
setValue((os) => {
const temp = [...os];
temp[index] = event.target.value;
return temp;
});
}
return (
<div className="App">
{arr.map((n, i) => {
return (
<div key={i}>
<input onChange={(e) => onInputChange(i, e)} value={n} />
</div>
);
})}
</div>
);
}
Maybe I would create a custom hook to generate my inputs like
import React, { useState } from "react";
const CreateInput = (n) => {
const array = new Array(n).fill("");
const [valueInput, setValueInput] = useState({});
const handleChange = (event) => {
const { name, value } = event.target;
setValueInput({
...valueInput,
[name]: value,
});
};
const Input = array.map((_, i) => (
<input key={i} name={i} onChange={handleChange} />
));
return {
Input,
};
};
const Inputs = () => {
const { Input } = CreateInput(3);
console.log(Input);
return <div>{Input}</div>;
};
export default Inputs;
This could be done with an array in the state, with the values in the inputs. Initialize with empty strings
const [values, setValues] = useState(Array(n).fill(""))
const handleChange = (e, i) => {
const copy = values;
copy[i] = e.target.value
setValues(copy)
}
return (
<div>
{Array(n).map((x,i) => (
<input value={values[i]} onChange={e => handleChange(e,i)} />
))}
</div>
)
you can use useState([]) with array as default value for example
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const n = 3;
const [values, setValues] = useState(new Array(n).fill(1, 0, n));
const handleChange = (i, value) => {
const v = [...values];
v[i] = value;
setValues(v);
};
const inputsRendrer = (values) => {
return values.map((v, i) => {
return (
<input
key={i}
value={values[i]}
onChange={(event) => handleChange(i, event.target.value)}
/>
);
});
};
return <div className="App">{inputsRendrer(values)}</div>;
}
new Array(n).fill(1, 0, n) // this create new array with values of 1 and length of n`

How do i concat an array with a different array which uses of local storage

Datalist is an array I'm trying to concat the boards array with the Datalist array, but when I console it doesn't reflect. On the other hand when I assign Datalist.concat(boards) to a variable it reflects example
const newArr = Datalist.concat(boards);
console.log(newArr)
(main code) please help me review it. Thanks in advance
import React, { useState, useEffect } from 'react';
import Modal from './Modal';
import { Datalist } from '../Data/Boards';
function Boards() {
const [boards, setboards] = useState(JSON.parse(localStorage.getItem('boards')) || []);
const [title, settitle] = useState('');
localStorage.setItem('boards', JSON.stringify(boards));
Datalist.concat(boards);
console.log(Datalist);
const handleChange = (e) => {
settitle(e.target.value);
};
const handleSubmit = () => {
if (title.length === 0) {
return;
}
setboards((prev) => [...prev, title]);
};
return (
<div>
<ul id="boards">
<BoardList boards={boards} />
</ul>
<Modal title={title} handleChange={handleChange} handleSubmit={handleSubmit} />
</div>
);
}
function BoardList({ boards }) {
const history = useHistory();
return (
<>
{boards.map((board, index) => (
<li
key={index}
onClick={() => {
history.push('./workspace');
}}
>
<h3>{board}</h3>
</li>
))}
</>
);
}
export default Boards;
That is the expected behaviour. The concat function does not alter the original arrays. You can read about it in the MDN docs
For your case you should be able to do Datalist = Datalist.concat(boards); and it should work like you're expecting

Force a single re-render with useSelector

This is a follow-up to Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function
I've declared a functional component Parameter that pulls in values from actions/reducers using the useSelector hook:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
}
);
};
const classes = useStyles();
return (
<div>
{drawerOpen ? (
Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterCurrent[key]}//This is where the change should be reflected in the radio button
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
) : (
<div />
)
}
</div >
)
};
export default Parameter;
What I need to have happen is for value={parameterCurrent[key]} to rerender on handleParameterChange (the handleChange does update the underlying dashboard data, but the radio button doesn't show as being selected until I close the main component and reopen it). I thought I had a solution where I forced a rerender, but because this is a smaller component that is part of a larger one, it was breaking the other parts of the component (i.e. it was re-rendering and preventing the other component from getting state/props from it's reducers). I've been on the internet searching for solutions for 2 days and haven't found anything that works yet. Any help is really apprecaited! TIA!
useSelector() uses strict === reference equality checks by default, not shallow equality.
To use shallow equal check, use this
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Read more
Ok, after a lot of iteration, I found a way to make it work (I'm sure this isn't the prettiest or most efficient, but it works, so I'm going with it). I've posted the code with changes below.
I added the updateState and forceUpdate lines when declaring the overall Parameter function:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
Then added the forceUpdate() line here:
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
forceUpdate() //added here
}
);
};
Then called forceUpdate in the return statement on the item I wanted to re-render:
<RadioGroup
aria-label="parameter"
name="parameter"
value={forceUpdate, parameterCurrent[key]}//added forceUpdate here
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
I've tested this, and it doesn't break any of the other code. Thanks!

How can I use multiple refs for an array of elements with hooks?

As far as I understood I can use refs for a single element like this:
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>
How can I implement this for an array of elements? Obviously not like that: (I knew it even I did not try it:)
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>
I have seen this and hence this. But, I'm still confused about how to implement that suggestion for this simple case.
As you cannot use hooks inside loops, here is a solution in order to make it work when the array changes over the time.
I suppose the array comes from the 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>
));
}
A ref is initially just { current: null } object. useRef keeps the reference to this object between component renders. current value is primarily intended for component refs but can hold anything.
There should be an array of refs at some point. In case the array length may vary between renders, an array should scale accordingly:
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>
);
This piece of code can be optimized by unwrapping useEffect and replacing useState with useRef but it should be noted that doing side effects in render function is generally considered a bad practice:
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>
);
Update
New React Doc shows a recommended way by using map.
Check the Beta version here (Dec, 2022)
There are two ways
use one ref with multiple current elements
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>
use an Array of ref
As the above post said, it's not recommended since the official guideline (and the inner lint check) won't allow it to pass.
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.
However, since it's not our current case, the demo below still works, only not recommended.
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>
The simplest and most effective way is to not use useRef at all. Just use a callback ref that creates a new array of refs on every 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>
Note that you shouldn't use useRef in a loop for a simple reason: the order of used hooks does matter!
The documentation says
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
But consider that it obviously applies to dynamic arrays... but if you're using static arrays (you ALWAYS render the same amount of components) don't worry too much about that, be aware of what you're doing and leverage it 😉
You can use an array(or an object) to keep track of all the refs and use a method to add ref to the array.
NOTE: If you are adding and removing refs you would have to empty the array every render cycle.
import React, { useRef } from "react";
const MyComponent = () => {
// intialize as en empty array
const refs = useRefs([]); // or an {}
// Make it empty at every render cycle as we will get the full list of it at the end of the render cycle
refs.current = []; // or an {}
// since it is an array we need to method to add the refs
const addToRefs = el => {
if (el && !refs.current.includes(el)) {
refs.current.push(el);
}
};
return (
<div className="App">
{[1,2,3,4].map(val => (
<div key={val} ref={addToRefs}>
{val}
</div>
))}
</div>
);
}
working example
https://codesandbox.io/s/serene-hermann-kqpsu
Assuming that your array contains non primitives, you could use a WeakMap as the value of the Ref.
function MyComp(props) {
const itemsRef = React.useRef(new WeakMap())
// access an item's ref using itemsRef.get(someItem)
render (
<ul>
{props.items.map(item => (
<li ref={el => itemsRef.current.set(item, el)}>
{item.label}
</li>
)}
</ul>
)
}
I use the useRef hook to create panels of data that I want to control independently. First I initialize the useRef to store an array:
import React, { useRef } from "react";
const arr = [1, 2, 3];
const refs = useRef([])
When initializing the array we observe that it actually looks like this:
//refs = {current: []}
Then we apply the map function to create the panels using the div tag which we will be referencing, adds the current element to our refs.current array with one button to review:
arr.map((item, index) => {
<div key={index} ref={(element) => {refs.current[index] = element}}>
{item}
<a
href="#"
onClick={(e) => {
e.preventDefault();
onClick(index)
}}
>
Review
</a>
})
Finally a function that receives the index of the pressed button we can control the panel that we want to show
const onClick = (index) => {
console.log(index)
console.log(refs.current[index])
}
Finally the complete code would be like this
import React, { useRef } from "react";
const arr = [1, 2, 3];
const refs = useRef([])
//refs = {current: []}
const onClick = (index) => {
console.log(index)
console.log(refs.current[index])
}
const MyPage = () => {
const content = arr.map((item, index) => {
<div key={index} ref={(element) => {refs.current[index] = element}}>
{item}
<a
href="#"
onClick={(e) => {
e.preventDefault();
onClick(index)
}}
>
Review
</a>
})
return content
}
export default MyPage
It works for me! Hoping that this knowledge will be of use to you.
All other options above are relying on Arrays but it makes things extremely fragile, as elements might be reordered and then we don't keep track of what ref belongs to what element.
React uses the key prop to keep track of items. Therefore if you store your refs by keys there won't be any problem :
const useRefs = () => {
const refs = useRef<Record<string,HTMLElement | null>>({})
const setRefFromKey = (key: string) => (element: HTMLElement | null) => {
refs.current[key] = element;
}
return {refs: refs.current, setRefFromKey};
}
const Comp = ({ items }) => {
const {refs, setRefFromKey} = useRefs()
const refsArr = Object.values(refs) // your array of refs here
return (
<div>
{items.map(item => (
<div key={item.id} ref={setRefFromKey(item.id)}/>
)}
</div>
)
}
Note that React, when unmounting an item, will call the provided function with null, which will set the matching key entry to null in the object, so everything will be up-to-date.
If I understand correctly, useEffect should only be used for side effects, for this reason I chose instead to use useMemo.
const App = props => {
const itemsRef = useMemo(() => Array(props.items.length).fill().map(() => createRef()), [props.items]);
return props.items.map((item, i) => (
<div
key={i}
ref={itemsRef[i]}
style={{ width: `${(i + 1) * 100}px` }}>
...
</div>
));
};
Then if you want to manipulate the items / use side effects you can do something like:
useEffect(() => {
itemsRef.map(e => e.current).forEach((e, i) => { ... });
}, [itemsRef.length])
import React, { useRef } from "react";
export default function App() {
const arr = [1, 2, 3];
const refs = useRef([]);
return (
<div className="App">
{arr.map((item, index) => {
return (
<div
key={index}
ref={(element) => {
refs.current[index] = element;
}}
>
{item}
</div>
);
})}
</div>
);
}
Credits: https://eliaslog.pw/how-to-add-multiple-refs-to-one-useref-hook/
React will re-render an element when its ref changes (referential equality / "triple-equals" check).
Most answers here do not take this into account. Even worse: when the parent renders and re-initializes the ref objects, all children will re-render, even if they are memoized components (React.PureComponent or React.memo)!
The solution below has no unnecessary re-renders, works with dynamic lists and does not even introduce an actual side effect. Accessing an undefined ref is not possible. A ref is initialized upon the first read. After that, it remains referentially stable.
const useGetRef = () => {
const refs = React.useRef({})
return React.useCallback(
(idx) => (refs.current[idx] ??= React.createRef()),
[refs]
)
}
const Foo = ({ items }) => {
const getRef = useGetRef()
return items.map((item, i) => (
<div ref={getRef(i)} key={item.id}>
{/* alternatively, to access refs by id: `getRef(item.id)` */}
{item.title}
</div>
))
}
Caveat: When items shrinks over time, unused ref objects will not be cleaned up. When React unmounts an element, it will correctly set ref[i].current = null, but the "empty" refs will remain.
You can avoid the complexity array refs bring in combination with useEffect by moving the children into a separate component. This has other advantages the main one being readability and making it easier to maintain.
const { useRef, useState, useEffect } = React;
const ListComponent = ({ el }) => {
const elRef = useRef();
const [elWidth, setElWidth] = useState();
useEffect(() => {
setElWidth(elRef.current.offsetWidth);
}, []);
return (
<div ref={elRef} style={{ width: `${el * 100}px` }}>
Width is: {elWidth}
</div>
);
};
const App = () => {
return (
<div>
{[1, 2, 3].map((el, i) => (
<ListComponent key={i} el={el} />
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
We can't use state because we need the ref to be available before the render method is called.
We can't call useRef an arbitrary number of times, but we can call it once:
Assuming arr is a prop with the array of things:
const refs = useRef([]);
// free any refs that we're not using anymore
refs.current = refs.current.slice(0, arr.length);
// initialize any new refs
for (let step = refs.current.length; step < arr.length; step++) {
refs.current[step] = createRef();
}
You can use a father element to get a bounch of children elements.
In my case i was trying to get a bounch of inputs inside my form element then i get the form element and use it to handle with all the inputs.
Somthing like that:
function Foo() {
const fields = useRef<HTMLFormElement>(null);
function handlePopUp(e) {
e.preventDefault();
Array.from(fields.current)
.forEach((input: HTMLInputElement | HTMLTextAreaElement) => {
input.value = '';
});
}
return (
<form onSubmit={(e) => handlePopUp(e)} ref={fields}>
<input
placeholder="Nome"
required
id="name"
type="text"
name="name"
/>
<input
placeholder="E-mail"
required
id="email"
type="email"
name="email"
/>
<input
placeholder="Assunto"
required
id="subject"
type="text"
name="subject"
/>
<textarea
cols={120}
placeholder="Descrição"
required
id="description"
name="description"
/>
<button type="submit" disabled={state.submitting}>enviar</button>
</form>
);
}
We can use an array ref to memoize the ref list:
import { RefObject, useRef } from 'react';
type RefObjects<T> = RefObject<T>[];
function convertLengthToRefs<T>(
length: number,
initialValue: T | null,
): RefObjects<T> {
return Array.from(new Array(length)).map<RefObject<T>>(() => ({
current: initialValue,
}));
}
export function useRefs<T>(length: number, initialValue: T | null = null) {
const refs = useRef<RefObjects<T>>(convertLengthToRefs(length, initialValue));
return refs.current;
}
It is a demo:
const dataList = [1, 2, 3, 4];
const Component: React.FC = () => {
const refs = useRefs<HTMLLIElement>(dataList.length, null);
useEffect(() => {
refs.forEach((item) => {
console.log(item.current?.getBoundingClientRect());
});
}, []);
return (
<ul>
{dataList.map((item, index) => (
<li key={item} ref={refs[index]}>
{item}
</li>
))}
</ul>
);
};
import { createRef } from "react";
const MyComponent = () => {
const arrayOfElements = Array.from({ length: 10 }).map((_, idx) => idx + 1);
const refs = arrayOfElements.map(() => createRef(null));
const onCLick = (index) => {
ref[index]?.current?.click();
};
return (
<div>
<h1>Defaults Elements</h1>
{arrayOfElements.map((element, index) => (
<div key={index} ref={refs[index]}>
Default Elemnt {element}
</div>
))}
<h2>Elements Handlers</h2>
{arrayOfElements.map((_, index) => (
<button key={index} onClick={() => onCLick(index)}>
Element {index + 1} Handler
</button>
))}
</div>
);
};

Categories