How to toggle class of a single element in a .map() function? - reactjs

I am trying to toggle a class for a specific element inside a loop.
const ItemList: React.FC<ListItemUserProps> = (props) => {
const { items } = props;
const [showUserOpt, setShowUserOpt] = useState<boolean>(false);
function toggleUserOpt() {
setShowUserOpt(!showUserOpt);
}
const userOptVisible = showUserOpt ? 'show' : 'hide';
return (
<>
{items.map((t) => (
<React.Fragment key={t.userId}>
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={() => toggleUserOpt()}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}
</>
);
};
export default ItemList;
When I click on an element, the class toggles for every single one.

You can create another component that can have it's own state that can be toggled without effecting other sibling components' state:
Child:
const ItemListItem: React.FC<SomeInterface> = ({ item }) => {
const [show, setShow] = useState<boolean>(false);
const userOptVisible = show ? "show" : "hide";
const toggleUserOpt = (e) => {
setShow((prevState) => !prevState);
};
return (
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={toggleUserOpt}
onKeyDown={toggleUserOpt}
>
{item.userNav.firstName}
</div>
);
};
Parent:
const ItemList: React.FC<ListItemUserProps> = ({ items }) => {
return (
<>
{items.map((t) => (
<ItemListItem key={t.userId} item={t} />
))}
</>
);
};

If you simply adding classes to the element, I would keep it simple and use a handler to toggle the class using pure JS.
const handleClick = (e) => {
// example of simply toggling a class
e.currentTarget.classList.toggle('selected');
};
Demo:
const {
useState,
} = React;
// dummy data
const data = Array(20).fill(null).map((i, index) => `item ${(index + 1).toString()}`);
function App() {
const [items, setItems] = useState(data);
const handleClick = (e) => {
e.currentTarget.classList.toggle('selected');
};
return (
<div>
{items.map((item) => (
<button key={item} onClick={handleClick}>{item}</button>
))}
</div>
);
}
ReactDOM.render( <
App / > ,
document.getElementById("app")
);
.selected {
background: red;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>

I think it'd be best if you kept track of the index so that you could target a single item in your list. As it stands the boolean is going to change the styling for all as you haven't specified which one should get the className.
Add a useState hook to keep track of it like:
const [activeIndex, setActiveIndex] = useState(null);
Then create a new function:
function handleIndexOnClick(index) {
setActive(index);
}
Then in your map() function add index. You'll then need to pass index in to you className attribute and the onClick function. The end result for that bit should look like:
{items.map((t, index) => (
<React.Fragment key={t.userId}>
<div
className={`item ${activeIndex && items[activeIndex] ? 'show' : 'hide }`}
role="button"
tabIndex={0}
onClick={() => handleIndexOnClick(index)}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}

Related

React - How to prevent parent re-render on prop change

I am making a calculator using react.
Every time I press a number button, the whole application re-renders, instead of the <Display />.
To prevent it, I tried 2 different approaches for App, But neither of them worked.
Here is the sandbox link.
Any help would be appreciated.
Put clickHandler inside of useCallback()
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = useCallback(
(val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
},
[screen]
);
return (
<div className="App">
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</div>
);
};
Put Display component inside of React.memo
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
const displayComponent = () => {
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
};
const MemoizedComponent = React.memo(displayComponent);
return (
<div className="App">
<MemoizedComponent />
</div>
);
};
And here's the ButtonList & Button component.
export const ButtonList = ({ clickHandler }) => {
const arr = [...Array.from(Array(10).keys()).reverse(), "AC"];
return (
<div className="buttons">
<div className="numbersWrapper">
{arr.map((item) => (
<Button
key={item}
clickHandler={clickHandler}
value={item.toString()}
/>
))}
</div>
</div>
);
};
export const Button = ({ value, clickHandler }) => {
return (
<button
name={value}
onClick={() => {
clickHandler(value); //where the clickEvent happens
}}
>
{value}
</button>
);
};
If you don't want a component re-render,You would have to define the click handler in another component that you would like to re-render.
So do it like this:
const App = () => {
console.log("render");
return (
<div className="App">
<childComponent />
</div>
);
};
export const childComponent = () => {
const [screen, setScreen] = useState("0");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
}
This way you prevent a particular component from re-rendering. But note that if you update a state or do anything from which causes re-renders from the parent component, It would equally re-render the child component.

useRef for element in loop in react

Using React, i have a list of ref statically declared this way:
let line1 = useRef(null);
let line2 = useRef(null);
let line3 = useRef(null);
...
//IN MY RENDER PART
<li ref={(el) => (line1 = el)}>line1</li>
<li ref={(el) => (line2 = el)}>line1</li>
<li ref={(el) => (line3 = el)}>line1</li>
the refs are then passed to an animation function and everything works correctly;
now things changed a bit and i create the list item using map and im no longer able to ref the element correctly;
i tried something like:
{menu.menu.map((D) => {
let newRef = createRef();
LiRefs.push(newRef);
return (
<li
key={D.id}
ref={(el) => (newRef = el)} >
{D.label}
</li>
);
})}
but the array i pass to the animation function is empty (i guess because the function is called inside useEffect hook and LiRefs is not yet a useRef)
i also know the number of i will create, so i can declare them at the beginning and the reference with something like
ref={(el) => (`line${i}` = el)}
which is not working
any other solution i could try?
Issue
This won't work as each render when menu is mapped it creates new react refs.
Solution
Use a ref to hold an array of generated refs, and assign them when mapping.
const lineRefs = React.useRef([]);
lineRefs.current = menu.menu.map((_, i) => lineRefs.current[i] ?? createRef());
later when mapping UI, attach the react ref stored in lineRefs at index i
{menu.menu.map((D, i) => {
return (
<li
key={D.id}
ref={lineRefs.current[i]} // <-- attach stored ref
{D.label}
</li>
);
})}
Mine is React Hooks version.
useMemo to create an array of refs for performance sake.
const vars = ['a', 'b'];
const childRefs = React.useMemo(
() => vars.map(()=> React.createRef()),
[vars.join(',')]
);
React will mount each ref to childRefs
{vars.map((v, i) => {
return (
<div>
<Child v={v} ref={childRefs[i]} />
<button onClick={() => showAlert(i)}> click {i}</button>
</div>
)
})
}
Here is a workable demo, hope that helps. ^_^
const Child = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
showAlert() {
window.alert("Alert from Child: " + props.v);
}
}));
return <h1>Hi, {props.v}</h1>;
});
const App = () => {
const vars = ['a', 'b'];
const childRefs = React.useMemo(
() => vars.map(()=> React.createRef()),
// maybe vars.length
[vars.join(',')]
);
function showAlert(index) {
childRefs[index].current.showAlert();
}
return (
<div>
{
vars.map((v, i) => {
return (
<div>
<Child v={v} ref={childRefs[i]} />
<button onClick={() => showAlert(i)}> click {i}</button>
</div>
)
})
}
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
There may be some Typescript inconsistencies and complexity in other answers. So I think the best way to use the useRef hook in a loop is:
// Declaration
const myRef = useRef([]);
myRef.current = [];
const addToRefs: (el) => void = (el) => {
if (el && !myRef.current.includes(el)) {
myRef.current.push(el);
}
};
And
// Assignment (input element example)
...
...
{anyArrayForLoop.map((item, index) => {
return (
<input
key={index}
ref={addToRefs}
/>
);
})}
...
...
Final:
// The Data
myRef.current
Instead of storing refs in an array, you could create a ref for each component within the loop.
You can also access that ref in the parent component by a function.
You could do something similar to this.
const { useRef, useState } = React;
const someArrayToMapStuff = ["a", "b", "c"];
const ListWithRef = ({ text, setDisplayWhatsInsideRef }) => {
const ref = React.useRef(null);
const logAndDisplayInnerHTML = () => {
setDisplayWhatsInsideRef(ref.current.innerHTML);
console.log(ref.current);
};
return (
<li
ref={ref}
onClick={logAndDisplayInnerHTML}
>
<button>{text}</button>
</li>
);
};
const App = () => {
const [displayWhatsInsideRef, setDisplayWhatsInsideRef] = useState("");
return (
<ul>
{someArrayToMapStuff.map(thing => <ListWithRef
key={thing}
text={thing}
setDisplayWhatsInsideRef={setDisplayWhatsInsideRef}
/>)}
{displayWhatsInsideRef && (
<h1>Look, I'm a ref displaying innerHTML: {displayWhatsInsideRef}</h1>
)}
</ul>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Hopefully this helps someone.

How do you use refs with a map function and Intersection Observer?

I'm trying to create an animated timeline on react with a map function and intersection observer so each part of the timeline loads sequentially.
I'm having trouble with the refs as I believe the ref only links to the last item on the map? I have had a look around and can't seem to find anything.
Here is my code:
import dataxp from '../Data/data-xp'
import TimelineItem from './TimelineItem'
function useOnScreen(options) {
const ref = React.createRef()
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setVisible(entry.isIntersecting);
}, options);
if (ref.current) {
observer.observe(ref.current)
}
return () => {
if (ref.current) {
observer.unobserve(ref.current)
}
}
}, [ref, options])
return [ref, visible]
}
function Timeline() {
const [ref, visible] = useOnScreen({rootMargin: '-500px'})
return (
dataxp.length > 0 && (
<div className='timeline-container'>
<div className='title-container'>
<h1 className='xp-title'>EXPERIENCE</h1>
</div>
{visible ? (dataxp.map((data, i) => (
<TimelineItem data={data} key={i} ref={ref}/>
)
)) : (
<div style={{minHeight: '30px'}}></div>)}
<div className='circle-container'>
<div className='end-circle'> </div>
</div>
</div>
)
)
}
export default Timeline

How do I make one component talk to another when they are generated with .map() in React?

I have multiple components with the same module using map().
list.map((data, index) => <MyComponent key={index} value={d}/>)
Then <p> in each of MyComponent changes colors from green to red when it is clicked.
const MyComponent = ({value}) => {
const [clicked, setClicked] = useState(false);
const buttonOnClick = () => {
setClicked(true);
}
return (
<div>
<p style={clicked ? {color: 'green'} : {color: 'red'}}>{value}</p>
<button onClick={buttonOnClick}>click</button>
</div>
);
};
In this case, I would like to turn color of <p> in other MyComponent red when one of them are clicked.
How can I check the <p> state of other MyComponent?
You need to pass a callback into your child component, and have your parent component to store and control the state. Here's an example:
const list = [1, 2, 3];
const MyComponent = ({ value, clickedValue, onClick }) => {
const style = { color: clickedValue === value ? 'green' : 'red' };
return (
<div>
<p style={style}>{value}</p>
{/* Callback with the value */}
<button onClick={() => onClick(value)}>click</button>
</div>
);
};
const App = () => {
const [clickedValue, setClickedValue] = React.useState();
const handleClick = value => {
setClickedValue(value);
};
return React.Children.toArray(
list.map(value => (
<MyComponent
value={value}
clickedValue={clickedValue}
onClick={handleClick}
/>
))
);
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Probably the easiest option would be to lift state up into the parent component: https://reactjs.org/docs/lifting-state-up.html

How to show/hide an item of array.map()

I want to show/hide a part of JSX depending on isCommentShown state property. But as this part is inside a map loop isCommentShown acts for all mapped items not only the current one. So when I toggleComment every comment inside a loop is shown/hidden. I imagine this can be solved by moving everything into a separate component because every component has its own state. But I wonder if I can can solve this without that.
const SearchResults = () => {
const [isCommentShown, setIsCommentShown] = useState(false);
const toggleComment = () => {
setIsCommentShown(!isCommentShown);
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={toggleComment}/> : null }
<div>{obj.text}</div>
{ isCommentShown ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
You could use the useState hook to create an object that will keep all the search result ids as keys and a boolean value indicating if the comment should be shown or not.
Example
const { useState, Fragment } = React;
const SearchResults = props => {
const [shownComments, setShownComments] = useState({});
const toggleComment = id => {
setShownComments(prevShownComments => ({
...prevShownComments,
[id]: !prevShownComments[id]
}));
};
return (
<Fragment>
{props.search_results.map(obj => (
<div key={obj.id}>
{obj.comment ? (
<button onClick={() => toggleComment(obj.id)}>Toggle</button>
) : null}
<div>{obj.text}</div>
{shownComments[obj.id] ? <p>{obj.comment}</p> : null}
</div>
))}
</Fragment>
);
};
ReactDOM.render(
<SearchResults
search_results={[
{ id: 0, text: "Foo bar", comment: "This is rad" },
{ id: 1, text: "Baz qux", comment: "This is nice" }
]}
/>,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Instead of storing true or false, you must store the comment id to show provided you only want to show one comment at a time. Its important to uniquely identify the item to be expanded
const SearchResults = () => {
const [commentShown, setCommentShown] = useState({});
const toggleComment = (id) => {
setCommentShown(prev => Boolean(!prev[id]) ? {...prev, [id]: true} : {...prev, [id]: false});
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={() => toggleComment(obj.id)}/> : null }
<div>{obj.text}</div>
{ commentShown[id] ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
If at all you need to open multiple comments simultaneously you can maintain a map of open ids
const SearchResults = () => {
const [commentShown, setCommentShown] = useState('');
const toggleComment = (id) => {
setCommentShown(prev => prev.commentShown !== id? id: '');
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={() => toggleComment(obj.id)}/> : null }
<div>{obj.text}</div>
{ commentShown === obj.id ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
Use the id to target the toggle on the comment you want.
More precisely, use the state to store the show/hide values, and pass the id to the onclick event to precise which comment to toggle. This should do the job:
class SearchResults extends React.Component {
constructor(props) {
super(props);
this.state = {};
for (let result of props.search_results) {
this.state[`${result.id}IsShown`] = true;
}
}
toggleComment(id) {
let key = `${result.id}IsShown`;
this.setState({[key]: !this.state[key]});
}
render() {
return (
<>
{this.props.search_results.map(result =>
<div key={result.id}>
{
result.comment
? <img onClick={() => toggleComment(result.id)}/>
: null
}
<div>{result.text}</div>
{ isCommentShown ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
}
}

Resources