Render component after action dispatch

2019-05-26 17:16发布

问题:

Upon page load, I am dispatching, inside my index.js, an action store.dispatch(getWeatherReports()); that hits a weather API. This action goes through the redux process and eventually adds the data returned to a property on the state called weatherReports. This property is an object with an empty array. Now, I'm going to paste in an overview of the code.. not all of it to save you the trouble from going line to line, as outputting the data from the API is NOT the issue I am having.

Here is my index.js:

import 'babel-polyfill';
import React from 'react';
import {render} from 'react-dom';
import configureStore from './store/configureStore';
import {Provider} from 'react-redux';
import {Router, browserHistory} from 'react-router';
import {StyleRoot} from 'radium';
import routes from './routes';
import {loadBlogs} from './actions/blogActions';
import {loadUsers, getActiveUser} from './actions/userActions';
import {getWeatherReports} from './actions/weatherActions';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../node_modules/toastr/build/toastr.min.css';
import './styles/app.scss';

const store = configureStore();
store.dispatch(loadBlogs());
store.dispatch(loadUsers());
store.dispatch(getActiveUser());
store.dispatch(getWeatherReports());

render(
  <StyleRoot>
    <Provider store={store}>
      <Router history={browserHistory} routes={routes}/>
    </Provider>
  </StyleRoot>,
  document.getElementById('app')
);

Trust that this goes through the right process to return the data. I have then created a smart component that takes this state and looks to pass it to a dumb component.

class DashboardPage extends React.Component {
  constructor(props, context) {
    super(props, context);

    this.state = {
      weatherReports: []
    };

  }

  <ContentBox
    title="What The Wind Blew In"
    content={<WeatherReport reports={this.props.weatherReports} />
  />

Again, trust that I have appropriately mapStateToProps, mapDispatchToProps, etc. I then look to display the data in the dumb component. WeatherReport.js:

import React, {PropTypes} from 'react';
import styles from '../common/contentBoxStyles';

const WeatherReport = ({reports}) => {
    console.log(reports);
    return (
        <div style={styles.body} className="row">
            <div style={styles.weatherBoxContainer}>
                <div className="col-sm-2 col-md-offset-1" style={styles.weatherBoxContainer.weatherCard}>
                    <div style={styles.weatherBoxContainer.weatherReport}>
                        <div style={styles.weatherBoxContainer.currentTemp}>
                            HERE IS MY PROBLEM!!
                            {reports[0].main.temp}
                        </div>
                    </div>
                    CA
                </div>
                <div className="col-sm-2" style={styles.weatherBoxContainer.weatherCard}>
                    <div style={styles.weatherBoxContainer.weatherReport}>
                        Report
                    </div>
                    UT
                </div>
                <div className="col-sm-2" style={styles.weatherBoxContainer.weatherCard}>
                    <div style={styles.weatherBoxContainer.weatherReport}>
                        Report
                    </div>
                    MN
                </div>
                <div className="col-sm-2" style={styles.weatherBoxContainer.weatherCard}>
                    <div style={styles.weatherBoxContainer.weatherReport}>
                        Report
                    </div>
                    DC
                </div>
                <div className="col-sm-2" style={styles.weatherBoxContainer.weatherCard}>
                    <div style={styles.weatherBoxContainer.weatherReport}>
                        Report
                    </div>
                    NY
                </div>
            </div>
        </div>
    );
};

WeatherReport.propTypes = {
    reports: PropTypes.array
};

export default WeatherReport;

My underlying issue is that before my dispatch action returns the data.. I am trying to render it through my dumb component. Thus I receive the following error when trying to access the data: Uncaught TypeError: Cannot read property 'main' of undefined(…) when doing the following: reports[0].main.temp What is happening is this is stopping my application from moving forward so that the store.dispatch(getWeatherReports()); never gets called. Thus if I remove the {reports[0].main.temp} from the equation, then the process continues and the action gets dispatched. Then I can call the equation w/ HMR and hooray!! The data is there...

So my question, and thanks for bearing with me, is how can I get it so that my dumb component waits to try and access these properties on the state until AFTER my initial dispatched actions occur?

回答1:

If that getWeatherReports request is being made by something that supports promises rather than callbacks to notify you of a successful response (i.e. something like axios), then you could try and use a middleware like redux-promise.

What redux-promise will do is intercept the dispatch action and stops it from forwarding the action to the store's subscribers if the promise has not been resolved.

Once you get a successful response back and the promise is resolved, the middleware creates a new action with the same action type as the original but with a payload that contains the data you need.

You'd use it as follows:

import { createStore, applyMiddleware } from 'redux';
import promise from 'redux-promise'; 

const store = configureStore();
store.dispatch(loadBlogs());
...
...
store.dispatch(getWeatherReports());

const storeWithMiddleWare = applyMiddleware(promise)(store);


回答2:

Seems like store.dispatch(getWeatherReports()) is an asynchronous API call. So you need to wait for the response before you render your component in the DOM.

Solution: use redux-connect.

Key points:

  • It allows you to request async data, store them in redux state and connect them to your react component.

  • This package consist of 2 parts: one part allows you to delay containers rendering until some async actions are happening. Another stores your data to redux state and connect your loaded data to your container.

Your component won't render until the promise is fulfilled.

The main advantages are:

  • Simple and elegant to configure for both CSR (Client-Side Rendering) and SSR (Server-Side Rendering).

  • you can react on fetching actions like data loading or load success in your own reducers

  • you can create own middleware to handle Redux Async Connect actions
  • you can connect to loaded data anywhere else, just using simple redux @connect
  • finally, you can debug and see your data using Redux Dev Tools

  • Also it's integrated with React Router to prevent routing transition until data is loaded.

Make sure you don't forget the 4 steps of configuration (described here):

  1. Connect your data, similar to react-redux @connect
  2. access data as props
  3. Connect redux async reducer
  4. Render Router with ReduxAsyncConnect middleware

So in DashboardPage (assuming getWeatherReports() will return a promise), what we can do is:

if you have babel presets for using decorators:

import { asyncConnect } from 'redux-connect';

@asyncConnect([{
  key: 'lunch',
  promise: ({ store: { dispatch }, params }) => dispatch(getWeatherReports())
}])
export default class DashboardPage extends React.Component {
 ...
}

otherwise:

class DashboardPage extends React.Component {
 ...
}


// sorry for the long name ;)
const asyncConnectedDashboardPage = asyncConnect([{
  key: 'lunch',
  promise: store.dispatch(getWeatherReports())
}])

export default asyncConnectedDashboardPage;

Use multipe promises,

@asyncConnect([{
  promise: ({ store: { dispatch, getState } }) => {
    const promises = [];
    const state = getState();
    if (/* some condition */) {
      promises.push(dispatch(loadBlogs()))
    }

    if (/* some other condition */) {
      promises.push(dispatch(loadUsers()))
    }

    promises.push(dispatch(getWeatherReports()))
    return Promise.all(promises);
  },
}])


回答3:

Check if reports[0] exists before trying to access .main. If it's undefined then display a loading spinner or something...

 if (!reports[0]) return <div>Loading...</div>

 return (
   // your component like normal.
 )