How target DOM with react useRef in map - javascript

I looking for a solution about get an array of DOM elements with react useRef() hook.
example:
const Component = () =>
{
// In `items`, I would like to get an array of DOM element
let items = useRef(null);
return <ul>
{['left', 'right'].map((el, i) =>
<li key={i} ref={items} children={el} />
)}
</ul>
}
How can I achieve this?

useRef is just partially similar to React's ref(just structure of object with only field of current).
useRef hook is aiming on storing some data between renders and changing that data does not trigger re-rendering(unlike useState does).
Also just gentle reminder: better avoid initialize hooks in loops or if. It's first rule of hooks.
Having this in mind we:
create array and keep it between renders by useRef
we initialize each array's element by createRef()
we can refer to list by using .current notation
const Component = () => {
let refs = useRef([React.createRef(), React.createRef()]);
useEffect(() => {
refs.current[0].current.focus()
}, []);
return (<ul>
{['left', 'right'].map((el, i) =>
<li key={i}><input ref={refs.current[i]} value={el} /></li>
)}
</ul>)
}
This way we can safely modify array(say by changing it's length). But don't forget that mutating data stored by useRef does not trigger re-render. So to make changing length to re-render we need to involve useState.
const Component = () => {
const [length, setLength] = useState(2);
const refs = useRef([React.createRef(), React.createRef()]);
function updateLength({ target: { value }}) {
setLength(value);
refs.current = refs.current.splice(0, value);
for(let i = 0; i< value; i++) {
refs.current[i] = refs.current[i] || React.createRef();
}
refs.current = refs.current.map((item) => item || React.createRef());
}
useEffect(() => {
refs.current[refs.current.length - 1].current.focus()
}, [length]);
return (<>
<ul>
{refs.current.map((el, i) =>
<li key={i}><input ref={refs.current[i]} value={i} /></li>
)}
</ul>
<input value={refs.current.length} type="number" onChange={updateLength} />
</>)
}
Also don't try to access refs.current[0].current at first rendering - it will raise an error.
Say
return (<ul>
{['left', 'right'].map((el, i) =>
<li key={i}>
<input ref={refs.current[i]} value={el} />
{refs.current[i].current.value}</li> // cannot read property `value` of undefined
)}
</ul>)
So you either guard it as
return (<ul>
{['left', 'right'].map((el, i) =>
<li key={i}>
<input ref={refs.current[i]} value={el} />
{refs.current[i].current && refs.current[i].current.value}</li> // cannot read property `value` of undefined
)}
</ul>)
or access it in useEffect hook. Reason: refs are bound after element is rendered so during rendering is running for the first time it is not initialized yet.

I'll expand on skyboyer's answer a bit. For performance optimization (and to avoid potential weird bugs), you might prefer to use useMemo instead of useRef. Because useMemo accepts a callback as an argument instead of a value, React.createRef will only be initialized once, after the first render. Inside the callback you can return an array of createRef values and use the array appropriately.
Initialization:
const refs= useMemo(
() => Array.from({ length: 3 }).map(() => createRef()),
[]
);
Empty array here (as a second argument) tells React to only initialize refs once. If ref count changes you may need to pass [x.length] as "a deps array" and create refs dynamically: Array.from({ length: x.length }).map(() => createRef()),
Usage:
refs[i+1 % 3].current.focus();

take the parent Reference and manipulate the childrens
const Component = () => {
const ulRef = useRef(null);
useEffect(() => {
ulRef.current.children[0].focus();
}, []);
return (
<ul ref={ulRef}>
{['left', 'right'].map((el, i) => (
<li key={i}>
<input value={el} />
</li>
))}
</ul>
);
};
I work this way and I think that's more simple than other proposed answers.

Instead of using array of refs or something like that, you can seperate each map item to component. When you seperate them, you can use useRefs independently:
const DATA = [
{ id: 0, name: "John" },
{ id: 1, name: "Doe" }
];
//using array of refs or something like that:
function Component() {
const items = useRef(Array(DATA.length).fill(createRef()));
return (
<ul>
{DATA.map((item, i) => (
<li key={item.id} ref={items[i]}>
{item.name}
</li>
))}
</ul>
);
}
//seperate each map item to component:
function Component() {
return (
<ul>
{DATA.map((item, i) => (
<MapItemComponent key={item.id} data={item}/>
))}
</ul>
);
}
function MapItemComponent({data}){
const itemRef = useRef();
return <li ref={itemRef}>
{data.name}
</li>
}

If you know the length of the array ahead of time, to which you do in your example you can simply create an array of refs and then assign each one by their index:
const Component = () => {
const items = Array.from({length: 2}, a => useRef(null));
return (
<ul>
{['left', 'right'].map((el, i)) => (
<li key={el} ref={items[i]}>{el}</li>
)}
</ul>
)
}

I had a problem like this and read 'Joer's answer and realised you can just loop through using the index setting the querySelector class dynamically and set only one ref to the overall parent. Apologies for the load of code but hope this helps someone:
import React, { useRef, useState } from 'react';
import { connectToDatabase } from "../util/mongodb";
export default function Top({ posts }) {
//const [count, setCount] = useState(1);
const wrapperRef = useRef(null);
const copyToClipboard = (index, areaNumber) => {
//
// HERE I AM USING A DYNAMIC CLASS FOR THE WRAPPER REF
// AND DYNAMIC QUERY SELECTOR, THEREBY ONLY NEEDING ONE REF ON THE TOP PARENT
const onePost = wrapperRef.current.querySelector(`.index_${index}`)
const oneLang = onePost.querySelectorAll('textarea')[areaNumber];
oneLang.select();
document.execCommand('copy');
};
var allPosts = posts.map((post, index) => {
var formattedDate = post.date.replace(/T/, ' \xa0\xa0\xa0').split(".")[0]
var englishHtml = post.en1 + post.en2 + post.en3 + post.en4 + post.en5;
var frenchHtml = post.fr1 + post.fr2 + post.fr3 + post.fr4 + post.fr5;
var germanHtml = post.de1 + post.de2 + post.de3 + post.de4 + post.de5;
return (
<div className={post.title} key={post._id}>
<h2>{formattedDate}</h2>
<h2>{index}</h2>
<div className={"wrapper index_" + index}>
<div className="one en">
<h3>Eng</h3>
<button onClick={() => {copyToClipboard(index, 0)}}>COPY</button>
<textarea value={englishHtml} readOnly></textarea>
</div>
<div className="one fr">
<h3>Fr</h3>
<button onClick={() => {copyToClipboard(index, 1)}}>COPY</button>
<textarea value={frenchHtml} readOnly></textarea>
</div>
<div className="one de">
<h3>De</h3>
<button onClick={() => {copyToClipboard(index, 2)}}>COPY</button>
<textarea value={germanHtml} readOnly></textarea>
</div>
</div>
</div>
)
})
return (
<div ref={wrapperRef}>
<h1>Latest delivery pages </h1>
<ul>
{allPosts}
</ul>
<style jsx global>{`
body{
margin: 0;
padding: 0;
}
h1{
padding-left: 40px;
color: grey;
font-family: system-ui;
font-variant: all-small-caps;
}
.one,.one textarea {
font-size: 5px;
height: 200px;
width: 300px;
max-width: 350px;
list-style-type:none;
padding-inline-start: 0px;
margin-right: 50px;
margin-bottom: 150px;
}
h2{
font-family: system-ui;
font-variant: all-small-caps;
}
.one h3 {
font-size: 25px;
margin-top: 0;
margin-bottom: 10px;
font-family: system-ui;
}
.one button{
width: 300px;
height: 40px;
margin-bottom: 10px;
}
#media screen and (min-width: 768px){
.wrapper{
display: flex;
flex-direction: row;
}
}
`}</style>
</div>
);
}

In this case you would need to create an array of empty refs and push the refs while they are generated inside the React component.
You would then use useEffect to handle the corresponding refs.
Here's an example:
const refs = []
useEffect(() => {
refs.map(ref => {
console.log(ref.current)
// DO SOMETHING WITH ref.current
})
}, [refs])
{postsData.map((post, i) => {
refs.push(React.createRef())
return (
<div ref={refs[i]} key={post.id}>{...}</div>
)}
}

Related

selecting each individual button from the to toggle a css class but when I do only the last item in the list is shown in react

The following is the code for my component
import React from 'react'
import Wrapper from '../../assets/css/LandingPageSmallerSectionCss/faqSectioncss'
// import FaqComponent from '../../components/faqComponent'
import faqs from '../../utils/faqs'
const FaqSection = () => {
const ref = React.createRef();
const toggleHandler = (e) => {
e.preventDefault()
ref.current.classList.toggle('.show-more')
}
return (
<Wrapper>
<div className="faq-container">
<h1 className="faqs-header" style={{ color: "rgb(40, 102, 129)" }}>FAQs</h1>
<div className="list-container">
<ul>
{faqs.map((faq) => {
const { id, question, answer } = faq
return (
<li key={id}>
<p>{question}</p>
<button className="toggle-more-btn" onClick={toggleHandler}></button>
<p className="faq-text" ref={ref}>{answer}</p>
</li>
)
})}
</ul>
</div>
</div>
</Wrapper>
)
}
export default FaqSection
I can implement the code for toggling in pure javascript and plain html but i'm having trouble translating it to react since react does not have queryselectorAll hence i'm having trouble,the following is the code for the pure javascript
let listContent= document.querySelectorAll('.faq-text')
let buttonContent = document.querySelectorAll('.toggle-more-btn')
const listArray=Array.from(listContent)
const buttonArray=Array.from(buttonContent)
//display both array list
// console.log(listArray)
// console.log(buttonArray)
//check if individual examples buttons are coherent with each other
// if (buttonArray[0] && listArray[0]) {
// buttonArray[0].addEventListener('click',(e)=>{
// listArray[0].classList.toggle('poopoo')
// })
// }
// if (buttonArray[1] && listArray[1]) {
// buttonArray[1].addEventListener('click',(e)=>{
// listArray[1].classList.toggle('poopoo')
// })
// }
//loop through all buttons in the list and if both the index of the button and list match
// for (let i = 0; i < buttonArray.length; i++) {
// if (buttonArray[i]&&listArray[i]) {
// buttonArray[i].addEventListener('click',e=>{
// listArray[i].classList.toggle('poopoo')
// })}
// }
// create funcion that works with individual array indexes
//experimental
const buttonPress=(button,list)=>{
for (let i = 0; i < button.length; i++) {
if (button[i]&&list[i]) {
button[i].addEventListener('click',e=>{
list[i].classList.toggle('show-more')
button[i].classList.toggle('rotate-btn')
})
}
}
}
buttonPress(buttonArray,listArray)
I've spent hours on this.
If anyone could answer me i'd be greatfull,kindly?
It is a couple things so far:
Handler function is not named the same as what is called.
The DOT on your css class .show-more! It took awhile (and a console.log to find this).
I don't think you want a ref in the first place.
Here is what I might do using some hooks. I'm faking first load with the useEffect, with empty array arg.
Then, I'm going to set the changed faq entry in a clone of the list "faqs", and then spread my temp array into the setFaqs. This is using the useState hook, which generally you give a variable name and then a setter function name (and for these built-ins React takes care of actually setting up in the background - including the default I have an empty array).
import { useEffect, useState } from "react";
import "./styles.css";
const mockServerReturn = [
{ id: 0, question: "Foo?", answer: "Bar!" },
{ id: 1, question: "Baz?", answer: "Bar!" },
{ id: 2, question: "Bar?", answer: "Bar!" }
];
const FaqSection = () => {
const [faqs, setFaqs] = useState([]);
useEffect(() => setFaqs(mockServerReturn), []);
const toggleHandler = (e) => {
e.preventDefault();
const temp = [...faqs];
temp[e.target.id].chosen = !temp[e.target.id].chosen;
setFaqs([...temp]);
};
return (
<>
<div className="faq-container">
<h1 className="faqs-header">FAQs</h1>
<div className="list-container">
<ul>
{faqs.map((faq) => {
const { id, question, answer, chosen } = faq;
return (
<li key={id}>
<p>{question}</p>
<button
className="toggle-more-btn"
onClick={toggleHandler}
id={id}
>
I choose {id}
</button>
<p className={`faq-text${chosen ? " show-more" : ""}`}>
{id}:{answer}:{chosen?.toString()}
</p>
</li>
);
})}
</ul>
</div>
</div>
</>
);
};
export default function App() {
return (
<div className="App">
<FaqSection />
</div>
);
}

get all elements with specific Classname in React

I want to get all elements with the classname selected from this component
function ChooseElements() {
const listItems = elementObjects.map((object) =>
<ListItem key={object.id.toString()} value={object.Element} />
);
return (
<div> <ul>
{listItems}
</ul>
<button onClick={ console.log("get all list items")}>get Elements</button>
</div>
);
}
in plain js i could use document.getElementsByClassName('selected') to get all elements
I have read somewhere that useRef should be used
to access the elements in the virtual dom, is this correct and how can I do this ?
i think you can use document.querySelectorAll('.list-item') but you need to assign a classname first. example my class is list-item so you can get the DOM elements.
You should probably see it from a different perspective. You can lift your state one level up to your <ChooseElements /> component.
This is the practice encouraged by react for similar problems. here
function ChooseElements() {
const [selectedItems, setSelectedItems] = useState([]);
const handleItemSelect = item => {
setSelectedItems([...selectedItems, item]);
}
const handleItemUnselect = item => {
setSelectedItems(selectedItems.filter(i => i !== item));
}
const listItems = elementObjects.map((object) =>
<ListItem
key={object.id.toString()}
value={object.Element}
onSelect={() => handleItemSelect(object)}
onUnselect={() => handleItemUnselect(object)}
/>
);
return (
<div>
<ul>
{listItems}
</ul>
<button onClick={() => console.log(selectedItems)}>get Elements</button>
</div>
);
}
However, if ONLY AND ONLY lifting state up is not possible to you, you can use ref like this:
function ChooseElements() {
const myRef = useRef();
const listItems = elementObjects.map((object) =>
<ListItem
key={object.id.toString()}
value={object.Element}
/>
);
return (
<div>
<ul ref={myRef}>
{listItems}
</ul>
<button onClick={() => {
console.log(ref.current.querySelector('.selected'));
}}>
get Elements
</button>
</div>
);
}

Using querySelectorAll on a React component's children

I am trying to render a list of elements lazily by initially rendering only what's in view and then replacing placeholder elements with the real thing as you scroll down using an IntersectionObserver. This keeps the list's length from changing as I add new elements and is much cheaper to render as I'm only listing empty divs when the page loads. Like a poor man's virtualization.
The issue:
The parent element adds its children to the IO as so:
useEffect(() => {
if (!observer.current) return
const els = [...document.querySelectorAll(`.list > :nth-child(n + 10})`)]
els.forEach(el => observer.current.observe(el))
}, [list])
els does not always find elements as React renders its elements as it sees fit. The problem is that I don't know how I could do this using ref. Using context I may be able to do this but I'd imagine there would be constant rerendering of the entire list.
I hope this example can help you understand how to use Refs in your case. I recommend you to read React Docs about Refs.
Please let me know if you have any doubt.
const MyItem = React.forwardRef((props, ref) => {
return (<div ref={ref}>{'ITEM ' + props.index}</div>);
});
const MyList = () => {
const [list, setList] = React.useState([]);
const refsMap = React.useRef(new Map());
const onClickHandler = React.useCallback(() => {
setList((previousList) => {
const key = previousList.length;
return ([
...previousList,
<MyItem key={key} index={key} ref={(ref) => {
if (ref) {
refsMap.current.set(key, ref);
} else {
refsMap.current.delete(key);
}
console.log(refsMap.current.get(key));
}} />
]);
});
}, []);
return (
<div>
<div>
<button type={'button'} onClick={onClickHandler}>{'ADD'}</button>
</div>
<div>
{list}
</div>
</div>
);
};
function App() {
return (
<MyList />
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<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="app"></div>

Removing an item from an array stored in state, delete button (line 55) am i missing a parameter?

Code is in app component (I know that's bad) help would be greatly appreciated. I don't know what I'm doing. Everything works at least except for delete button, trying to remove an item from the list after its been added and can't figure it out. They're asking me to add more details but I don't really know what else to say.
import React, {useState, useEffect} from 'react';
import './App.css';
function App() {
const [memories, setMemories] = useState([]);
const [newItem, setNewItem] = useState('');
const dateAndTime = new Date();
const dateString = dateAndTime.toLocaleDateString();
const [allowSubmit, setAllowSubmit] = useState(false);
const handleSubmit = (evt) => {
evt.preventDefault();
if (allowSubmit === true) {
setMemories([...memories,{
id : memories.length,
value : newItem,
date : dateString
}]);
setNewItem('');
setAllowSubmit(false);
}
}
const clearAllMemories = () => {
setMemories([]);
localStorage.setItem('memories',JSON.stringify(memories));
}
useEffect(() => {
const memoriesData = localStorage.getItem('memories');
if (memoriesData) {
setMemories(JSON.parse(memoriesData));
}
},[]);
useEffect(() => {
localStorage.setItem('memories',JSON.stringify(memories));
});
return (
<div className='App'>
<h2>Favorite moments of the day</h2>
<form onSubmit={handleSubmit}>
<textarea className='text-area'
type='text'
value={newItem}
onChange={e => {
setAllowSubmit(true);
setNewItem(e.target.value)
}}
/>
<input className='input-button'type="submit" value="Add Memory"/>
</form>
<h3 className='memories-title'>Memories</h3>
<div className='memories'>
<ul className='ul'>
{memories.map(memories => (
<li key = {memories.id}>{memories.date}{memories.value}
<button onClick={() => {
setMemories(memories.value.split(memories.value, 1));
}}>delete</button>
</li>
))}
</ul>
</div> <br/>
<button className='clear-button'
onClick={clearAllMemories}
>
Clear All Memories
</button>
</div>
);
}
export default App;
I'm seeing something that could be the problem.
<ul className='ul'>
{memories.map(memories => (
<li key = {memories.id}>{memories.date}{memories.value}
<button onClick={() => {
setMemories(memories.value.split(memories.value, 1));
}}>delete</button>
</li>
))}
</ul>
When using the map function with the memories array, you're assigning the array's name to the array element. And then you access memories.value.split (even though it still doesn't much sense) on the onClick callback of the button, wanting to access the array, but probably accessing the array element.
You should give the array element a different name, like memory or memoryElement, to avoid confusion.
When calling the split function (which from looking to the arguments I think you meant to write splice) you would call it directly in the memories array, not memories.value.
I don't know if I have understood the problem well, but this could be a solution?
<ul className='ul'>
{memories.map(memory => (
<li key = {memory.id}>{memory.date}{memory.value}
<button onClick={() => {
setMemories([...memories.filter(m => memory.id !== m.id)]);
}}>delete</button>
</li>
))}
</ul>

Two way communication between two functional React JS components

I have two functional components built in React, one is an Item component - it holds some data about stuff, with optional graphics, some text data and price information. On the bottom there is a button, that allows you to select this particular item. It also keeps information in its props on ID of currently selected Item - that's how i planned to solve this problem.
My second component is a ItemList - it basically holds a list of aforemetioned Items - plus it sorts all the items and must keep information about which component is currently selected - the selected one basically looks different - some stuff like the border box and button's colour gets switched via CSS.
My logic to implement goes like this - when user clicks on a "Select" button of a particular Item, the Item should change its look (unless it's already selected, then do nothing), after that somehow propagate info up onto the ItemList, so that it can "disable" the previously selected component. There can be only one selected Item, and once user decide to select another one, the previously selected should change its state and go back to unselected standard graphic style.
I've ran across a solution with state in the ItemList component plus passing a function via props into Item, but that doesn't solve the second part - ItemList needs to get info about a change, so it can rerender all the components according to actual state. What part of React API should I dive into to solve this issue?
Here is code for my components:
Item
interface Props {
receivedObject: itemToDisplay;
selectedItemId: string;
onClick?: () => void;
}
export default function Item(props: Props) {
const {name, description, price} = props.receivedObject;
const imageUrl = props.receivedObject?.media?.mainImage?.small?.url;
const priceComponent = <Price price={price}/>;
const [isItemSelected, setSelection] = useState(props.selectedItemId == props.receivedObject.id);
const onClick = props.onClick || (() => {
setSelection(!isItemSelected)
});
return (
<>
<div className="theDataHolderContainer">
// displayed stuff goes here
<div className="pickButtonContainer">
// that's the button which should somehow send info "upwards" about the new selected item
<Button outline={isItemSelected} color="danger" onClick={onClick}>{isItemSelected ? "SELECTED" : "SELECT"}</Button>
</div>
</div>
</>)
};
ItemList
interface Props {
packageItems: Array<itemToDisplay>
}
export default function ItemList(props: Props) {
const itemsToDisplay = props.packageItems;
itemsToDisplay.sort((a, b) =>
a.price.finalPrice - b.price.finalPrice
);
let selectedItemId = itemsToDisplay[0].id;
const [currentlySelectedItem, changeCurrentlySelectedItem] = useState(selectedItemId);
const setSelectedItemFunc = () => {
/* this function should be passed down as a prop, however it can only
* have one `this` reference, meaning that `this` will refer to singular `Item`
* how do I make it change state in the `ItemList` component?
*/
console.log('function defined in list');
};
return(
<div className="packageNameList">
<Item
key={itemsToDisplay[0].id}
receivedObject={itemsToDisplay[0]}
onClick={setSelectedItemFunc}
/>
{itemsToDisplay.slice(1).map((item) => (
<Item
key={item.id}
receivedObject={item}
onClick={setSelectedItemFunc}
/>
))}
</div>
);
}
In React, the data flows down, so you'd better hold state data in a stateful component that renders presentation components.
function ListItem({ description, price, selected, select }) {
return (
<li className={"ListItem" + (selected ? " selected" : "")}>
<span>{description}</span>
<span>${price}</span>
<button onClick={select}>{selected ? "Selected" : "Select"}</button>
</li>
);
}
function List({ children }) {
return <ul className="List">{children}</ul>;
}
function Content({ items }) {
const [selectedId, setSelectedId] = React.useState("");
const createClickHandler = React.useCallback(
id => () => setSelectedId(id),
[]
);
return (
<List>
{items
.sort(({ price: a }, { price: b }) => a - b)
.map(item => (
<ListItem
key={item.id}
{...item}
selected={item.id === selectedId}
select={createClickHandler(item.id)}
/>
))}
</List>
);
}
function App() {
const items = [
{ id: 1, description: "#1 Description", price: 17 },
{ id: 2, description: "#2 Description", price: 13 },
{ id: 3, description: "#3 Description", price: 19 }
];
return (
<div className="App">
<Content items={items} />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
.App {
font-family: sans-serif;
}
.List > .ListItem {
margin: 5px;
}
.ListItem {
padding: 10px;
}
.ListItem > * {
margin: 0 5px;
}
.ListItem:hover {
background-color: lightgray;
}
.ListItem.selected {
background-color: darkgray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

Categories

Resources