Изменения массива состояний React Redux не повторяют рендеринг компонента

У меня есть проект, который использует React + Redux + Thunk, и я новичок в стеке. У меня есть сценарий, в котором я извлекаю массив из вызова API в моем действии / редукторе, но это не повторный рендеринг в компоненте / контейнере, подключенном к Store. Компонент отображает первый раз, когда я запускаю приложение, но в этот момент массив равен undefined при входе в консоль.

Я пытаюсь отобразить длину массива, поэтому всегда получается 0. С ReduxDevTools я вижу, что состояние network_identities заполняется правильно и больше нуля ... Где я ошибаюсь?

Вот мой пример действия

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

const url = '[email protected]';
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 ///////////// 
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 /////////////
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;

Пример компонента - посмотрите, завершил ли API выборку массива, а затем отобразите длину массива

///////////// 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));

Я подозреваю, что я либо изменяю состояние в своем редукторе, либо неправильно использую Thunk.


person AdB    schedule 11.04.2018    source источник
comment
Быстрый вопрос, какую версию React вы используете? В React 16 были внесены некоторые изменения, которые могут изменить ответ на этот вопрос. До 16 вы могли использовать функцию жизненного цикла componentWillReceiveProps () и даже публиковать React 16, я думаю, вы все равно использовали бы это для синхронного обновления состояния. Передайте что-нибудь в componentWillReceiveProps (nextProps) и console.log nextProps, и вы должны увидеть свои изменения, которые затем можно использовать для обновления компонента по мере необходимости.   -  person Ron    schedule 11.04.2018
comment
Независимо от вашего вопроса, я бы рекомендовал отделить логику клиента api от действий. Такие вещи, как настройка пользовательских заголовков и авторизация, не должны обрабатываться в модуле создания действий.   -  person timotgl    schedule 11.04.2018
comment
@Ron React - это версия 15.6.2. Попробую эту стратегию, спасибо!   -  person AdB    schedule 11.04.2018
comment
Это connections.data.forEach отрывочно, так как он не будет ждать завершения выборки. После этого извлечения изменят состояние, но не вызовут повторную визуализацию, потому что диспетчеризация уже запущена. Может быть лучше сопоставить connections с обещаниями и дождаться результатов с .all().   -  person Oblosys    schedule 11.04.2018
comment
@AdB Я дважды проверил, и это должно быть нормально даже в 16. 16.3 - это то место, где происходит изменение, и теперь это считается устаревшим методом жизненного цикла. Эта статья объясняет это лучше, чем я. medium.com/@baphemot/whats-new-in- react-16-3-d2c9b7b6193b Также я добавил более подробный пример в качестве ответа ниже.   -  person Ron    schedule 11.04.2018
comment
Не думайте, что это имеет какое-то отношение к жизненным циклам. В [connections REDUCER] журналах консоли в connectionsReducer печатаются два пустых массива?   -  person Oblosys    schedule 11.04.2018
comment
@Oblosys [connections REDUCER] изначально регистрирует []. Когда я проверяю его и значение оценивается, он становится правильным массивом, заполненным данными ... Также я пробую ваш подход Promise.all (), просто пытаясь выяснить, как его реализовать в данный момент :)   -  person AdB    schedule 11.04.2018
comment
Думаю, я знаю, что происходит. Я могу выразить идею решения в ответ.   -  person Oblosys    schedule 11.04.2018
comment
@Oblosys Я весь в ушах! : D   -  person AdB    schedule 11.04.2018


Ответы (2)


Проблема в коде заключается в том, что connections.data.forEach((value) => {..}) отправит кучу выборок, а затем немедленно вернется, не дожидаясь заполнения массивов результатов. Действие 'FETCH_CONNECTIONS' отправляется с пустыми массивами, и все подключенные компоненты будут повторно визуализированы с пустыми результатами.

Что делает его сложным, так это то, что объекты массива, которые вы помещаете в хранилище, будут отправлены после завершения выборки, поэтому при проверке хранилища будет казаться, что он заполнен правильно.

Отказ от использования каких-либо мутаций предотвратит случайное заполнение магазина, но не решит тот факт, что отправка запускается до получения результатов. Для этого вы можете либо создать действия для добавления отдельных результатов, либо отправить их в axios.get().then частях , или вы можете создать список обещаний и дождаться их разрешения с помощью Promise.all().

Вот как могло бы выглядеть последнее решение.

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));

});

Вы, вероятно, захотите использовать более подходящие имена, а если вы используете lodash, вы можете сделать его немного красивее.

person Oblosys    schedule 11.04.2018
comment
Вот и все! Спасибо! Не могли бы вы объяснить двойную отдачу? эта часть меня смущает: return axios.get('content', params) .then((entity_data) => { return {connected_profile: {...entity_data.data, status: value.status}}; }); - person AdB; 11.04.2018
comment
Внешний возврат должен вернуть обещание в обратный вызов map, поэтому оно попадает в массив. Внутренний возврат должен вернуть значение, к которому будет разрешено обещание. Вы можете избавиться от внутреннего возврата, переписав (arg) => {return {...}} на (arg) => ({...}), если хотите. - person Oblosys; 11.04.2018

Проблема здесь в том, что вы выполняете асинхронную операцию внутри componentWillMount. Когда вызывается этот метод жизненного цикла, он не блокирует вызов метода визуализации. То есть он не ждет ответа от своих операций. Так что лучше переместите это асинхронное действие в componentDidMount.

person Siya    schedule 11.04.2018