React-Redux state in the component differs from the state in the store

I'm building an application with React and Redux.

I have an Account component that fetches data from the server in the componentWillMount method. While the data is being fetched, the component must show the "loading" text, so I've added the "isFetching" property to the account reducer. This property is set to true while data is fetching from the server.

The problem is, that while data is being fetched, the value of the "isFetching" property in the render method is false, while at the same time the value of store.getState().account.isFetching is true (as it must be). This causes the exception, because this.props.isFetching is false, so the code is trying to show the this.props.data.protectedString while the data is still being loaded from the server (so it is null).

I assume that the mapStateToProps bind some wrong value (maybe the initial state), but I cannot figure out why and how can I fix it.

Here is my AccountView code:

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actionCreators from '../../actions/account';

class AccountView extends React.Component {
    componentWillMount() {
        const token = this.props.token;
        // fetching the data using credentials
        this.props.actions.accountFetchData(token);
    }

    render() {
        console.log("store state", window.store.getState().account); // isFetching == true
        console.log("componentState", window.componentState); // isFetching == false
        return (
            <div>
                {this.props.isFetching === true ? <h3>LOADING</h3> : <div>{this.props.data.protectedString}</div> }
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    window.componentState = state.account;
    return {
        data: state.account.data,
        isFetching: state.account.isFetching
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        actions: bindActionCreators(actionCreators, dispatch)
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(AccountView);

Account reducer:

const initialState = {
    data: null,
    isFetching: false
};

export default function(state = initialState, action) {
    switch (action.type) {
    case ACCOUNT_FETCH_DATA_REQUEST:
        return Object.assign({}, state, {
            isFetching: true
        });
    case ACCOUNT_RECEIVE_DATA:
        return Object.assign({}, state, {
            data: action.payload.data,
            isFetching: false
        });
    default:
      return state;
  }
}

Actions:

export function accountReceiveData(data) {
    return {
        type: ACCOUNT_RECEIVE_DATA,
        payload: {
            data
        }
    };
}

export function accountFetchDataRequest() {
    return {
        type: ACCOUNT_FETCH_DATA_REQUEST
    };
}

export function accountFetchData(token) {
    return (dispatch, state) => {
        dispatch(accountFetchDataRequest());

        axios({
            // axios parameters to fetch data from the server
        })
        .then(checkHttpStatus)
        .then((response) => {
            dispatch(accountReceiveData(response.data));
        })
        .catch((error) => {
            //error handling
        });
    };
}

This is how I'm creating the store:

import { applyMiddleware, compose, createStore } from 'redux';
import { routerMiddleware } from 'react-router-redux';

import rootReducer from '../reducers';

export default function configureStore(initialState, history) {
    // Add so dispatched route actions to the history
    const reduxRouterMiddleware = routerMiddleware(history);

    const middleware = applyMiddleware(thunk, reduxRouterMiddleware);

    const createStoreWithMiddleware = compose(
        middleware
    );

    return createStoreWithMiddleware(createStore)(rootReducer, initialState);
}

And in index.js:

import { createBrowserHistory } from 'history';
import { syncHistoryWithStore } from 'react-router-redux';
import configureStore from './store/configureStore';

const initialState = {};
const newBrowserHistory = createBrowserHistory();
const store = configureStore(initialState, newBrowserHistory);
const history = syncHistoryWithStore(newBrowserHistory, store);

// for test purposes
window.store = store;

My code is based on this example - https://github.com/joshgeller/react-redux-jwt-auth-example

The code looks the same, but I've changed some places because of new versions of some modules.

2 answers

  • answered 2017-10-23 23:18 Andrew

    Isn't your ternary statement switched? Your render function has this:

    {this.props.isFetching === true ? <h3>LOADING</h3> : <div>{this.props.data.protectedString}</div> }
    

    and your initialState in your reducer is this:

    const initialState = {
      data: null,
      isFetching: false
    };
    

    That would default to this.props.data.protectedString immediately on mount.

  • answered 2017-10-23 23:18 yuantonito

    You should always ask yourself these two questions when you are fetching data with react & redux:

    1. Are my data still valid ?
    2. Am I currently fetching data ?

    You have already answered the second question by using the isFetching but the first question remains and that is what causing your problem. What you should do is to use the didInvalidate in your reducer (https://github.com/reactjs/redux/blob/master/docs/advanced/AsyncActions.md)

    With didInvalidate you can easily check if your data are valid and invalidate them if needed by dispatching an action like INVALIDATE_ACCOUNT. As you haven't fetched your data yet, your data are invalid by default.

    (Bonus) Some examples of when you might invalidate your data:

    • The last fetch date is > X minutes
    • You have modified some data and need to fetch this data again
    • Someone else has modified this data, you receive the invalidation action through Websockets

    Here is how your render should look like:

    class AccountView extends React.Component {
      componentDidMount() { // Better to call data from componentDidMount than componentWillMount: https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
        const token = this.props.token;
        // fetching the data using credentials
        if (this.props.didInvalidate && !this.props.isFetching) {
          this.props.actions.accountFetchData(token);
        }
      }
    
      render() {
        const {
          isFetching,
          didInvalidate,
          data,
        } = this.props;
    
        if (isFetching || (didInvalidate && !isFetching)) {
          return <Loading />; // You usually have your loading spinner or so in a component
        }
    
        return (
          <div>
            {data.protectedString}
          </div>
        );
      }
    }
    

    Here is your Account reducer with didInvalidate:

    const initialState = {
      isFetching: false,
      didInvalidate: true,
      data: null,
    };
    
    export default function(state = initialState, action) {
      switch (action.type) {
        case INVALIDATE_ACCOUNT:
          return { ...state, didInvalidate: true };
        case ACCOUNT_FETCH_DATA_REQUEST:
          return {
            ...state,
            isFetching: true,
          };
        case ACCOUNT_RECEIVE_DATA:
          return {
            ...state,
            data: action.payload.data,
            isFetching: false,
            didInvalidate: false,
          };
        default:
          return state;
      }
    }
    

    Below your new lifecycle:

    1. First render

    • Description: No fetch happened yet
    • Reducers: { isFetching: false, didInvalidate: true, data: null }
    • Render: <Loading />

    2. componentDidMount

    • Description: The data is invalidated && no fetching --> go fetch data

    3. Function called: accountFetchData (1)

    • Decription: Notify reducers that you are currently fetching and then fetch the data asynchronously
    • Dispatch: { type: ACCOUNT_FETCH_DATA_REQUEST }

    4. Account Reducer

    • Description: Reducers are notified of the dispatch and modifies their values accordingly
    • Reducers: { isFetching: true, didInvalidate: false, data: null }

    5. Second render

    • Description: Goes a second in the render because the Account reducer changed
    • Reducers: { isFetching: true, didInvalidate: false, data: null }
    • Render: <Loading />

    6. Function called: accountFetchData (2)

    • Description: Data are finally fetched from the step 3
    • Dispatch: { type: ACCOUNT_RECEIVE_DATA, payload: { data } }

    7. Account Reducer

    • Description: Reducers are notified of the dispatch and modifies their values accordingly
    • Reducers: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }

    8. Third render

    • Description: Data are fetched and ready to be displayed
    • Reducers: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }
    • Render: <div>42: The answer to life</div>

    Hope it helps.