skip to Main Content

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.

4

Answers


  1. 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.

    It does.

    Batching is when React groups multiple state updates into a single re-render for better performance. Without automatic batching, we only batched updates inside React event handlers

    But since you are updating activePage and this latter is included in the useEffect dependency array, once the component finishes rerendering, useEffect will run, and since the code inside it is updating clipsDetails and clipsDetails this will make the component rerender again, and it looks like only activePage is updated.

    So if you want to run getClips only on the first mount of the component you have to give an empty dependency array to useEffect:

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

    And if you want to execute getClips() in some specific cases, not only on the first mount, then you can create another state:

    const [trigger, setTrigger] = useState(false);
    //...
    useEffect(() => {
      getClips();
    },[trigger]);
    

    then each time you want to trigger getClips() all you have to do is to update trigger :

    setTrigger(prev => !prev);
    

    Is it good to use array as dependency in useEffect. If not, then what is the other option?

    It is not about good or bad, it’s about the use case.
    If you want to trigger the code inside a useEffect when the component rerenders and ‘something’ is different from what it was during the last render, then you include this ‘thing’ in the dependency array, and if you want useEffect to act as the componentdidmount (run only when the component is mounted) than you give it an empty dependency array [] and if you don’t give a dependency array at all, this will make the useEffect hook run on every component rerender.

    And if you mean an array inside the dependency array then yes you can, but keep in mind that each time the array is updated the component will rerender and useEffect will fire (the component only rerenders when the new value of useState is different from the current one) but since two objects are always not equal in javascript, even if the array is updated with the same value this will make the component rerender anyway.

    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
  3. 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

    Login or Signup to reply.
  4. Yes, It is better to keep dependency array in useEffect.

    useEffect(() => {
    console.log("UseEffect is calling..")
    console.log(thirdState);
    console.log(firstState);
    console.log(secondState);
    
    }, [thirdState])
    
    const onClick = () => {
        setThirdState(["something"]);
        console.log("Set Third State..")
        setFirstState(1)
        console.log("Set First State..")
        setSecondState(2)
        console.log("Set Second State..")
      }
    

    enter image description here

    If your onClick() function have more than one functions and you need a guarantee to to see the state changes from the firstfunction to consecutive functions, then you can use callback function. since you want first function to get executed in onClick(), what you can do is you can call the second function after setting the required state in the first function.

    All about dependency array in useEffect, nice explanation is given on useEffect dependency array in React.js

    See explanation by nem035 for explanation on empty dependency

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