I am new to React and open to totally new suggestions concerning my code. I know I might not be following some best practices and I would be happy to be enlightened.
The problem I am facing is that every time a change is made to my textarea, the whole page is rerendered, leading to a loss in focus. So basically, I can only input 1 character at a time, then I have to reclick the textarea again.
I understand that this is caused by the fact that announcementElements
is dependent on announcements
. So when announcements
change, announcementElements
is rerendered.
My Code
import React from 'react';
import { FaThumbtack } from 'react-icons/fa6';
import { nanoid } from 'nanoid';
import { dateTime } from '../AppManager';
export default function Announcement({ announcements, setAnnouncements }) {
function handleChange(event){
const { name, value } = event.target;
setAnnouncements(oldAnnouncements => {
return oldAnnouncements.map(old => {
return old.id === name ? {...old, info: value, time: dateTime} : old;
})
})
}
const announcementsElement = announcements.map(announcement => {
return <div key={nanoid()}>
<textarea readOnly={false} className="announce" id={announcement.id} name={announcement.id} value={announcement.info} onChange={handleChange} />
<label htmlFor={announcement.id} style={{justifyContent: announcement.pinned ? "space-between" : "flex-end"}}>
{announcement.pinned && <FaThumbtack className='announcepinned' />}
<span className='announcetime'>{announcement.time}</span>
</label>
</div>
});
React.useEffect(() => {
function resize(){
const announceText = document.getElementsByClassName("announce");
for (let p = 0; p < announceText.length; p++) {
const ann = announceText[p];
const height = ann.scrollHeight > 120 ? 120 : ann.scrollHeight;
ann.style.height = height + "px";
}
}
document.querySelector(".announcement-wrapper").addEventListener("DOMNodeInserted", resize);
return () => {
document.querySelector(".announcement-wrapper").removeEventListener("DOMNodeInserted", resize);
};
}, []);
return (
<section>
<div className="announcement" id="announcement">
<h2>Announcements</h2>
<div className="announcement-wrapper">
{announcementsElement}
</div>
<div id="add1">
<button id="add" className="download update">Add</button>
</div>
</div>
</section>
)
}
Announcement Array
[
{
"id": "etmlkmlekrmpe",
"info":"Lorem ipsum dolor sit amet",
"pinned": false,
"time":"20/12/23 10:16:23"
},
{
"id": "okoikowekl",
"info":"Lorem ipsum, dolor sit amet consectetur.",
"pinned": false,
"time":"21/12/23 10:16:23"
},
{
"id": "oolkmuhyuf",
"info":"Lorem ipsum, dolor sit amet consectetur",
"pinned": true,
"time":"13/12/23 10:16:23"
},
{
"id": "kmkmlmklmkl",
"info":"Lorem ipsum, dolor sit amet",
"pinned": false,
"time":"25/12/23 10:16:23"
}
]
I have tried using useRef()
, but it doesn’t solve the problem
const announceRef = React.useRef([]);
announceRef.current = announcements;
const announcementsElement = announceRef.current.map(announcement => {
return <div key={nanoid()}>
<textarea readOnly={false} className="announce" id={announcement.id} name={announcement.id} value={announcement.info} onChange={handleChange} />
<label htmlFor={announcement.id} style={{justifyContent: announcement.pinned ? "space-between" : "flex-end"}}>
{announcement.pinned && <FaThumbtack className='announcepinned' />}
<span className='announcetime'>{announcement.time}</span>
</label>
</div>
});
2
Answers
I was not able to find a fix to my problem, so I took an entirely new approach
Instead of using the
onChange
listener, I decided to work with theonBlur
listener.onBlur
is triggered when an element previously focused on isn't any more.That way, the user can type everything without being interrupted by the change. A submit button is also needed to complete the update, so the
onBlur
event would occur.Since I can't have a
textarea
orinput
element in React without anonChange
listener (I think so), I had to change from usingtextarea
to adiv
with acontentEnditable
property.Here is my updated code
So, I would be using this for now, but will keep track of this question if someone has a better solution.
I have another question though, is it actually possible to have an
input
ortextarea
without anonChange
listener?I would prefer to continue using the
textarea
if possibleReact is a very simple system:
useState()
)In your case you are sending the "set state function" from the parent down to
Announcement
, and then having the text area update the PARENT’S STATE everytime the text area has a new value (every single key press).So it is the PARENT that is re-rendering, which then re-renders
Announcement
.This is likely NOT what you want.
Where to store "state" in the tree of components is part of the "art" of React. Understanding the simple rules listed above will help you determine where to store state, and when to pass new values upwards towards your parent (if ever)
I suspect what you really want to do is to store the value of the textarea as state in Announcement, and then have some other trigger that sends that value to the parent (if needed). Maybe you offer a SAVE button or have some type of debouncing timer that sets the value on the parent after some time delay?
….or…do you even need to set STATE on the parent at all? Could you pass the value as a standard variable rather than a "set state function"?
Addtionally, recognize that when you do:
the
[[SOME_JSX_STUFF_HERE]]
is actually a separate React component. If you want to track state for each iteration of that .map(), then consider putting state (i.e. use useState()) inside[[SOME_JSX_STUFF_HERE]]
.And…to be a better/cleaner codebase, move the definition of
[[SOME_JSX_STUFF_HERE]]
to its own file (e.g.SomeJsxStuffHere.jsx
) so that it is clear that this is a component, and that it be maintained as such.