React-Redux - обновление объекта состояния, повторная визуализация компонентов, но изменение не передается дочернему компоненту - PullRequest
0 голосов
/ 13 июля 2020

Я работаю над приложением-календарем, используя React и Redux. Приложение календаря извлекает даты и их события из API Google, чтобы пользователи могли видеть свои календари Google. В отладчике я вижу, что данные в порядке от отправки до функции рендеринга. единственная проблема заключается в том, что компонент внутри (MonthlyCalendar), который получает события, получает пустой объект. когда не используется redux и вместо этого просто реагирует на состояние, компонент получает новые реквизиты с обновленным объектом событий. Я добавил console.log к каждому жизненному циклу компонента.

При использовании redux я получаю следующее: enter image description here

using regular state (not redux):

enter image description here

i don't know what I'm missing with redux flow but when debugging it's shown that render is executed with the updated state events, still the console.log print empty.

The code from dispatch to render :

Dispatch

useEffect(() => {
    window.addEventListener(
      'message',
      e => {
        if (e.data && e.data.data) {

          dispatch(connectBtnClicked(e.data.data));

        }
      },
      false,
    );
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }
  }, [])

Action

export const connectBtnClicked = (userName: string) => (dispatch: any) => {
  const urlArray = [
    'https://calendar-server.codev.co.il/getEvents',
    'https://calendar-server.codev.co.il/getCalendarsListIds',
    'https://calendar-server.codev.co.il/getSettings',
  ];
  const requestsArray = urlArray.map(url => {
    const request = new Request(url, {
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      method: 'GET',
    });

    return fetch(request).then(res => res.json());
  });
  Promise.all(requestsArray).then(allResults => {
    console.log('***allResults', allResults);
    dispatch({
      type: CONNECT_BTN_CLICKED,
      payload: {
        isConnect: true,
        userName,
        dates: allResults[0].eventsByDates,
        calendarsList: allResults[1].calendarsList,
      },
    });
  });
};

Reducer

import {
  CONNECT_BTN_CLICKED,
  SET_COMP_ID,
  DISCONNECT_BTN_CLICKED,
  CONNECT_ERROR,
} from '../actions/actionType';

const initState = {
  isConnect: false,
  isDisconnect: false,
  isSettingsLoaded: false,
  userName: '',
  isLoader: false,
  connectError: false,
  statusCode: '',
  dates: {},
  calendarsList: [],
};

export default (state = initState, action: any) => {
  console.log('setReducer', action);
  switch (action.type) {
    case SET_COMP_ID:
      return {
        ...state,
        compId: action.payload,
      };
    case CONNECT_ERROR:
      return {
        ...state,
        connectEtsyError: action.payload.err,
        statusCode: action.payload.statusCode,
      };
    case CONNECT_BTN_CLICKED:
      return {
        ...state,
        isConnect: action.payload.isConnect,
        userName: action.payload.userName,
        dates: action.payload.dates,
        calendarsList: action.payload.calendarsList,
        isSettingsLoaded: true,
      };
    case DISCONNECT_BTN_CLICKED:
      return {
        ...state,
        isConnect: false,
        // isDisconnect: action.payload,
        dates: {},
        calendarsList: [],
        userName: '',
      };
    default:
      return state;
  }
};

Root Reducer

import { combineReducers } from 'redux';
//@ts-ignore
import settings from './settingsReducer';

const rootReducer = combineReducers({
  settings,
});

export default rootReducer;

Store

import { createStore, applyMiddleware, compose } from 'redux';

import rootReducer from '../reducers/rootReducer';

import thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;

Provider Wrapping App.js Component

import { BrowserRouter as Router } from 'react-router-dom';

import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import { I18nextProvider } from 'react-i18next';
import App from './components/App/App';
// import App from './components/App';
import i18n from './i18n';
import { Provider } from 'react-redux';
import store from './components/settings/store';
const locale = window.__LOCALE__;
const baseURL = window.__BASEURL__;


fedopsLogger.appLoaded();

ReactDOM.render(
  
    Please wait...}>
      
        {/*  */}
        
        {/*  */}
      
    
  ,
  document.getElementById('root'),
);

App.js render - MonthlyCalendar is the events receiving component

render() {
    const { t } = this.props;
    const events = this.props.events;
    return (
      
         (
            
          )}
        >
         (
            
          )}
        >
         (
            
          )}
        >
      
    );
  }
}
const mapDispatchToProps = (dispatch: any) => ({});
const mapStateToProps = (state: any) => ({
  isConnect: state.settings.isConnect,
  userName: state.settings.userName,
  events: state.settings.dates,
  calendarsList: state.settings.calendarsList,
});

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(withTranslation()(withEnhancedStyleLoader(App)));

There is not setState in componentDidMount or any other method in App.js I removed it all, so only props change could re-render

Edit 1 i dropped all the HOCS and still in render i see the updated events, but the child component print empty events.

Render on events received

введите описание изображения здесь

EDIT 2

дочерний компонент отображается только один раз. даже когда свойства событий обновляются, и повторная визуализация MonthlyCalendar (дочерний компонент) не выполняется повторно. Я пробовал:

const events = {...this.props.events};

также:

const events = JSON.parse(JSON.stringfy(this.props.events);

не сработало ...

EDIT 3 - компонент MonthlyCalendar

constructor(props: any) {
    super(props);
    console.log('[constructor] props.events: ',props.events)
    const timezone = moment.tz.guess();
    const dateObject = moment().tz(timezone, true);
    this.state = {
      dateObject,
      timezone,
      isTimezonesOpen: false,
    };
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('[shouldComponentUpdate] props.events: ',this.props.events)
    return true
  }

....

getCalendar() {
    const { events } = this.props;
    const { dateObject } = this.state;
    const beforeFillers = this.getMonthBeforFillers(dateObject, events);
    const days = this.getDays(dateObject, events);
    const afterFillers = this.hasAfterFillers(beforeFillers, days) ?
      this.getAfterMonthFillers(dateObject, events) : {};
    return { days, beforeFillers, afterFillers };
  }



async componentDidUpdate(prevProps) {
    console.log('[componentDidUpdate] props.events: ',this.props.events)
    this.props.locale !== prevProps.locale && await this.updateLocale();
  }

  updateLocale = async () => {
    const { locale, i18n } = this.props;
    await i18n.changeLanguage(locale);
    moment.locale(locale);
    const { timezone, dateObject } = this.state;
    const dateObjectToSet = moment(dateObject.format()).tz(timezone, true);
    this.setState({ dateObject: dateObjectToSet });
  }

  async componentDidMount() {
    console.log('[componentDidMount] props.events: ',this.props.events)
    this.props.locale !== 'en' && await this.updateLocale();
  }

  render() {
    const { t, weekStarter, isTodayButtonStyleSeconday, isTimeZoneShown, isTimeShown } = this.props;
    const { dateObject, timezone, isTimezonesOpen } = this.state;
    const { days, beforeFillers, afterFillers } = this.getCalendar();
    const month = dateObject.format(t('Google_Calendar_Picker_Month'));
    const timezoneSelected = moment().tz(timezone).format(t('Google_Calendar_Timezone_Selected'));
    const timezoneSelectedTitle = t('Google_Calendar_Timezone_Selected_Title', { timezoneSelected });
    console.log('[render] props.events: ',this.props.events)
    return (
      <TPAComponentsProvider value={{ mobile: false, rtl: false }}>
        <div className={classes.MonthlyCalendar}>
          <CalendarControllers
            isTodayButtonStyleSeconday={isTodayButtonStyleSeconday}
            todayClicked={this.todayClickedHander}
            onPreviousClicked={() => this.timePickerClickedHandler(false)}
            timeToDisplay={month}
            onNextClicked={() => this.timePickerClickedHandler(true)}
            onTimezoneChange={this.timezoneChangeHandler}
            timezone={timezoneSelectedTitle}
            isTimezonesOpen={isTimezonesOpen}
            openTimezones={this.openTimezones}
            closeTimezones={this.closeTimezones}
            isTimeZoneShown={isTimeZoneShown}
          />
          <MonthTable
            weekStarter={weekStarter}
            days={days}
            beforeFillers={beforeFillers}
            dateObject={dateObject}
            afterFillers={afterFillers}
            renderCell={(
              time: any,
              events: any,
              cellRef: any,
              handleEventClick: any,
              setExpendedEvent: any,
              expendedEvent: any,
              isOutsideClicked: any,
            ) => (
                <MonthlyCell
                  events={events}
                  handleEventClick={handleEventClick}
                  time={time}
                  cellRef={cellRef}
                  expendedEvent={expendedEvent}
                  isOutsideClicked={isOutsideClicked}
                  setExpendedEvent={setExpendedEvent}
                  isTimeShown={isTimeShown}
                />
              )}
          />
        </div>
      </TPAComponentsProvider>
    );
  }
}

export default withTranslation()(MonthlyCalendar);

EDIT 4

после поиска решений я добавил ключ в div MonthlyCalendar, а также добавил деструктор {... this.props.events}. по-прежнему нет повторного рендеринга. Ниже приводится обновленный MonthlyCalendar:

getCalendar() {
    const mutableEvents = {...this.props.events};
    const { dateObject } = this.state;
    const beforeFillers = this.getMonthBeforFillers(dateObject, mutableEvents);
    const days = this.getDays(dateObject, mutableEvents);
    const afterFillers = this.hasAfterFillers(beforeFillers, days) ?
      this.getAfterMonthFillers(dateObject, mutableEvents) : {};
    return { days, beforeFillers, afterFillers };
  }

  async componentDidUpdate(prevProps) {
    console.log('[componentDidUpdate] props.events: ',this.props.events)
    this.props.locale !== prevProps.locale && await this.updateLocale();
  }

  updateLocale = async () => {
    const { locale, i18n } = this.props;
    await i18n.changeLanguage(locale);
    moment.locale(locale);
    const { timezone, dateObject } = this.state;
    const dateObjectToSet = moment(dateObject.format()).tz(timezone, true);
    this.setState({ dateObject: dateObjectToSet });
  }

  async componentDidMount() {
    console.log('[componentDidMount] props.events: ',this.props.events)
    this.props.locale !== 'en' && await this.updateLocale();
  }

  render() {
    const { t, weekStarter, isTodayButtonStyleSeconday, isTimeZoneShown, isTimeShown, events: propEvents } = this.props;
    const eventsKey = Object.keys(propEvents).length;
    const { dateObject, timezone, isTimezonesOpen } = this.state;
    const { days, beforeFillers, afterFillers } = this.getCalendar();
    const month = dateObject.format(t('Google_Calendar_Picker_Month'));
    const timezoneSelected = moment().tz(timezone).format(t('Google_Calendar_Timezone_Selected'));
    const timezoneSelectedTitle = t('Google_Calendar_Timezone_Selected_Title', { timezoneSelected });
    console.log('[render] props.events: ',this.props.events)
    return (
      <TPAComponentsProvider value={{ mobile: false, rtl: false }}>
        <div key={eventsKey} className={classes.MonthlyCalendar}>
          <CalendarControllers
            isTodayButtonStyleSeconday={isTodayButtonStyleSeconday}
            todayClicked={this.todayClickedHander}
            onPreviousClicked={() => this.timePickerClickedHandler(false)}
            timeToDisplay={month}
            onNextClicked={() => this.timePickerClickedHandler(true)}
            onTimezoneChange={this.timezoneChangeHandler}
            timezone={timezoneSelectedTitle}
            isTimezonesOpen={isTimezonesOpen}
            openTimezones={this.openTimezones}
            closeTimezones={this.closeTimezones}
            isTimeZoneShown={isTimeZoneShown}
          />
          <MonthTable
            weekStarter={weekStarter}
            days={days}
            beforeFillers={beforeFillers}
            dateObject={dateObject}
            afterFillers={afterFillers}
            renderCell={(
              time: any,
              events: any,
              cellRef: any,
              handleEventClick: any,
              setExpendedEvent: any,
              expendedEvent: any,
              isOutsideClicked: any,
            ) => (
                <MonthlyCell
                  events={events}
                  handleEventClick={handleEventClick}
                  time={time}
                  cellRef={cellRef}
                  expendedEvent={expendedEvent}
                  isOutsideClicked={isOutsideClicked}
                  setExpendedEvent={setExpendedEvent}
                  isTimeShown={isTimeShown}
                />
              )}
          />
        </div>
      </TPAComponentsProvider>
    );
  }
}

export default withTranslation()(MonthlyCalendar);
...