Warm tip: This article is reproduced from stackoverflow.com, please click
react-hooks reactjs use-effect use-state use-layout-effect

React, how to update a state without repainting, or how to trigger useEffect without useState (and w

发布于 2020-03-28 23:15:01

The situation

  • I want to redistribute a grid based on the height of the images (using a masonry approach)
  • Those images are lazy loaded

What I want

  • When the images are loaded, they send a flag, this flag makes the masonry logic to redistribute the grid, and everybody is happy
  • Then when the user scrolls, more images are loaded, trigger the flag again and masonry again adjusts the grid

What happens

* const [myState, setMyState] = use State(0)

  • When the images are loaded I setMyState(1)
  • In masonry logic, I have a useLayoutEffect(() =>{}, [myState]) so when the state changes, I adjust the grid
  • Then I reset the state with setMyState(0), so that when a new image is loaded it will trigger again the useLayoutEffect
  • BUT, when I reset the state it repaints the images, so the images get loaded again and trigger again the useLayoutEffect, which again resets the state, creating an infinite loop
  • If I don't reset the state, the loop is stopped
  • BUT then when a new image is loaded with the scroll, setMyState(1) doesn't change anything and the useLayoutEffect is not triggered

So I'm now stuck

This is a simplified code

import React, { useRef, useLayoutEffect, useState} from "react";

const wait = ms =>
  new Promise((res, rej) => setTimeout(() => res("timed"), ms));

const Image = ({ setImageLoader }) => {
  // lazy load the image with an Observer, and then changes the state
  setImageLoader(1);
  return <></>;
};

export default function App() {
  const [imageLoaded, setImageLoader] = useState(0);

  useLayoutEffect(() => {
    const update_grid = async stillMounted => {
      // change stuff
      await wait(10000); // careful with crashing chrome
      console.log("updated");
      setImageLoader(0);
    };

    const stillMounted = { value: true };
    update_grid(stillMounted);
    return () => (stillMounted.value = false);
  }, [imageLoaded]);

  return <Image setImageLoader={setImageLoader} />;
}

Attempt 1

I cannot change the state in the useLayoutEffect, so I will change stuff only when the images are loaded and not again

Problem

When I scroll and more images are loaded, I cannot triger the useLayoutEffect anymore

import React, { useRef, useLayoutEffect, useState} from "react";

const wait = ms =>
  new Promise((res, rej) => setTimeout(() => res("timed"), ms));

const Image = ({ setImageLoader }) => {
  setImageLoader(1);
  return <>hey</>;
};

export default function App() {
  const [imageLoaded, setImageLoader] = useState(0);

  useLayoutEffect(() => {
    // change stuff
    const update_grid = async stillMounted => {
      await wait(40000);
      console.log("updated");
      // setImageLoader(0); NOW THIS IS NOT CAUSING THE REPAINT, BUT NEITHER WHEN NEW IMAGES ARE LOADED
    };

    const stillMounted = { value: true };
    update_grid(stillMounted);
    return () => (stillMounted.value = false);
  }, [imageLoaded]);

  return <Image setImageLoader={setImageLoader} />;
}

Attempt 2

If I don't want the repaint, but want to trigger the useLayoutEffect, I could use useRef instead of useState

import React, { useRef, useLayoutEffect, useState, useEffect } from "react";

const wait = ms =>
  new Promise((res, rej) => setTimeout(() => res("timed"), ms));

const Image = ({ imageLoaded }) => {
  imageLoaded.current++;
  return <>hey</>;
};

export default function App() {
  const imageLoaded = useRef(0);

  useLayoutEffect(() => {
    // change stuff
    const update_grid = async stillMounted => {
      await wait(10000);
      console.log("updated " + imageLoaded.current);
      imageLoaded.current++;
    };

    const stillMounted = { value: true };
    update_grid(stillMounted);
    return () => (stillMounted.value = false);
  }, [imageLoaded.current]);

  return <Image imageLoaded={imageLoaded} />;
}

But codesandbox already warns me that imageLoaded.current will not cause a re-render of the component (even though I more or less wants this?)

So at the end, I see imageLoaded.current increasing when new images are loaded, but the useLayoutEffect is not triggered

Questioner
GWorking
Viewed
42
GWorking 2020-01-31 18:18

I haven't been able to solve that using useEffect or similar

Instead, I've used a simple function to adjust the grid, and call it from the lazy-loading logic whenever it loads an image

import React, { useRef, useEffect, useState } from "react";

const wait = ms =>
  new Promise((res, rej) => setTimeout(() => res("timed"), ms));

const Image = ({ adjustMasonry }) => {
  // lazy load the image with an Observer, and then changes the state
  const scroll = async () => {
    adjustMasonry();
    adjustMasonry(); // only causes one update due to the implemented buffer
    await wait(4000);
    adjustMasonry();
  };
  scroll();
  return <></>;
};

export default function App() {
  let signals = 0;
  const adjustMasonry = async stillMounted => {
    signals++;
    const last_signal = signals;
    await wait(200); // careful with crashing chrome
    if (signals !== last_signal) return; // as a way to minimize the number of consecutive calls
    if (stillMounted && !stillMounted.value) return;

    // change stuff
    console.log("updated");
  };

  useEffect(() => {
    const stillMounted = { value: true };
    adjustMasonry(stillMounted);

    return () => {
      stillMounted.value = false;
    };
  }, []);

  return <Image adjustMasonry={adjustMasonry} />;
}