1

I have a react app that uses page navigation. The routing information is in App.js

const router = createBrowserRouter([
        {
            path: '/',
            element: <RootLayout loggedInUser={loggedInUser} onShowModal={handleShowModal} config={config}/>,
            children: [
                { path: '/', element: <HomePage config={config} loggedInUser={loggedInUser} userDataFeeds={userDataFeeds} initialActiveFeedName={initialActiveFeedName}  /> },
                { path: '/manage', element: <ManagePage/>, children: [
                    { path: '/manage/apps', element: <ManageApps appItems={availableAppItems} /> },
                    { path: '/manage/feeds', element: <Outlet/>, children: [
                        { index: true, element: <ManageFeeds userDataFeeds={userDataFeeds} onDeleteFeedSuccess={getUserDataFeeds} /> },
                        { path: '/manage/feeds/add', element: <EditFeedForm loggedInUser={loggedInUser} action='Add' onEditFeedSuccess={handleEditFeedSuccess}  /> },
                        { path: '/manage/feeds/edit', element: <EditFeedForm loggedInUser={loggedInUser} action='Edit' onEditFeedSuccess={handleEditFeedSuccess} getAccessToken={getAccessToken} /> }
                    ] }
                ] }
            ]
        },
    ]);

Homepage.js is the component mapped to the root '/' route. In Homepage.js I have a hook that's triggered by any changes to a list of objects. The props.userDataFeeds in the snippet below is a list of objects.

useEffect(() => {
        if (props.userDataFeeds && props.userDataFeeds.length){
            console.log(props.initialActiveFeedName);
            console.log(props.userDataFeeds);
            if (props.initialActiveFeedName) {
                const initialActiveFeed = props.userDataFeeds.find(x => x.userDataFeedName === props.initialActiveFeedName);
                setActiveUserDataFeed(initialActiveFeed);
            } else {
                setActiveUserDataFeed(props.userDataFeeds[0]);
            }
        }
        
    }, [props.userDataFeeds, props.initialActiveFeedName]);

The second dependency is there to further control the outcome of the hook.

The hook is triggered when a form in a separate page is submitted. After submission these things happen:

  1. form data is persisted to a DB
  2. the DB table is fetched again
  3. fetched data is used to update props.userDataFeeds
  4. finally the user is redirected (using navigate) to Homepage

The problem is that for some reason, this hook is always triggered twice by the props.userDataFeeds dependency even though it only gets changed once. I have confirmed it is only changed once by logging it every time this hook triggers.

In the first triggering, the state of props.userDataFeeds is identical to the state prior to the from submission.

In the second triggering, the state of props.userDataFeeds matched what was submitted in the form, which is what I expect.

I need to prevent the hook from triggering twice. Is this happening because Homepage.js is a child component of RootLayout? I'm baffled why the exact same state of props.userDataFeeds would trigger the hook when no changes to any of the objects in the array or their properties have taken place.

Update - I have already disabled strict mode. The problem persists.

LNX
  • 383
  • 4
  • 11
  • When React watches an array as a dependency, a new array that is identical in value will trigger useEffect to run as it is not the same array. You can either stringify the array and use it on the dependency array, or use a custom solution that watches object/array changes deeply, eg `useDeepCompareEffect` by react-hookz. – Terry Jun 05 '23 at 18:24
  • My first two inclinations would be that either (1) the `initialActiveFeedName` state change is not batched with the `userDataFeeds` state change, so the 2 changes will each invoke the use effect once separately, or (2) the Homepage component is re-mounting instead of only re-rendering, so the use effect is running on initial render and on the immediate state change. You can check #1 by logging both state values at the top of the function (outside of the `if` statement). You can check #2 by ignoring the initial render [with a ref hook](https://stackoverflow.com/a/53254028/14077491). – Jacob K Jun 05 '23 at 19:47
  • `I have already disabled string mode`, you probably mean `strict mode` ? – Olivier Boissé Jun 05 '23 at 19:57
  • Jacob K, I logged both dependencies and found that props.userDataFeeds to be the culprit. The link you provided about the ref hook helped fix the problem. Thank you! – LNX Jun 06 '23 at 14:59

1 Answers1

0

I followed the second suggestion by Jacob K and added a boolean ref with a starting value of true. On the first render I set the ref to false. Logic within the hook prevents it from running on the first render.

useEffect(() => {
    if (initialRender.current) {
        initialRender.current = false;
    } else {
        if (props.userDataFeeds && props.userDataFeeds.length){
            if (props.initialActiveFeedName) {
                const initialActiveFeed = props.userDataFeeds.find(x => x.userDataFeedName === props.initialActiveFeedName);
                setActiveUserDataFeed(initialActiveFeed);
            } else {
                setActiveUserDataFeed(props.userDataFeeds[0]);
            }
        }
    }
}, [props.userDataFeeds, props.initialActiveFeedName]);

I think a better solution would be to figure out why the component is remounting unnecessarily, but for now this solves my problem.

LNX
  • 383
  • 4
  • 11