0

I have react app and I'm using redux in order to manage state.

What suppose to happen:

Redux holds an object that contains an _id string, and on a page I have a useEffect that first checks that checks _id is not an empty string and only after, invokes an async function onPlantTimelineMount() to fetch data using the _id. When a user reloads the page, Redux state is default loaded where _id is an empty string so the if statement inside the useEffect is invoked, to fetch that item using a different async function ifUserRefreshPage(), dispatch the item to Redux state, and only after invoke onPlantTimelineMount().

The Problem:

The ifUserRefreshPage() successfully fetches the data and dispatch it to Redux state, but the onPlantTimelineMount() still uses the state prior to the ifUserRefreshPage() invocation - which means that _id is still an empty string.

Worth mentioning:

Inside ifUserRefreshPage() I serialize the fetched item (line 38), I dispatch it to Redux (line 39), console.log the fetched item (line 40 in the image), and I console.log the item in Redux state (line 41) and its still empty!!!!!! HOW???

enter image description here

The Code:

const currentPlant = useAppSelector(state => state.plants.currentPlant)
    const plantUpdates = useAppSelector(state => state.plants.currentPlant.updates)
    const reduxPlants = useAppSelector(state => state.plants.plants)
    const location = useLocation();
    const { show: showSnackbar, component: snackBar } = useSnackbar();
    async function ifUserRefreshPage() {
        setIsFetching(true)
        const plantId = location.pathname.split('/')[2];
        try {
            console.log(`ifUserRefreshPage invoked`);
            const plantResponse = await fetchPlantById(plantId, '1')
            const serializedPlant = plantManager.serializePlant(plantResponse.data)  // line 38
            dispatch(setCurrentPlant(serializedPlant))                               // line 39
            console.log(serializedPlant);                                            // line 40
            console.log(currentPlant);                                               // line 41
            const gardenResponse = await fetchMyGarden('1')
            dispatch(addPlants(plantManager.serializeGarden(gardenResponse.data)))
        } catch (err) {
            console.log(`error while running ifUserRefreshPage function` + err);
            showSnackbar("Failed to load Timeline", "error");
        } finally {
            setIsFetching(false)
        }
    }

    async function onPlantTimelineMount() {
        setIsFetching(true)
        console.log(`ifUserRefreshPage invoked`);
        try {
            const response = await fetchPlantUpdates(currentPlant._id!)
            if (response.success) {
                dispatch(setUpdatesToCurrentPlant(plantUpdateManager.serializeUpdatesArray(response.data)))
            }
        } catch (err) {
            showSnackbar("Failed to load Timeline", "error");
        } finally {
            setIsFetching(false)
        }
    }
    
    useEffect(() => {
        async function start() {
            if (!currentPlant._id) {
                await ifUserRefreshPage()
            }
           await onPlantTimelineMount()
        }
        start()
    },[])
IdoBa
  • 63
  • 6

1 Answers1

1

I console.log the item in Redux state (line 41) and its still empty!!!!!! HOW???

Because that's a local constant. It can never change. It's similar to the common issue that updating state does not change your local variables. Stack overflow on this topic, React docs on this topic

When you dispatch your action, you update the redux store. This updated state could be accessed by calling store.getState(), though that's not typically something you would do directly in a react app. You've called const currentPlant = useAppSelector(state => state.plants.currentPlant), which subscribes to the store, and so the change of state causes your component to rerender. When this rerender happens a new local constant will be created with the new value, but your code from the previous render has no access to this. Your console.log(currentPlant); from the previous render will only ever log out the old value.

Since you do have the new value in a different variable, i recommend you use that. Eg, modify ifUserRefreshPage to return the new plant:

async function ifUserRefreshPage() {
  setIsFetching(true);
  const plantId = location.pathname.split("/")[2];
  try {
    console.log(`ifUserRefreshPage invoked`);
    const plantResponse = await fetchPlantById(plantId, "1");
    const serializedPlant = plantManager.serializePlant(plantResponse.data);
    dispatch(setCurrentPlant(serializedPlant));
    const gardenResponse = await fetchMyGarden("1");
    dispatch(addPlants(plantManager.serializeGarden(gardenResponse.data)));
    return serializedPlant; // <--------------Added return
  } catch (err) {
    console.log(`error while running ifUserRefreshPage function` + err);
    showSnackbar("Failed to load Timeline", "error");
  } finally {
    setIsFetching(false);
  }
}

And modify onPlantTimelineMount so you can pass in the plant:

//              Added argument -----VVVVV
async function onPlantTimelineMount(plant) {
  setIsFetching(true);
  console.log(`ifUserRefreshPage invoked`);
  try {
    const response = await fetchPlantUpdates(plant._id!);
    if (response.success) {
      dispatch(
        setUpdatesToCurrentPlant(
          plantUpdateManager.serializeUpdatesArray(response.data),
        ),
      );
    }
  } catch (err) {
    showSnackbar("Failed to load Timeline", "error");
  } finally {
    setIsFetching(false);
  }
}

And update your effect to connect the two:

useEffect(() => {
  async function start() {
    let plant = currentPlant;
    if (!plant._id) {
      plant = await ifUserRefreshPage();
    }
    await onPlantTimelineMount(plant);
  }
  start();
}, []);
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98