1

I have a peculiar problem when using the useEffect-hook in React Native. I have a functional component, which has one useEffect-hook that fetches data for pinpoints and another that then rearranges the pinpoints (filteredPinpoints) into a useable format. filteredPinpoints is updated three times, but the first two times, the object is empty.

Now the weird behaviour: if I comment out dispatch(organiseRoutes(...)) in the second useEffect, this useEffect is called three times, but if I want to execute the dispatch function, the useEffect is only called twice. Since I return early if filteredPinpoints is empty, the code never reaches the dispatch.

EDIT: Also, when I implement dispatch(organiseRoutes(...)), the app freezes, only showing the (spinning) ActivityIndicator, but leaving me unable to navigate to the previous screen again.

What do I have to change, so that the useEffect is run every single time filteredPinpoints is updated?

import { View, ActivityIndicator } from 'react-native';
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getRouteData, organiseRoutes } from '../utils';

export default function RoutePreviewScreen() {

    const dispatch = useDispatch();
    const [loadingData, setLoadingData] = useState(true);
    const currentRouteID = useSelector(state => state.currentRouteID);
    const filteredPinpoints = useSelector(state =>
        // Uses ObjectFilter from https://stackoverflow.com/questions/5072136/javascript-filter-for-objects/37616104
        ObjectFilter(state.allPinpoints, pinpoint => pinpoint.Route_ID == state.currentRouteID)
    );

    const dispatch = useDispatch();

    // This updates state.allPinpoints.
    useEffect(() => {
        (async function myFirstAsyncFunction() {
            await dispatch(getRouteData(currentRouteID));
        })();
    }, [currentRouteID]);

    useEffect(() => {
        if (Object.keys(filteredPinpoints).length === 0) {
            return
        }

        console.log("Could EXECUTE now!!")

        // If the following line is commented out, the useEffect executes a third time.
        // However, only in the third run, filteredPinpoints is not a empty object.
        // If it is not commented out, it simply refuses to execute a third time.
        dispatch(organiseRoutes(filteredPinpoints));

        setLoadingData(false)
    }, [filteredPinpoints]);

    if (loadingData) { return (<View><ActivityIndicator/></View>)}

    return(<ComponentUsingOrganisedRoutes/>)
Beolap
  • 768
  • 9
  • 15

2 Answers2

0

Looks like your filteredPinpoints is an object. useEffect does not do a deep equality check on object, for that you'll have to deal with it differently.

You can use a dependency like: [JSON.stringify(filteredPinpoints)], which would do a better check and won't be as slow as a deep equality check.

ref: https://twitter.com/dan_abramov/status/1104414272753487872

Danyal
  • 860
  • 7
  • 13
  • Just checked, but the behavior is the same. `console.log("Could EXECUTE now!!")` is only called if `dispatch(organiseRoutes(filteredPinpoints))` is commented out. – Beolap Jun 06 '20 at 17:45
  • Maybe try adding `Object.keys(filteredPinpoints).length` as the useEffect dependency? – Danyal Jun 06 '20 at 19:09
0

Okay, fixed it. Pretty stupid mistake. Basically, organiseRoutes() affects filteredPinpoints, so useEffect is caught in an infinite loop. I guess in these scenarios, console.log() is not called because it's being blocked.

I introduced a new state-hook organisedData aside to loadingData, to give the second useEffect another value to check for before it executes the dispatch. Once organisedData is true, it's going to stop executing and thereby stop triggering itself now.

import React, { useState, useEffect } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { getRouteData, organiseRoutes } from '../utils';

export default function RoutePreviewScreen() {

    const dispatch = useDispatch();
    const [loadingData, setLoadingData] = useState(true);
    const currentRouteID = useSelector(state => state.currentRouteID);
    const filteredPinpoints = useSelector(state =>
        ObjectFilter(state.allPinpoints, pinpoint => pinpoint.Route_ID == state.currentRouteID)
    );

    // New:
    const [organisedData, setOrganisedData] = useState(false);

    useEffect(() => {
        (async function myFirstAsyncFunction() {

            // New:
            setLoadingData(true)

            await dispatch(getRouteData(currentRouteID));

            // New:
            setOrganisedData(false)
        })();
    }, [currentRouteID]);

    useEffect(() => {

        // New:
        if (!loadingData || organisedData) {
            return
        }

        if (Object.keys(filteredPinpoints).length === 0) {
            return
        }

        // New:
        setOrganisedData(true)

        dispatch(organiseRoutes(filteredPinpoints));

        // New:
        setLoadingData(false)

    // Adjusted:
    }, [organisedData, loadingData, filteredPinpoints]);

    if (loadingData) { return (<View><ActivityIndicator/></View>)}

    return(<ComponentUsingOrganisedRoutes/>)
Beolap
  • 768
  • 9
  • 15