Search code examples
reactjsreduxreact-reduxredux-sagareact-boilerplate

React Redux action is being called before init


I am pretty new to Redux and the whole Redux-Saga thing and wanted to use React-Boilerplate to try a small project that basically just makes an API call and iterates over the data. And I currently have a problem I've been stuck at for hours. Maybe you have an idea?

My React Component looks like this:

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { compose } from 'redux';

import { useInjectSaga } from 'utils/injectSaga';
import { useInjectReducer } from 'utils/injectReducer';
import { 
  makeSelectDevices, 
  makeSelectLoading, 
  makeSelectError 
} from './selectors';
import reducer from './reducer';
import { fetchDevices } from './actions';
import saga from './saga';

export function LeafletMap(props) {
  const {devices, loading, error, fetchDevices } = props;

  useInjectReducer({ key: 'leafletMap', reducer });
  useInjectSaga({ key: 'leafletMap', saga });

  useEffect(() => {
    fetchDevices();
  }, [fetchDevices]);

  if (loading) return(<div>Loading...</div>)
  return (
    <div>
      { !error ? 
        <Map center={[47.3, 9.9]} zoom={9} style={{height: '500px'}}>
          <TileLayer 
              url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' 
              attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          />
            { devices && devices.map((device)=> {
                let coordinates = [device.latitude, device.longitude];
                return (
                  <Marker key={device.id} position={coordinates}></Marker>
                ); 
            })}
        </Map>
        : ''
      }
    </div>
  );
};

LeafletMap.propTypes = {
  devices: PropTypes.array,
  loading: PropTypes.bool,
  error: PropTypes.any,
};

const mapStateToProps = createStructuredSelector({
  devices: makeSelectDevices(),
  loading: makeSelectLoading(),
  error: makeSelectError(),
});

function mapDispatchToProps(dispatch) {
  return {
    fetchDevices: () => dispatch(fetchDevices())
  };
}

const withConnect = connect(
  mapStateToProps,
  mapDispatchToProps,
);

export default compose(withConnect)(LeafletMap);

When my component mounts I use the useEffect Hook to dispatch an action that I bound to my props using mapDispatchToProps. The actions file looks like this:

import { 
  FETCH_DATA, 
  FETCH_DATA_ERROR, 
  FETCH_DATA_SUCCESS,
  CLICK_DEVICE
} from './constants';

export function fetchDevices() {
  return {
    type: FETCH_DATA,
  };
}

export function fetchDevicesSuccess(devices) {
  return {
    type: FETCH_DATA_SUCCESS,
    devices
  };
}

export function fetchDevicesError(error) {
  return {
    type: FETCH_DATA_ERROR,
    error
  };
}

My saga then reacts to the FETCH_DATA action and calls a generator to fetch the data from my local API:

import { all, call, put, takeEvery } from 'redux-saga/effects';
import request from 'utils/request';
import { fetchDevicesSuccess, fetchDevicesError } from './actions';
import { FETCH_DATA } from './constants';

function* fetchDevicesAsync() {
  yield takeEvery(FETCH_DATA, fetchAllDevices);
}

function* fetchAllDevices() {
  try {
    const requestUrl = '/api/devices';
    const devices = yield call(request, requestUrl);

    yield put(fetchDevicesSuccess(devices));
  } catch (error) {
    yield put(fetchDevicesError(error.toString()));    
  }
}

export default function* rootSaga() {
  yield all([fetchDevicesAsync()]);
}

This in return should trigger my reducer which looks as follows:

import produce from 'immer';
import { 
  FETCH_DATA, 
  FETCH_DATA_ERROR, 
  FETCH_DATA_SUCCESS,
} from './constants';
export const initialState = {
  devices: [],
  loading: true,
  error: false,
};

/* eslint-disable default-case, no-param-reassign */
const leafletMapReducer = (state = initialState, action) =>
  produce(state, () => {
    switch (action.type) {
      case FETCH_DATA:
        state.loading = true;
        state.error = false;
        break;
      case FETCH_DATA_ERROR:
        state.loading = false
        state.error = action.error;
        break;
      case FETCH_DATA_SUCCESS:
        state.loading = false;
        state.error = false;
        state.devices = action.devices;
        break;
    }
  });

export default leafletMapReducer;

My problem here is that everything seems to work but my action is neither being displayed in Redux DevTools nor does my component update after the initial render. It seems as if the action is being dispatched before the @@INIT event.

action is missing but data is in the store

Any idea why this happens?

Thanks in advance!

EDIT:

Just in case it has something to do with my selectors:

import { createSelector } from 'reselect';
import { initialState } from './reducer';

/**
 * Direct selector to the leafletMap state domain
 */

const selectLeafletMapDomain = state => state.leafletMap || initialState;

/**
 * Other specific selectors
 */

const makeSelectDevices = () =>
  createSelector(
    selectLeafletMapDomain,
    leafletMapState => leafletMapState.devices
  ); 

const makeSelectLoading = () =>
  createSelector(
    selectLeafletMapDomain,
    leafletMapState => leafletMapState.loading,
  );

const makeSelectError = () =>
  createSelector(
    selectLeafletMapDomain,
    leafletMapState => leafletMapState.error,
  );

/**
 * Default selector used by LeafletMap
 */

const makeSelectLeafletMap = () =>
  createSelector(selectLeafletMapDomain, leafletMapState => leafletMapState.toJS());

export default makeSelectLeafletMap;
export { 
  selectLeafletMapDomain, 
  makeSelectDevices, 
  makeSelectLoading, 
  makeSelectError
};

Solution

  • Found the problem myself :) The problem was in my reducer:

    const leafletMapReducer = (state = initialState, action) =>
      produce(state, () => {             // <-- here
        switch (action.type) {
          case FETCH_DATA:
            state.loading = true;
            state.error = false;
            break;
    

    I here wrongly mutated my state which leads to the error. The correct solution is:

    const leafletMapReducer = (state = initialState, action) =>
      produce(state, draftState => {     // use draftState instead of normal state
        switch (action.type) {
          case FETCH_DATA:
            draftState.loading = true;   //<------
            draftState.error = false;    //<------
            break;