Search code examples
javascriptreactjsreact-hooksuse-effectuse-reducer

A property of the state changed by useReducer() is never displaying changes even when those are dispatched


I have been working with useReducer() so that I can share some state among some components. There is a property in the requestReducer called isLoading which default/initial state is set to false. There are two functions dispatching actions to the reducer (initializeLoading: changing isLoading to true, and getRequestResponse: changing isLoading back to false). But when the function that the reducer uses to initialize loading -initializeLoading-, is called by CallingComponent that uses the pointer of those functions to call them; the change of state inside the useEffect (which has a dependency for the requestState.isLoading), is never displayed in the console or called neither in the useCurrentRequest hook nor the CallingComponent, when it is supposed to be called; because in that first calling the isLoading prop, from false, turns into true, through the SEND action.type. Moreover when calling the getRequestResponse function (which launches a RESPONSE action.type => isLoading = false) from the CallingComponent, it should switch from true, to false, thereby it should be displayed also by the useEffect that is supposed to capture those changes. But nothing of that happens, instead nothing is displayed.

Here is the useCurrentRequest and the requestReducer:

import {useReducer, useCallback, useEffect} from 'react';

export const initialState = {
    isLoading: false,
    data: null,
}

const requestReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'SEND':
            return {...state,
                isLoading: true,
            };
        case 'RESPONSE':
            return {...state,
                isLoading: false,
                data: action.responseData
            };
        default:
            return state;
    }
}

//This is the hook with the logic that sets state using the reducer
const useCurrentRequest = () => {

    const [requestState, dispatchRequestActions] = useReducer(requestReducer, initialState);

    //The loading change of state is not displayed in the console
    useEffect(() => {
        console.log(requestState.isLoading);
    }, [requestState.isLoading]);

    const initializeLoading = useCallback(() => {
        dispatchRequestActions({type: 'SEND'});
        console.log(requestState);
    },[]);

    const getRequestResponse = useCallback(() => {
        //some call to an api happens here
        //Then the response is given using the reducer 
        dispatchRequestActions({type: 'RESPONSE', responseData: {someData:true}});
    }, []);

    return {
        initializeLoadingPointer: initializeLoading,
        getRequestResponsePointer: getRequestResponse,
        isLoading: weatherRequestState.isLoading,
        data: weatherRequestState.data,
    }
}

export default useCurrentRequest;

And here is the CallingComponent, which uses the function pointers provided by the useCurrentRequest hook:

import {useEffect} from "react";
import useCurrentRequest from "../hooks/useCurrentRequest";

const CallingComponent = props => {
    const {initializeLoadingPointer, getRequestResponsePointer, isLoading} = useCurrentWeather();

    const getData = () => {
            initializeLoadingPointer();
            getRequestResponsePointer();
    };

    useEffect(() => {
        console.log(isLoading);
    }, [isLoading]);

    return (<button onClick={getData}>Get data</button>) 
}

The problem basically is that nothing is displayed after the Get Data button is clicked, when there is supposed to display in the console, true and false in that respective order, because of the useEffect() depending on isLoading and those functions changing isLoading, but again nothing is displayed.

I will appreciate any help on this.

Thanks! :)


Solution

  • I finally solved the issue by using async-await, the problem was that since the requests to those functions that were modifying the reducer were happening synchronously, the state was only updated until both of them were finished, and the function that was calling them has already finished too. So what I did was to block some pieces of code using promises via de async-await mode. Basically, I only modified useCurrentRequest() hook by replacing those 2 functions by this:

    const useCurrentRequest = () => {
    
        const [requestState, dispatchRequestActions] = useReducer(requestReducer, initialState);
    
        useEffect(() => {
            console.log(requestState.isLoading);
        }, [requestState.isLoading]);
    
      const sendRequestAsync = useCallback(async (city) => {
        const query = `weather?q=${city}`;
        console.log(query);
        dispatchRequestActions({ type: "SEND" });
    
        try {
          const result = await fakeRequest();
          dispatchRequestActions({ type: "RESPONSE", responseData: result.data });
        } catch (error) {
          dispatchRequestActions({ type: "ERROR", errorMessage: error.message });
        }
      }, []);
    
      const fakeRequest = () => {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve({ data: "data" });
          }, 1000);
        });
      };
    
        return {
            getRequestResponsePointer: sendRequestAsync,
            isLoading: weatherRequestState.isLoading,
            data: weatherRequestState.data,
        }
    }
    
    export default useCurrentRequest;
    

    That way I was able to see the changes using the useEffect() hook.