简体   繁体   中英

React Redux state array changes are not re-rendering a component

I have a project that uses React + Redux + Thunk and I am rather new to the stack. I have a scenario where I am fetching an array from an API call in my action/reducer, but it is not re-rendering in a component/container that is hooked up to the Store. The component does render the first time when I fire up the app, but at that point the array is undefined when logged to console.

I am trying to display the array's length, so this is always resulting in 0 . With ReduxDevTools I see that the state of network_identities does populate correctly and is longer zero... Where am I going wrong?

Here is my sample action

///////////// Sample action ///////////// 
import axios from 'axios';

const url = 'sample@url.com';
const authorization = 'sample_auth';

export function fetchConnections() {

    const params = {
            headers: {
            authorization,
        },
    };

    return (dispatch) => {
        // call returns an array of items
        axios.get(`${url}/connection`, params)
        .then((connections) => {

            let shake_profiles = [];
            let connected_profiles = [];
            let entity_res;

            // map through items to fetch the items data, and split into seperate arrays depending on 'status'
            connections.data.forEach((value) => {
                switch (value.status) {
                case 'APPROVED': case 'UNAPPROVED':
                    {
                    axios.get(`${url}/entity/${value.entity_id_other}`, params)
                    .then((entity_data) => {
                        entity_res = entity_data.data;
                        // add status
                        entity_res.status = value.status;
                        // append to connected_profiles
                        connected_profiles.push(entity_res);
                    });
                    break;
                    }
                case 'CONNECTED':
                    {
                    axios.get(`${url}/entity/${value.entity_id_other}`, params)
                    .then((entity_data) => {
                        entity_res = entity_data.data;
                        entity_res.status = value.status;
                        shake_profiles.push(entity_res);
                    })
                    .catch(err => console.log('err fetching entity info: ', err));
                    break;
                    }
                // if neither case do nothing
                default: break;
                }
            });

            dispatch({
                type: 'FETCH_CONNECTIONS',
                payload: { shake_profiles, connected_profiles },
            });
        });
    };
}

Sample Reducer

///////////// Sample reducer ///////////// 
const initialState = {
    fetched: false,
    error: null,
    connections: [],
    sortType: 'first_name',
    filterType: 'ALL',
    shake_identities: [],
    network_identities: [],
};

const connectionsReducer = (state = initialState, action) => {
    switch (action.type) {
    case 'FETCH_CONNECTIONS':
        console.log('[connections REDUCER] shake_profiles: ', action.payload.shake_profiles);
        console.log('[connections REDUCER] connected_profiles: ', action.payload.connected_profiles);
        return { ...state,
        fetched: true,
        shake_identities: action.payload.shake_profiles,
        network_identities: action.payload.connected_profiles,
        };
    default:
        return state;
    }
};

export default connectionsReducer;

Sample Store

///////////// Sample Store /////////////
import { applyMiddleware, createStore, compose } from 'redux';
import thunk from 'redux-thunk';
import promise from 'redux-promise-middleware';
import reducers from './reducers';

const middleware = applyMiddleware(promise(), thunk);
// Redux Dev Tools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(middleware));

export default store;

Sample Component - see if the API is done fetching the array, then display the length of the array

///////////// Sample Component /////////////
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import CSSModules from 'react-css-modules';
import * as ConnectionActions from 'actions/connections';

import styles from './styles.scss';

function mapStateToProps(state) {
return {
    network_identities: state.connections.network_identities,
    loadedConnections: state.connections.fetched,
};
}

function mapDispatchToProps(dispatch) {
return {
    actions: bindActionCreators(Object.assign({}, ConnectionActions), dispatch),
};
}

class Counter extends Component {
componentWillMount() {
    const { network_identities, actions } = this.props;
    if (!network_identities.length) {
    console.log('||| fetching Connections');
    actions.fetchConnections();
    }
}

render() {
    let { network_identities, loadedConnections} = this.props;

    console.log('[Counter] network_identities[0]: ', network_identities[0]);
    console.log('[Counter] network_identities: ', network_identities);
    console.log('[Counter] loadingConnections: ', loadingConnections);

    return (
    <div>
        <Link to="/network">
        <div>
            <span>Connections</span>
            { !loadedConnections ? (
            <span><i className="fa fa-refresh fa-spin" /></span>
            ) : (
            <span>{network_identities.length}</span>
            ) }
        </div>
        </Link>
    </div>
    );
}
}

export default connect(mapStateToProps, mapDispatchToProps)(CSSModules(Counter, styles));

I suspect I am either mutating the state in my reducer, or I am misusing Thunk.

The issue here is that you are making an async operation within a componentWillMount. When this lifecycle method is called,it does not block the render method from being called. That is, it does not wait until there is a response from its operations. So, rather move this async action to componentDidMount.

The problem in the code is that connections.data.forEach((value) => {..}) will send out a bunch of fetches, and then immediately return without waiting for the result arrays to be populated. A 'FETCH_CONNECTIONS' action is dispatched with empty arrays, and all connected components will rerender with the empty results.

What makes it tricky though is that the array objects that you put in the store will get pushed to once the fetches finish, so when you inspect the store it will seem populated correctly.

Not using any mutations will prevent the accidental population of the store, but won't solve the fact that dispatch is fired before the results are in. To do that, you could either create actions to add single results and dispatch those in the axios.get().then parts, or you could create a list of promises and wait for all of them to resolve with Promise.all() .

Here's what the latter solution could look like.

axios.get(`${url}/connection`, params)
.then((connections) => {

  const connectionPromises = connections.data.map((value) => {
    switch (value.status) {
      case 'APPROVED': case 'UNAPPROVED':
        return axios.get(`${url}/entity/${value.entity_id_other}`, params)
        .then((entity_data) => {
          return {connected_profile: {...entity_data.data, status: value.status}};
        });
      case 'CONNECTED':
        return axios.get(`${url}/entity/${value.entity_id_other}`, params)
        .then((entity_data) => {
            return {shake_profile: {...entity_data.data, status: value.status}};
        })
      // if neither case do nothing
      default:
        return {};
    }
  });

  Promise.all(connectionPromises)
  .then((connections) => {
    const connected_profiles =
      connections.filter((c) => c.connected_profile).map((r) => r.connected_profile);
    const shake_profiles =
      connections.filter((c) => c.shake_profile).map((r) => r.shake_profile);

    dispatch({
      type: 'FETCH_CONNECTIONS',
      payload: { shake_profiles, connected_profiles },
    });
  }).catch(err => console.log('err fetching entity info: ', err));

});

You'll probably want to use some more appropriate names though, and if you use lodash, you can make it a bit prettier.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM