Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does multiple setState at once guarantee to be happened at once in react hooks?

I have below piece of code

import React, { useEffect, useState } from 'react';
const Mycomp = () => {
  const [firstState, setFirstState] = useState(0);
  const [secondState, setSecondState] = useState(0);
  const [thirdState, setThirdState] = useState([]);

  useEffect(() => {
    console.log(thirdState);
    console.log(firstState);
    console.log(secondState);
  }, [thirdState])

  const onClick = () => {
    setThirdState(["something"])
    setFirstState(1)
    setSecondState(2)
  }

  return (
    <div>
      <button onClick={onClick}>Submit</button>
    </div>
  );
};

export default Mycomp;

I have two questions here

  1. Is it possible that as I have written setThirdState at the first in onClick function then it gets called & set the state and call the useEffect and I do not get updated firstState & secondState?
  2. Is it good to use array as dependency in useEffect. If not, then what is the other option?

Edit: Here is my use case

const Mycomponent = () => {
    const [clipcount, setClipcount] = useState(0);
    const [clipsDetails, setClipsDetails] = useState([]);
    const [activePage, setActivePage] = useState(1);
    const [socketClipCount, setSocketClipCount] = useState(0);

    useEffect(() => {
    webhookSocket.on(`livecut${id}`, (data) => {
      setSocketClipCount((socketClipCount) => socketClipCount + 1);
    });
  }, [webhookSocket]);

    useEffect(() => {
        getClips();
    }, [activePage])

    const getClips = async () => {
        const { value } = await getAllClips({
            limit: clipLimit,
            pageNo: activePage,
            sort: { _id: -1 },
        });
        if (value?.success) {
            setClipcount(value.totalLength);
            setClipsDetails([...clipsDetails, ...value?.clips]);
        }
    };

    return (
        <div className="custom-generated-clips styled-scroll">
            {socketClipCount && <button onClick={() => {
                setSocketClipCount(0);
                setClipsDetails([]);
                setActivePage(1);
            }}>
                Refresh
            </button>}
            <InfiniteScroll
                dataLength={clipsDetails?.length}
                hasMore={clipsDetails?.length < clipcount ? true : false}
                next={() => setActivePage(activePage + 1)}
                loader={
                    <div className="InfinitScrollLoadingBottom">
                        <div className="InfinitScrollLoadingBottomText">Loading...</div>
                    </div>
                }
                endMessage={
                    <div className="InfinitScrollEndBottom">
                        <div className="InfinitScrollEndBottomText">
                            {clipsDetails?.length > 4 ? "You've reached End!" : ""}
                        </div>
                    </div>
                }
                // pullDownToRefreshThreshold={limit}
                scrollableTarget="scrollableDiv"
            >
                {myData}
            </InfiniteScroll>
        </div>
    )
};

export default Mycomponent;

So here I am getting clipDetails and showing it in Infinite scroll which is simple thing. Now when I get clips from socket I show the Refresh button and refreshing that section by setting setSocketClipCount(0);setClipsDetails([]);setActivePage(1); but it does not set the state for the clipDetails & socketClipCount array.

What could be the solution for it? I am using React 18 even batch updating is not working.

It is hard for me to get things because sometimes React official docs says

  1. It is not good to club the state like useState({ state1: "", state2: "" })
  2. React 18 supports batch update still the above code does not work.
like image 211
Profer Avatar asked Nov 15 '25 22:11

Profer


2 Answers

the issue here is using useEffect for user actions

both of your usecases are good (according to docs), but only on paper:

  • subscribing to webhookSocket
  • fetching clips

but due to how you rely on setstate and rerenders to react to user action and for example fetch data, your flow breaks along the way. this, paired with impure functions makes it even harder to trace whats happening.

separate your events

we can break it down in two events that are user triggered, and two that are triggered either outside of react, or just happen without any action.

❌ no useEffect

  • user clicks refresh
  • user triggers infiniteScroll.next

useEffect

  • new socket message
  • initial load

heres what i'd change, to accomplish that:

  • for getClips() move the implicit dependency on activePage to an explicit page param. that way you can call it whenever you want with the parameters you need
  • run the initial getClips() in a run-once useEffect (we should supply getClips as a dependency for useEffect, and thus wrap it in useCallback to make it stable)
  • when appending new clipsDetails, dont use a potentially stale value for clipsDetails in the spread. setClipsDetails((prev) => [...prev, ...value?.clips]); this makes sure it uses the right value.
  • when clicking the refresh button: refetch data with the currently active page, and reset the "unseen clips" counter.
  • when triggering the next event in the infinite scroll: load the next page and increment activePage.
const Mycomponent = () => {
  const [clipcount, setClipcount] = useState(0);
  const [clipsDetails, setClipsDetails] = useState([]);
  const [activePage, setActivePage] = useState(1);
  const [socketClipCount, setSocketClipCount] = useState(0);

  useEffect(() => {
    // i'm 99% sure you dont need the webhookSocket instance inside the component scope. unless it dependes on some component state (like a room-id) you should move it outside of the component definition. that way its stable and not required as dependency.
    webhookSocket.on(`livecut${id}`, (data) => {
      setSocketClipCount((socketClipCount) => socketClipCount + 1);
    });
    return () => {
      // unsubscribe from webhookSocket
    };
  }, []);

  const getClips = useCallback(async (page) => {
    const { value } = await getAllClips({
      limit: clipLimit,
      pageNo: page,
      sort: { _id: -1 }
    });
    if (value?.success) {
      setClipcount(value.totalLength);
      setClipsDetails((prev) => [...prev, ...value?.clips]); // use the current value of clipsDetails in the spread
    }
  }, []);

  useEffect(() => {
    getClips();
  }, [getClips]);

  return (
    <div className="custom-generated-clips styled-scroll">
      {socketClipCount > 0 && ( // dont use non booleans with `&&` in jsx, as this will render the 0 as text
        <button
          onClick={() => {
            getClips({ page: activePage });
            setSocketClipCount(0);
          }}
        >
          Refresh ({socketClipCount})
        </button>
      )}
      <InfiniteScroll
        dataLength={clipsDetails?.length}
        hasMore={clipsDetails?.length < clipcount} // `? true : false` not needed as this already returns boolean
        next={() => {
          setActivePage(activePage + 1);
          getClips({ page: activePage + 1 });
        }}
        loader={
          <div className="InfinitScrollLoadingBottom">
            <div className="InfinitScrollLoadingBottomText">Loading...</div>
          </div>
        }
        endMessage={
          <div className="InfinitScrollEndBottom">
            <div className="InfinitScrollEndBottomText">
              {clipsDetails?.length > 4 ? "You've reached End!" : ""}
            </div>
          </div>
        }
        // pullDownToRefreshThreshold={limit}
        scrollableTarget="scrollableDiv"
      >
        {myData}
      </InfiniteScroll>
    </div>
  );
};

export default Mycomponent;

i've added a few more smaller things as comments. i hope this helps. i'd highly recommend reading this article in the new react docs, as this helps you prevent error-prone uses of useEffect

like image 171
Alex Spieslechner Avatar answered Nov 17 '25 16:11

Alex Spieslechner


If i understand your use case

  • Your getClips useEffect doesn't depend only on activePage but also on socketClipCount which is missing in your code
  • If you wanna set ClipsDetails to an empty array socketClipCount must be equal to 0

On your refresh click you are having two similar setState (set clipDetails to empty array then getClips executes and set the clipDetails again with the http response) , one after the other and only the last one will take effect

  1. setClipsDetails([])
  2. setActivePage(1) will trigger the getClips effect
  3. setClipsDetails([...clipsDetails, ...value?.clips]) is executed inside the effect and it's the change and the one that is painted on your view

To make your code works you have to add a new dependency (socketClipCount) to getClips effect and use it to check when the getClips function is executed

 useEffect(() => {
    // avoid loading when refreshing or initializing
    if(socketClipCount > 0) {
       getClips();
    }
 }, [activePage, socketClipCount])

As a result when you click refresh you will have

  • ClipsDetails set to an empty array
  • activePage to 1

If you find that socketClipCount is not changed it's probably changed in the same time here with a new value different from 0

useEffect(() => {
 webhookSocket.on(`livecut${id}`, (data) => {
  setSocketClipCount((socketClipCount) => socketClipCount + 1);
 });
}, [webhookSocket]);

May be closing the connection for few seconds when refreshing the open it again would be a good solution

like image 25
Fateh Mohamed Avatar answered Nov 17 '25 17:11

Fateh Mohamed



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!