javascriptreactjsarraylistrerendermemo

Preventing The Entire List Re-Render on Item Addition in React Without Using memo


Please consider the following code: https://codepen.io/kyxey/pen/XWEWBRY

const { useRef, useState } = React;

function App() {
  const inputRef = useRef(null);
  const [jobs, setJobs] = useState([]);

  const addJob = () => {
    const newJob = inputRef.current.value;

    if (newJob) {
      setJobs((prevJobs) => [...prevJobs, newJob]);
    }
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={addJob}>Add</button>
      <ul>
        {jobs.map((job) => {
          const listItem = job + " " + Math.random();
          return <li key={listItem}>{listItem}</li>;
        })}
      </ul>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<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>

In this example, Whenever a new item gets added to the list, the entire list re-renders. You can tell this by looking at the random number in front of each item which will change with every newly items that are added to the list.

Now I know there are some duplications of this very question which I read them all. But none of them could solve my issue completely.

My question is: How can I prevent the re-render of the entire list each time a new item is added to it, WITHOUT using memo and useMemo in any shapes and forms? Meaning that whenever a new item is added to the list, only the new item is rendered and the other items on the list remain completely untouched. Again, I'm not able to use memo or useMemo to solve this.

For example:

Current behavior is like this:

Expected behavior should be like this:

UPDATE:

This question was asked from me in a coding interview. They explicitly mentioned that I'm not allowed to use memo or useMemo in the sense that these are considered cheating! Now I don't know exactly why they think in such way but I'm sure there's an specific answer in their mind that is not how React is supposed to behave.


Solution

  • You've said:

    This question was asked from me in a coding interview. They explicitly mentioned that I'm not allowed to use memo or useMemo in the sense that these are considered cheating! Now I don't know exactly why they think in such way but I'm sure there's an specific answer in their mind that is not how React is supposed to behave.

    A reasonable answer to that — quite possibly the one they were expecting — is something along the lines of: "You can do that, but it would just be reinventing memo for no good reason, and by doing something so non-standard and unusual, it would make the code hard to understand and maintain for the next person." If they actually wanted to see a solution without using those things, I would suggest you cross them off the list of places you might consider working if you have any choice (I respect the fact you may not have a choice; I remember that vividly early in my career). That's a terrible interview question if they really wanted anything other than pushback (arguably even if they were looking for pushback), which can be indicative of a bad place to work.

    But again, technically, you can it with a ref by storing the rendered li elements in it (perhaps in a Map). To me, that's much more "cheating" than doing it with memo as you should, but... Here's an example:

    const { useRef, useState } = React;
    
    // *** A means of having unique keys for jobs
    let nextJobId = 1;
    function App() {
        const inputRef = useRef(null);
        const [jobs, setJobs] = useState([]);
        const renderedJobs = useRef(new Map());
        const jobLiElements = renderedJobs.current;
    
        const addJob = () => {
            // *** Make the jobs objects, not just strings, so the
            // same string can be used by more than one job and
            // so we can give the job a unique ID.
            const newJob = {
                id: nextJobId++,
                value: inputRef.current.value,
            };
    
            if (newJob) {
                setJobs((prevJobs) => [...prevJobs, newJob]);
            }
        };
    
        return (
            <div>
                <input ref={inputRef} type="text" />
                <button onClick={addJob}>Add</button>
                <ul>
                    {jobs.map((job) => {
                        // *** Reuse li elements you already have if you have them,
                        // adding new ones if you don't
                        let li = jobLiElements.get(job);
                        if (!li) {
                            const listItem = job.value + " " + Math.random();
                            console.log(`Rendering li for ${job.value}`);
                            // *** You can't use `listItem` as the key, since there's
                            // _some_ chance of the same `li` having the same text and
                            // random number. Use the job object instead.
                            li = <li key={job.id}>{listItem}</li>;
                            jobLiElements.set(job, li);
                        }
                        return li;
                    })}
                </ul>
            </div>
        );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <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>

    But even if I went so far as to show them that, I'd also show them a solution using memo and putting the random value in the state of the job, talking about the strengths of doing it that way (not least that it's the normal, expected way to do this):

    const { useRef, useState } = React;
    
    const JobItem = React.memo(({ job: { value, rand } }) => {
        const text = `${value} ${rand}`;
        console.log(`Rendering JobItem for "${text}"`);
        return <li>{text}</li>;
    });
    
    // *** A means of having unique keys for jobs
    let nextJobId = 1;
    
    function App() {
        const inputRef = useRef(null);
        const [jobs, setJobs] = useState([]);
    
        const addJob = () => {
            // *** Make the jobs objects, not just strings, so the
            // same string can be used by more than one job, and so
            // we can assign it a unique ID to use as a key.
            // *** Assign the random number once, as part of the job.
            const newJob = {
                id: nextJobId++,
                value: inputRef.current.value,
                rand: Math.random(),
            };
    
            if (newJob) {
                setJobs((prevJobs) => [...prevJobs, newJob]);
            }
        };
    
        return (
            <div>
                <input ref={inputRef} type="text" />
                <button onClick={addJob}>Add</button>
                <ul>
                    {/* *** Now just use JobItem*/}
                    {jobs.map((job) => (
                        <JobItem key={job.id} job={job} />
                    ))}
                </ul>
            </div>
        );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <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>