skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 the onBlur 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 or input element in React without an onChange listener (I think so), I had to change from using textarea to a div with a contentEnditable property.

    Here is my updated code

    import React from 'react';
    import { FaThumbtack } from 'react-icons/fa6';
    import { dateTime } from '../AppManager';
    
    export default function Announcement({ announcements, setAnnouncements }) {
      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";
        }
      }
    
      function AnnouncementBox({ id, info, pinned, time, admin = true, handleChange, resize }){
        return <div>
          <div contentEditable={admin} className="announce" id={id} onBlur={handleChange} onKeyUp={resize}>
            {info}
          </div>
          <label htmlFor={id} style={{justifyContent: pinned ? "space-between" : "flex-end"}}>
            {pinned && <FaThumbtack className='announcepinned' />}
            <span className='announcetime'>{time}</span>
          </label>
        </div>
      }
    
      function handleChange(event){
        const { id, innerText } = event.target;
        setAnnouncements(oldAnnouncements => {
          return oldAnnouncements.map(old => {
            return old.id === id ? {...old, info: innerText, time: dateTime} : old;
          })
        });
      }
    
      const announcementsElement = announcements.map(announcement => {
        return <AnnouncementBox key={announcement.id} id={announcement.id} info={announcement.info} pinned={announcement.pinned} time={announcement.time} handleChange={handleChange} admin={true} resize={resize} />
      });
    
      React.useEffect(() => {
        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>
      )
    }
    

    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 or textarea without an onChange listener?
    I would prefer to continue using the textarea if possible


  2. React is a very simple system:

    • your app is a ‘tree of components’
    • a ‘component’ is simply a JavaScript function
    • each component can have its own data stored in "state" (via useState())
    • a component gets ‘rendered’ (i.e. the function is run) under only 2 cases:
      1. its parent is rendered, so that parent renders all its children
      2. state in the component is modified via the "set state function"

    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:

    announcements.map((announcement) => {
          [[SOME_JSX_STUFF_HERE]]
    });
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search