Search code examples
reactjsreduxreact-reduxfrontendredux-saga

React: integrating a redux reducer to dandelion-pro project


end developer and recently I started to learn front-end. I have troubles with adding some new data to redux store. I am working with dandelion-pro react template and can't figure out how to add my reducers to their stores, it seems much more complex then redux stores I have build for other projects, also I observed they used redux saga. I am trying to introduce a global state for user data on login.

Here is code for my reducer

import { CallToAction } from '@material-ui/icons';
import { SUCCESSFUL_LOGIN, FETCH_LOGIN, ERROR_LOGIN } from '../../actions/actionConstants';

const initialState = {
    auth: false,
    isLoading: false,
    errMess: null,
    isAdmin: false,
    token: ''
}

export default function userReducer (state = initialState, action) {
    console.log("Action: ")
    console.log(action)
    switch (action.type) {
        case SUCCESSFUL_LOGIN: return {
            ...state,
            auth: true,
            isLoading: false,
            errMess: null,
            isAdmin: action.payload.isAdmin,
            token: action.payload.token
        }
        case FETCH_LOGIN: return {
            ...state,
            auth: false,
            isLoading: true,
            errMess: null
        }
        case ERROR_LOGIN: return {
            ...state,
            auth: false,
            isLoading: false,
            errMess: action.payload
        }
        default: return state
    }
}

Code for fetch user data

import { SUCCESSFUL_LOGIN, FETCH_LOGIN, ERROR_LOGIN } from '../../actions/actionConstants';
import axios from 'axios';
import { server } from '../../config'

export const fetchUser = (username, password) => (dispatch) => {
    
    console.log("a ajuns")
    dispatch(loginLoading(true));

    axios.post(`${server + "/auth/login"}`, { username, password })
        .then(res => {
            const user = res.data;
            console.log(user);

            if (user.status) {
                window.location.href = '/app';
                return dispatch(loginUser(user));
            }
            else {
                var errmess = new Error("False Status of User");
                throw errmess;
            }

      })
      .catch(error => dispatch(loginFailed(error.message)))
}

export const loginLoading = () => ({
    type: FETCH_LOGIN
});

export const loginFailed = (errmess) => {
    return ({
        type: ERROR_LOGIN,
        payload: errmess
    })
};

export const loginUser = (user) => ({
    type: SUCCESSFUL_LOGIN,
    payload: user
})

Section that combine reducers

/**
 * Combine all reducers in this file and export the combined reducers.
 */
import { reducer as form } from 'redux-form/immutable';
import { combineReducers } from 'redux-immutable';
import { connectRouter } from 'connected-react-router/immutable';
import history from 'utils/history';

import languageProviderReducer from 'containers/LanguageProvider/reducer';
import login from './modules/login';
import uiReducer from './modules/ui';
import initval from './modules/initForm';
import user from '../my_redux/modules/initForm';

/**
 * Creates the main reducer with the dynamically injected ones
 */
export default function createReducer(injectedReducers = {}) {
  const rootReducer = combineReducers({
    user,
    form,
    login,
    ui: uiReducer,
    initval,
    language: languageProviderReducer,
    router: connectRouter(history),
    ...injectedReducers,
  });

  // Wrap the root reducer and return a new root reducer with router state
  const mergeWithRouterState = connectRouter(history);
  return mergeWithRouterState(rootReducer);
}

I try to connect my Login component like this

const mapStateToProps = state => ({
  user: state.user
});

const mapDispatchToProps = dispatch => ({
  fetchUser: (username, password) => dispatch(fetchUser(username, password))
});

// const mapDispatchToProps = dispatch => ({
//   actions: bindActionCreators(userActions, dispatch),
// });

export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Login));

The store is created here

/**
 * Create the store with dynamic reducers
 */

import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import { fromJS } from 'immutable';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';

export default function configureStore(initialState = {}, history) {
  let composeEnhancers = compose;
  const reduxSagaMonitorOptions = {};

  // If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them
  /* istanbul ignore next */
  if (process.env.NODE_ENV !== 'production' && typeof window === 'object') {
    /* eslint-disable no-underscore-dangle */
    if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});

    // NOTE: Uncomment the code below to restore support for Redux Saga
    // Dev Tools once it supports redux-saga version 1.x.x
    // if (window.__SAGA_MONITOR_EXTENSION__)
    //   reduxSagaMonitorOptions = {
    //     sagaMonitor: window.__SAGA_MONITOR_EXTENSION__,
    //   };
    /* eslint-enable */
  }

  const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);

  // Create the store with two middlewares
  // 1. sagaMiddleware: Makes redux-sagas work
  // 2. routerMiddleware: Syncs the location/URL path to the state
  const middlewares = [sagaMiddleware, routerMiddleware(history)];

  const enhancers = [applyMiddleware(...middlewares)];

  const store = createStore(
    createReducer(),
    fromJS(initialState),
    composeEnhancers(...enhancers),
  );

  // Extensions
  store.runSaga = sagaMiddleware.run;
  store.injectedReducers = {}; // Reducer registry
  store.injectedSagas = {}; // Saga registry

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      store.replaceReducer(createReducer(store.injectedReducers));
    });
  }

  return store;
}

on login form submit I call this.props.fetchUser("admin", "admin"); but I get the following error:

Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
    at dispatch (redux.js:198)
    at eval (middleware.js:29)
    at eval (redux-saga-core.dev.cjs.js:1412)
    at Object.fetchUser (Login.js?f3c5:66)
    at Login.submitForm (Login.js?f3c5:30)
    at onSubmit (Login.js?f3c5:49)
    at executeSubmit (handleSubmit.js?e3b3:39)
    at handleSubmit (handleSubmit.js?e3b3:131)
    at Form._this.submit (createReduxForm.js?d100:362)
    at HTMLUnknownElement.callCallback (react-dom.development.js:149)

Solution

  • I reviewed my answer, and update it according to your question update

    The syntax you use for defining async function is called a thunk a fancy name for a function that return a promise (or async function), anyway to use that pattern in code you need a library called redux-thunk

    To apply the redux-thunk middle ware for your application,

    npm install redux-thunk
    

    then apply the middleware in your app store

    import { createStore, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import rootReducer from './reducers/index';
    
    // Note: this API requires redux@>=3.1.0
    const store = createStore(rootReducer, applyMiddleware(thunk));
    

    example from official repo of redux-thunk

    and for your code just add the thunk imported from redux-thunk in middleware array

      import thunk from 'redux-thunk';
    
      const middlewares = [sagaMiddleware, routerMiddleware(history), thunk];
    

    Now for Saga

    you need to have a root saga that run others sagas, and run the root saga from the created saga middleware

    here're the steps:

    1- create saga middleware(just like how you did, but we need to run the root saga from there too)

    import createSagaMiddleware from 'redux-saga'
    
    const sagaMiddleware = createSagaMiddleware();
    
    // after you've created the store then run the root saga
    sagaMiddleware.run(rootSagas);
    

    2- create your rootSaga

    export function* rootSagas() {
    
        try {
    
            yield fork(fetchUsersSaga);
    
    
        } catch (error) {
            console.warn(error);
        }
    }
    

    3- create your fetch user saga

    import { take, put, call } from "redux-saga/effects";
    
    export function* fetchUsersSaga() {
    
        while (true) {
            const action: FetchUser = yield take(FETCH_USER);
    
            try {
    
                const response = yield call(usersService.fetchUsersOfProject, { ...paramsPassedToFetchUserFunction })
    
                if (response) {
                    const { data: { response: { user } } } = response;
                    yield put(setUser({ user }));
                }
    
    
            } catch (error) {
                yield put(fetchUser());
            }
        }
    }
    

    now you need to notice the big difference between saga and thunk, for thunk you write an action that is hard coded to do one thing(or multiple but it still for a more specific case) and in saga you listen for what ever action the store has dispatched and react to that action in generator code style