document.getElementById() does not find anything ReactJS - javascript

Hello I'm creating a progerss bar using ReactJS but I have a problem
My progress bar is going to be a React component and I will pass the progress value in the props
For some reason it seems like document.getElementById() does not find anything
import './MyProgressBar.css'
const MyProgressBar = (props) => {
const value = props.value
const progressbar = document.getElementById("hello");
progressbar.style.width = value + "%"
return (
<div class="progress">
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
It throws an error that says "Cannot read property 'style' of null at MyProgressBar (MyProgressBar.js:9)....."

The reason for the error is that the element doesn't exist yet as of when you go looking for it. You could "fix" that with a useEffect or useLayoutEffect callback, but that wouldn't be the React approach. Your component will be called to re-render when the props change, so handle rendering in the new state directly:
const MyProgressBar = ({value}) => {
return (
<div class="progress">
<div class="color" style={{width: value + "%"}}></div>
</div>
);
};
This also has the advantage that you can have multiple MyProgressBar instances in the page at the same time.
Live Example:
const {useState, useEffect} = React;
const MyProgressBar = ({value}) => {
return (
<div className="progress">
<div className="color" style={{width: value + "%"}}></div>
</div>
);
};
const App = () => {
const [bar1, setBar1] = useState(0);
const [bar2, setBar2] = useState(0);
useEffect(() => {
const t1 = setInterval(() => {
setBar1(b1 => {
if (b1 < 100) {
++b1;
return b1;
}
clearInterval(t1);
return b1;
});
}, 200);
const t2 = setInterval(() => {
setBar2(b2 => {
if (b2 < 100) {
++b2;
return b2;
}
clearInterval(t2);
return b2;
});
}, 400);
}, []);
return <div>
<div>
Every 200ms:
<MyProgressBar value={bar1} />
</div>
<div>
Every 400ms:
<MyProgressBar value={bar2} />
</div>
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.color {
background-color: blue;
height: 100%;
}
.progress {
height: 1em;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

This code is running before there is anything on the DOM.
You can use it inside a useEffect, running after the render.
useEffect(() => {
const value = props.value
const progressbar = document.getElementById("hello");
progressbar.style.width = value + "%";
}, [props.value]);
You can also use useRef

You document.getElementById is run before actual rendering, so it could not find any element with that Id
It's not reccomend to use document.getElementById in your Reactjs code, use useRef instead:
function App() {
const divRef = React.useRef();
React.useEffect(() => {
console.log(divRef.current);
}, []);
return (
<div class="progress">
<div ref={divRef} id="hello" class="color"></div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

As explained earlier document.getElementById won't work in case of react. Because it is running before actual DOM is being created.
First method is to use useEffect
import './MyProgressBar.css'
import { useEffect, useState } from 'react';
const MyProgressBar = (props) => {
const [progressbarWidth, setProgressbarWidth] = useState(0)
useEffect(() => {
const value = props.value
setProgressbarWidth(value)
}, [props.value])
return (
<div class="progress" style={{ width: `${progressbarWidth}%` }}>
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
Second method is to use useRef
import './MyProgressBar.css'
import { useEffect, useRef } from 'react';
const MyProgressBar = (props) => {
const progressbarRef = useRef()
useEffect(() => {
const value = props.value
progressbarRef.current.style.width = value + "%"
}, [props.value])
return (
<div class="progress" ref={progressbarRef}>
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar

In react you don't have to access elements by id.
I encourage you to do something like this:
import './MyProgressBar.css'
const MyProgressBar = (props) => {
const value = props.value
return (
<div class="progress">
<div style={{width: `${value}%` }}></div>
</div>
)
}
export default MyProgressBar

Related

Adding multiple elements to state with map function

I have 2 buttons, Single Component and Multiple Component.
When I click on Multiple Component, I expect it to add 3 components, but it adds only 1.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react-lite";
function App() {
const [component, setComponent] = useState([]);
useEffect(() => {});
const newArray = [1, 2, 3];
const Test = observer(() => {
return (
<div>
<p>Test</p>
</div>
);
});
const Test2 = observer(() => {
return (
<div>
<p>Test2</p>
</div>
);
});
const Test3 = observer(() => {
return (
<div>
<p>Test3</p>
</div>
);
});
async function MultipleComponent() {
newArray.map(async (x) => {
if (x === 1) {
await setComponent([...component, Test]);
} else if (x === 2) {
await setComponent([...component, Test2]);
} else {
await setComponent([...component, Test3]);
}
console.log(x);
});
}
return (
<div>
{component.map((Input, index) => (
<Input components={component} key={index} />
))}
<button onClick={() => setComponent([...component, Test])}>
Single Component
</button>
<button onClick={() => MultipleComponent()}>Multiple Component</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
codensadbox: https://codesandbox.io/s/react-hooks-useeffect-forked-edmgb5
There is no point in using await on setState, nowhere the docs say it is a good idea.
On the other hand you need to use version of setState which accepts an updater function, there you can get previous state.
setComponent(ps=>[...ps, Test2])
Also, I don't have link to official docs, but I am not sure storing components inside state is good idea either. You could store some identifier in state which indicates which component it is and then render that one when time comes. Here is what I mean by this:
let Test1 = (props) => {
return <div>1</div>;
};
let Test2 = (props) => {
return <div>2</div>;
};
let GeneralComponent = (props) => {
if (props.comp === '1') return <Test1 />;
if (props.comp === '2') return <Test2 />;
return null;
};
export default function App() {
let [comp, setComp] = React.useState('1');
return (
<div onClick={() => setComp(comp === '1' ? '2' : '1')}>
<GeneralComponent comp={comp} />
</div>
);
}
The GeneralComp accepts an identifier of which component to render, which is stored in state in parent.

Passing props/state between two functional child components in react?

I have been looking on google a lot about how to pass props between functional components but very little information seems to be out there(either that or I don't know what keywords to input into google).
I do not need redux or data stored globally, I simply want to pass a JSON object stored in a hook from one component file to another.
I have three files one is the parent and the other two are children, I want to pass the data between the children files.
Paerent
import React, { useState } from "react";
import ShoppingPageOne from "./ShoppingPageOne";
import ShoppingPageTwo from "./ShoppingPageSecond";
function ShoppingPageContainer() {
const [pageone_Open, setPageone_Open] = useState(true);
const [pagetwo_Open, setPagetwo_Open] = useState(false);
const page_showHandler = () => {
setPageone_Open(!pageone_Open);
setPagetwo_Open(!pagetwo_Open);
};
return (
<div className="Shopping_Container">
<div className="Shopping_Box">
<h2>Online food shop</h2>
<div className="Shopping_Page_Container">
<ShoppingPageOne showOne={pageone_Open} next_ClickHandler={page_showHandler} />
<ShoppingPageTwo showTwo={pagetwo_Open} Reset_Data={page_showHandler} />
</div>
</div>
</div>
);
}
export default ShoppingPageContainer;
Child one:
import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageOne = (props) => {
//element displays
const [pageone_show, setPageone_show] = useState("pageOne");
//stores quantities of items as JSON objects
const [Quantities, setQuantities] = useState({});
const [QuantiesProps, setQuantitiesProps] = useState(null)
useEffect(() => {
//sets info text using Json
if (props.showOne) {
setPageone_show("pageOne");
} else {
setPageone_show("pageOne hide");
}
}, [props.showOne]);
return (
<div className={"Shopping_Content " + pageone_show}>
{Data.map((Ingredients) => {
//updates Quanties Hook
const handleChange = (event) => {
setQuantities({
...Quantities,
[Ingredients.Name]: {
...(Quantities[Ingredients.Name] ?? {}),
quantities: event.target.value
}
});
};
return (<div className="Shopping_input" key={Ingredients.Name}>
<p>{Ingredients.Name} £{Ingredients.Price}</p>
<input onChange={handleChange.bind(this)} min="0" type="number"></input>
</div>)
})}
<div className="Shopping_Buttons">
<p onClick={props.next_ClickHandler}>Buy Now!</p>
</div>
</div>
);
};
export default ShoppingPageOne;
Child Two
import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageSecond = (props) => {
//element displays
const [pagetwo_show, setPagetwo_show] = useState("pageTwo hide");
useEffect(() => {
//resets info text
if (props.showTwo) {
setPagetwo_show("pageTwo");
} else {
setPagetwo_show("pageTwo hide");
}
}, [props.showTwo]);
return (
<div className={"Shopping_Content " + pagetwo_show}>
<div className="Shopping_Buttons">
<p onClick={props.Reset_Data}>Shop Again</p>
</div>
</div>
);
};
export default ShoppingPageSecond;import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageSecond = (props) => {
//element displays
const [pagetwo_show, setPagetwo_show] = useState("pageTwo hide");
useEffect(() => {
//resets info text
if (props.showTwo) {
setPagetwo_show("pageTwo");
} else {
setPagetwo_show("pageTwo hide");
}
}, [props.showTwo]);
return (
<div className={"Shopping_Content " + pagetwo_show}>
<div className="Shopping_Buttons">
<p onClick={props.Reset_Data}>Shop Again</p>
</div>
</div>
);
};
export default ShoppingPageSecond;
I simply want to pass the state contained in Quantities hook from Child One to Child Two when "Buy Now!" button is clicked.
What is the best approach to do doing this?
From my understand, I don't pass props between two children under the same parent. Instead, the parent holds the data, and pass the data and mutation function to children as props.
import React, { useState } from 'react';
const PageOne = ({ value, setValue }) => {
const PageOneFunction = () => {
setValue({
pageOneData: value.pageOneData + 1,
pageTwoData: value.pageTwoData + 1,
});
};
return (
<div>
<h4>Page One</h4>
<div>{value.pageOneData}</div>
<button onClick={PageOneFunction}>
Increase page one and page two value
</button>
</div>
);
};
const PageTwo = ({ value, setValue }) => {
const pageTwoFunction = () => {
setValue({
pageOneData: 0,
pageTwoData: 0,
});
};
return (
<div>
<h4>Page Two</h4>
<div>{value.pageTwoData}</div>
<button onClick={pageTwoFunction}>Reset</button>
</div>
);
};
const PageContainer = () => {
const [data, setData] = useState({
pageOneData: 0,
pageTwoData: 0,
});
return (
<div className="bg-white">
<PageOne value={data} setValue={setData} />
<PageTwo value={data} setValue={setData} />
</div>
);
};
export default PageContainer;

React Hooks - useEffect still being called even when object is empty

I have a question on React Hooks. This is a sample of my code :-
import React, { useState, useEffect } from "react";
import Card from "./Card";
const CardsBoard = () => {
useEffect(() => {
doRatingClickProcessing()
}, [ratingObj])
const doRatingClickProcessing = () => {
const { player, title, rating } = ratingObj
}
return (
<div className="container-fluid justify-content-center">
<div className="row">
<div className="col-md-6">
<Card
cardInfo={player1Card}
player={1}
showCard={visiblePl1}
clickableRatings = {clickableRatings}
onClick={ratingObj => setRatingObj(ratingObj)}
/>
</div>
<div className="col-md-6">
<Card
cardInfo={player2Card}
player={2}
showCard={visiblePl2}
clickableRatings = {clickableRatings}
onClick={ratingObj => setRatingObj(ratingObj)}
/>
</div>
</div>
)}
</div>
)
}
export default CardsBoard
Then in the card component I am returning the ratingObj successfully when the user clicks on a rating.
In the Card Component I have something like this:-
<div
className="col-md-2 text-left card-rating-color"
onClick={() =>
onClick({
player: player,
title: row[0].title,
rating: row[0].rating,
})
}
>
{row[0].rating}
</div>
However I am puzzled why useEffect() is triggered even when the Card component is loaded, and ratingObj is still empty. Shouldn't it be triggered only if the ratingObj is filled up?
Thanks for your help and time
useEffect will call at least once. it doesn't matter either your object is updating or not because when you write
useEffect(()=>{
},[ratingObj]);
In above code you are passing object into square brackets right. That means you are mentioning dependencies as e second parameter and empty [] in argument list will call once at least. After that, it depends on ratingObj that you have passed in.
import React, {useState,useMemo} from 'react';
const App = () => {
const [name, setName] = useState('');
const [modifiedName, setModifiedName] = useState('');
const handleOnChange = (event) => {
setName(event.target.value);
}
const handleSubmit = () => {
setModifiedName(name);
}
const titleName = useMemo(()=>{
console.log('hola');
return `${modifiedName} is a Software Engineer`;
},[modifiedName]);
return (
<div>
<input type="text" value={name} onChange={handleOnChange} />
<button type="button" onClick={handleSubmit}>Submit</button>
<Title name={titleName} />
</div>
);
};
export default App;
const Title = ({name}) => {
return <h1>{name}</h1>
}

How to add "refs" dynamically with react hooks?

So I have an array of data in and I am generating a list of components with that data. I'd like to have a ref on each generated element to calculate the height.
I know how to do it with a Class component, but I would like to do it with React Hooks.
Here is an example explaining what I want to do:
import React, {useState, useCallback} from 'react'
const data = [
{
text: 'test1'
},
{
text: 'test2'
}
]
const Component = () => {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
{
data.map((item, index) =>
<div ref={measuredRef} key={index}>
{item.text}
</div>
)
}
</div>
)
}
Not sure i fully understand your intent, but i think you want something like this:
const {
useState,
useRef,
createRef,
useEffect
} = React;
const data = [
{
text: "test1"
},
{
text: "test2"
}
];
const Component = () => {
const [heights, setHeights] = useState([]);
const elementsRef = useRef(data.map(() => createRef()));
useEffect(() => {
const nextHeights = elementsRef.current.map(
ref => ref.current.getBoundingClientRect().height
);
setHeights(nextHeights);
}, []);
return (
<div>
{data.map((item, index) => (
<div ref={elementsRef.current[index]} key={index} className={`item item-${index}`}>
{`${item.text} - height(${heights[index]})`}
</div>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Component />, rootElement);
.item {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
}
.item-0 {
height: 25px;
}
.item-1 {
height: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"/>
I created a tiny npm package that exposes a React Hook to handle setting and getting refs dynamically as I often run into the same problem.
npm i use-dynamic-refs
Here's a simple example.
import React, { useEffect } from 'react';
import useDynamicRefs from 'use-dynamic-refs';
const Example = () => {
const foo = ['random_id_1', 'random_id_2'];
const [getRef, setRef] = useDynamicRefs();
useEffect(() => {
// Get ref for specific ID
const id = getRef('random_id_1');
console.log(id)
}, [])
return (
<>
{/* Simple set ref. */}
<span ref={setRef('random_id_3')}></span>
{/* Set refs dynamically in Array.map() */}
{ foo.map( eachId => (
<div key={eachId} ref={setRef(eachId)}>Hello {eachId}</div>))}
</>
)
}
export default Example;
You have to use a separate set of hooks for each item, and this means you have to define a component for the items (or else you’re using hooks inside a loop, which isn’t allowed).
const Item = ({ text }) => {
const ref = useRef()
const [ height, setHeight ] = useState()
useLayoutEffect(() => {
setHeight( ref.current.getBoundingClientRect().height )
}, [])
return <div ref={ref}>{text}</div>
}

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