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.
Isn't your ternary statement switched? Your render function has this:
and your initialState in your reducer is this:
That would default to
this.props.data.protectedString
immediately on mount.You should always ask yourself these two questions when you are fetching data with react & redux:
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 thedidInvalidate
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 likeINVALIDATE_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:
Here is how your render should look like:
Here is your Account reducer with
didInvalidate
:Below your new lifecycle:
1. First render
{ isFetching: false, didInvalidate: true, data: null }
<Loading />
2. componentDidMount
3. Function called: accountFetchData (1)
{ type: ACCOUNT_FETCH_DATA_REQUEST }
4. Account Reducer
{ isFetching: true, didInvalidate: false, data: null }
5. Second render
{ isFetching: true, didInvalidate: false, data: null }
<Loading />
6. Function called: accountFetchData (2)
7. Account Reducer
{ isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }
8. Third render
{ isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }
<div>42: The answer to life</div>
Hope it helps.
Edit: Let me answer your question in one of your comment in the other answer
With the
didInvalidate
value, there is no risk of unlimited refetching as the component will know wether your data are valid or not.In the
componentDidMount
, the condition to refetch will be false as the values are the following{ isFetching: false, didInvalidate: false }
. No refetch then.Bonus: However be careful of data caching issues with the
didInvalidate
.People don't talk much about this issue but you will start asking this question "Starting when my data are invalid ?" (= When should I refetch ?)
Reducers
If I may, let me refactor your reducer code for the long run.
Your reducers will be way more modular and easy to maintain this way.
Actions
I will just add the invalidation function so it will be easier to understand which function I call in the component. (Note: I did not rename your functions but you should definitely pay attention at the naming)
Component
You will have to invalidate your data at some point. I considered that your account data would not be valid anymore after 60 minutes.
This code can be cleaner but it is clearer to read that way :)