skip to Main Content

I’m working with a list of notes in React Native, and I was using a bad-performant method to select/deselect the notes when I’m "edit mode". Everytime I selected a note, the application had to re-render the entire list everytime. If I do a test with 100 notes, I get input lags when I select/deselect a note, obviously.

So I decided to move the "select state" to the single Child component. By doing this, I’m having the re-render only on that component, so it’s a huge improvement of performance. Until here, everything’s normal.

The problem is when I’m disabling edit mode. If I select, for example, 3 notes, and I disable the "edit mode", those notes will remain selected (indeed also the style will persist). I’d like to reset the state of all the selected note, or finding a valid alternative.

I recreated the scene using React (not React Native) on CodeSandbox with a Parent and a Child: https://codesandbox.io/s/loving-field-bh0k9k

The behavior is exactly the same. I hope you can help me out. Thanks.

tl;dr:

Use-case:

  1. Go in Edit Mode by selecting a note for .5s
  2. Select 2/3 elements by clicking on them
  3. Disable Edit Mode by selecting a note for .5s

Expectation: all elements get deselected (state of children resetted)

Reality: elements don’t get deselected (state of children remains the same)

2

Answers


  1. If you use React.memo you can cache the Child components and prevent their re-renders.

    const Parent = () => {
      const [editMode, setEditMode] = useState(false);
      const [childrenList, setChildrenList] = useState(INITIAL_LIST);
      const [selected, setSelected] = useState([]);
    
      const toggleEditMode = useCallback(() => {
        if (editMode) {
          setSelected([]);
        }
        setEditMode(!editMode);
      }, [editMode]);
    
      const deleteSelectedChildren = () => {
        setChildrenList(childrenList.filter((x) => !selected.includes(x.id)));
        setEditMode(false);
      };
    
      const onSelect = useCallback((id) => {
        setSelected((prev) => {
          if (prev.includes(id)) {
            return prev.filter((x) => x !== id);
          }
          return [...prev, id];
        });
      }, []);
    
      // Check when <Parent /> is re-rendered
      console.log("Parent");
    
      return (
        <>
          <h1>Long press an element to enable "Edit Mode"</h1>
          <ul className="childrenWrapper">
            {childrenList.map((content, index) => (
              <Child
                key={content.id}
                index={index}
                content={content}
                editMode={editMode}
                toggleEditMode={toggleEditMode}
                onSelect={onSelect}
                selected={selected.includes(content.id)}
              />
            ))}
          </ul>
          {editMode && (
            <button onClick={deleteSelectedChildren}>DELETE SELECTED</button>
          )}
        </>
      );
    };
    

    You have to wrap the functions you pass as props inside useCallback, otherwise they will be different on every Parent render, invalidating the memoization.

    import { useRef, memo } from "react";
    
    const Child = memo(
      ({ content, editMode, toggleEditMode, onSelect, selected }) => {
        // Prevent re-rendering when setting timer thread
        const timerRef = useRef();
    
        // Toggle selection of the <Child /> and update selectedChildrenIndexes
        const toggleCheckbox = () => {
          if (!editMode) return;
          onSelect(content.id);
        };
    
        // Toggle Edit mode after .5s of holding press on a Child component
        const longPressStartHandler = () => {
          timerRef.current = setTimeout(toggleEditMode, 500);
        };
    
        // Release setTimeout thread in case it's pressed less than .5s
        const longPressReleaseHandler = () => {
          clearTimeout(timerRef.current);
        };
    
        // Check when <Child /> is re-rendered
        console.log("Child - " + content.id);
    
        return (
          <li
            className={`childContent ${editMode && "editMode"} ${
              selected && "selected"
            }`}
            onMouseDown={longPressStartHandler}
            onMouseUp={longPressReleaseHandler}
            onClick={toggleCheckbox}
          >
            <pre>
              <code>{JSON.stringify(content)}</code>
            </pre>
            {editMode && (
              <input type="checkbox" onChange={toggleCheckbox} checked={selected} />
            )}
          </li>
        );
      }
    );
    

    You can see a working example here.

    Login or Signup to reply.
  2. this is easy enough to do with a useEffect hook.
    It allows you to "watch" variable changes over time.

    When editMode changes the contents of the Effect hook runs, so when editMode goes from true to false, it will set the item’s selected state.

    Add this to your <Child /> component:

      useEffect(() => {
        if (!editMode) {
          setSelected(false);
        }
      }, [editMode]);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search