useReducer() is returning data for one component and for other component it is returning the initialState

Viewed 281

I actually have 2 components that require data from a reducer that has some state there. The CurrentWeather component shows the weather at a very high level and the CurrentWeatherDetailed in a detailed way. The first one is rendered first, and if clicked on Details it will render CurrentWeatherDetailed. CurrentWeather dispatches an action to fetch some data and update the state with that data, later it gets the state through useReducer and it renders the data from that state as expected. On the other hand, CurrentWeatherDetailed, also tries to get the state that is currently populated, but it just returns null(which is the initialState) instead of what is actually in the state, considering that CurrentWeather, I assume; has already changed the state. So I have tried a lot but I have not found an explanation of why this happens.

This is the reducer:

export const initialState = {
    loading: false,
    error: null,
    data: null,
}

const weatherRequestReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'SEND':
            return {...state,
                loading: true,
                error: null,
                data: null,
            };
        case 'RESPONSE':
            console.log(state);
            console.log(action.responseData);
            return {...state,
                loading: false,
                data: action.responseData
            };
        case 'ERROR':
            return {...state,
                loading: false,
                error: action.errorMessage,
                data: null
            };
        case 'CLEAR-ERROR':
            return {...state,
                error:null
            };
        default:
            return state;
            // throw new Error('Should not get there')
    }
}

export default weatherRequestReducer;

And here is a hook that uses the reducer to generate a state and return it to other components:

import {useReducer, useCallback, useState, useEffect} from 'react';
import weatherRequestReducer from "../reducers/weatherRequestReducer";
import {initialState} from "../reducers/weatherRequestReducer";
import api from '../utils/WeatherApiConfig'

const useCurrentWeather = () => {

    const [weatherRequestState, dispatchWeatherRequestActions] = useReducer(weatherRequestReducer, initialState);


    const sendRequestToGetCurrentWeather = useCallback(() => {
        dispatchWeatherRequestActions({type: 'SEND'});

         const uri = 'weather?q=London,uk';
         api.get(uri)
             .then(res => {
                 const responseData = res.data;
                 console.log(responseData.base);
                 dispatchWeatherRequestActions({type: 'RESPONSE', responseData: responseData});
             })
             .catch(err => {
                 console.log(err);
                 dispatchWeatherRequestActions({type: 'ERROR', errorMessage: 'Something went wrong!'});
             });
    }, []);


    return {
        getCurrentWeatherPointer: sendRequestToGetCurrentWeather,
        isLoading: weatherRequestState.loading,
        error: weatherRequestState.error,
        data: weatherRequestState.data,
    }
}

export default useCurrentWeather;

Here is the App component calling the CurrentWeather and CurrentWeatherDetailed, depending on a dummy state:

import CurrentWeather from "./components/CurrentWeather/CurrentWeather";
import CurrentWeatherDetailed from "./components/CurrentWeather/CurrentWeatherDetailed";
import {useState} from "react";
import weatherRequestReducer from "./reducers/weatherRequestReducer";

function App() {

    const [state, setState] = useState(0);

    const renderComponent = () => {
        setState(1)
    }

    let comp = <CurrentWeather renderComponent={renderComponent}/>;
    if(state === 1){
        comp = <CurrentWeatherDetailed/>
    }

    return {comp}
}

export default App;

The following component is working fine and console.log(data), shows the actual data that has been asked for.

import React, {useEffect} from 'react';
import useCurrentWeather from "../../hooks/useCurrentWeather";


const CurrentWeather = (props) => {

    const {getCurrentWeatherPointer, isLoading, error, data} = useCurrentWeather();

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

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

    const displayDetails = () => {
        props.renderComponent();
    }

    return ( <Some JSX>
                <div className={classes.buttonContainer}>
                    <button onClick={displayDetails}>Details →</button>
                </div>
             <Some JSX> );
}

export default CurrentWeather;

Here is the component that is failing, because is getting data as null instead of what has been already fetched by the CurrentWeather component:

import React, {useEffect} from 'react';
import useCurrentWeather from "../../hooks/useCurrentWeather";

const CurrentWeatherDetailed = (props) => {

    const {getCurrentWeatherPointer, isLoading, error, data} = useCurrentWeather();

    useEffect(() => {
        console.log(data)
        setTimeout(() => {console.log(data);}, 500 );
    }, [data])

    return ( <Some JSX> );
}

export default CurrentWeatherDetailed;

If I just add useEffect(() => {getCurrentWeatherPointer();},[getCurrentWeatherPointer]) to CurrentWeatherDetailed it works fine as CurrentWeather, but, I do not want to do that because I already loaded that from the API to the reducer state, so it does not make sense to load that again, but to use what is already there when calling this detailed component.

Does anyone know why is this happening?

Thanks in advance guys.

2 Answers

The reason why this is happening is you are rendering only one component at a time and your components are siblings to each other. when your weather component fetches the data there is no weather details component . So when you click on the details button you are mounting the CurrentWeatherDetailed meaning you are creating the component from scratch. At this point your CurrentWeatherDetailed is not aware of what CurrentWeather had done because components are isolated from each other and the only way of sharing information between them is via props . So your CurrentWeatherDetailed initializes the reducer again.

You should move your useReducer to the App and remove it from CurrentWeatherDetailed and CurrentWeather . So that you can share the information between the CurrentWeatherDetailed and CurrentWeather.

function App() {
  const [state, setState] = useState(0);
  const {getCurrentWeatherPointer, isLoading, error, data} = useCurrentWeather();

  const renderComponent = () => {
    setState(1);
  };

  let comp = <CurrentWeather renderComponent={renderComponent} getCurrentWeatherPointer={getCurrentWeatherPointer}/>;
  if (state === 1) {
    comp = <CurrentWeatherDetailed data={data} />;
  }

  return {comp};
}

export default App;

You have a misconception there: useReducer does not share state over components. It is, like useState part of React and only to be used for local component state.
Data is not shared "in the reducer" (which is just a function), it only uses the reducer to describe the transitions of the state based on actions you dispatch.

If you want this data shared between components you have to move it up to a common parent and pass it down to all children that require it using props or context, or just use a global state management solution like Redux.

Related