Trying out React + Redux, and probably am doing something obviously stupid, because a component that fires an action to fetch data over the network does not get updated (re-rendered) when the data is fetched.
Here are the relevant bits of my code:
The top-level index.js serving as an entry point for the app:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { Router, browserHistory } from 'react-router';
import reduxPromise from 'redux-promise';
import createLogger from 'redux-logger';
const logger = createLogger();
import routes from './routes';
import reducers from './reducers';
const createStoreWithMiddleware = applyMiddleware(reduxPromise, logger)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<Router history={browserHistory} routes={routes} />
</Provider>
, document.querySelector('.container'));
Top-level container App:
import React, {Component} from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
import Header from '../components/header';
import Showcase from '../components/showcase';
function mapStateToProps(state) {
return {
resources: state.resources
}
}
function mapDispatchToProps(dispatch) {
return {
fetchResources: () => {
dispatch(Actions.fetchResources());
}
}
}
class App extends Component {
render() {
console.log('props in App', this.props);
return (
<div>
<Header/>
<Showcase
fetchResources={this.props.fetchResources}
resources={this.props.resources}
/>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)
Component that triggers an action to sends a request for data when it is about to mount and is supposed to show the fetched data:
import React, {Component} from 'react';
import {connect} from 'react-redux';
class Showcase extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.fetchResources();
}
render() {
console.log('resources', this.props);
return (
<div>
This is showcase
</div>
);
}
}
export default connect(state => ({resources: state.resources}))(Showcase)
Action Creator:
import * as types from '../constants/ActionTypes';
import axios from 'axios';
export function fetchResources() {
return {
type: types.FETCH_FIRST,
payload: axios.get('/sampledata/1.json')
}
}
Reducer for the fetch action:
import * as types from '../constants/ActionTypes';
export default function resourcesReducer (state={}, action) {
switch (action.type) {
case types.FETCH_FIRST:
console.log('about to return', Object.assign (state, {resources: action.payload.data }))
return Object.assign (state, {resources: action.payload.data });
default:
return state
}
};
and finally the root reducer:
import { combineReducers } from 'redux';
import navigationReducer from './navigation-reducer';
import resourcesReducer from './resources-reducer';
const rootReducer = combineReducers({
navigationReducer,
resourcesReducer
});
export default rootReducer;
So, here is what I am observing. The action to request data is successfully triggered, a request is sent, the reducer receives it when the promise is resolved, and updates the state with the fetched data. At this point, I would expect the top-level App
component and the Showcase
component to detect that the store has updated, and to re-render, but I do not see it in the console.
Also, I am confused by redux-logger
’s console output:
Specifically, I am surprized to see that the state
contains reducers from the rootReducer — I don't know if it's right (an example on Redux logger Github page shows a state without reducers). It also seems surprising that the prev state
as reported by redux-logger
contains the same resourcesReducer
object as the next state
, although intuitively I would expect prev state
to be more or less empty.
Could you please point out what I am doing wrong and how to get React components respond to the state
changes?
==================================================
UPDATED:
1) Changed the mapStateToProps
function in the App
component so that it correctly maps to reducer states:
function mapStateToProps(state) {
return {
resources: state.resourcesReducer
}
}
2) Still passing the resources
down to the `Showcase component:
render() {
console.log('props in App', this.props);
return (
<div>
<Header navigateActions={this.props.navigateActions}/>
React simple starter
<Showcase
fetchResources={this.props.fetchResources}
resources={this.props.resources}
/>
</div>
);
3) Trying to display resources
on the screen by stringifying it to see what’s actually inside this object:
render() {
console.log('resources', this.props);
return (
<div>
This is showcase {JSON.stringify(this.props.resources)}
</div>
);
}
See this on the screen: This is showcase {}
. The component does not seem to re-render.
Here’s the screenshot of the console showing that App’s props have updated with the values from the next state
. Still, that did not cause the component to re-render:
UPDATED AGAIN: And my javascript-fu was poor, too. I did not quite realize that by returning Object.assign (state, {resources: action.payload.data });
I was in fact mutating the state, and that a simple inversion of arguments would let me achieve what I intended. Thanks to this discussion on SO for enlightenment.