Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redux Sagas firing multiple times if injected in different containers

We are using https://github.com/react-boilerplate/react-boilerplate and have a classic store layout with multiple components.

We have a redux store for adding products to the cart, which has a side effect saga to save the added product to the cart database.

Because there are multiple positions to add products to cart, we have used the same CartContainer on one page 3 times (the Cart itself, the product listing and in another product listing).

The problem we have now, is that the api will be called 3 times.

I guess that is because, by using the container 3 times, we also injected the Saga three times.

My question is now: What is the correct approach to only inject the Saga once, without having to rewrite all of the sagas again and again?

This is my saga:

import {
  call,
  put,
  select,
  takeLatest,
} from 'redux-saga/effects';
import {getRequest, putRequest} from '../../utils/request';

import {ADD_PRODUCT, LOAD_PRODUCTS} from './constants';
import {
  addProductSuccess,
  addProductError,
  productsLoaded,
  productsLoadingError,
} from './actions';
import {makeSelectProduct, makeSelectUserId} from './selectors';


/**
 * Github repos request/response handler
 */
export function* getProducts() {
  const requestURL = '/api/orders';
  const user_id = yield select(makeSelectUserId());
  try {
    let itemsData = yield call(getRequest, requestURL, user_id);
    if (itemsData) {
      itemsData = itemsData.slice(-1).items;
    }
    yield put(productsLoaded(itemsData));
  } catch (err) {
    yield put(productsLoadingError(err));
  }
}

/**
 * Github repos request/response handler
 */
export function* addProduct() {
  const requestURL = '/api/cart';
  const productData = yield select(makeSelectProduct());

  try {
    const orderData = yield call(putRequest, requestURL, productData);
    yield put(addProductSuccess(orderData.id));
  } catch (err) {
    yield put(addProductError(err));
  }
}

/**
 * Root saga manages watcher lifecycle
 */
export default function* root() {
  yield [
    takeLatest(ADD_PRODUCT, addProduct),
    takeLatest(LOAD_PRODUCTS, getProducts),
  ];
}

And this is the export part of my container:

export function mapDispatchToProps(dispatch) {
  return {
    onAddProduct: (id, quantity, variant) => {
      dispatch(addProduct(id, quantity, variant));
    },
    onLoadProducts: (user_id) => {
      dispatch(loadProducts(user_id));
    },
  };
}

const mapStateToProps = createStructuredSelector({
  products: makeSelectProducts(),
});

const withConnect = connect(mapStateToProps, mapDispatchToProps);

const withReducer = injectReducer({key: 'cart', reducer});
const withSaga = injectSaga({key: 'cart', saga});


export default compose(withReducer, withConnect)(CartContainer);
like image 854
SimonEritsch Avatar asked Jan 03 '18 18:01

SimonEritsch


2 Answers

You have to change the way you inject your saga so it will be injected only once.

In your containers/App/index.js

// ...
import { compose } from 'redux';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import reducer from 'containers/CartContainer/reducer';
import saga from 'containers/CartContainer/saga';

function App() { // We don't change this part
  // ...
}


// Add this lines
const withReducer = injectReducer({ key: 'cartContainer', reducer });
const withSaga = injectSaga({ key: 'cartContainer', saga });

export default compose(
  withReducer,
  withSaga,
)(App);

Now in your `containers/CartContainer/index.js``

import React from 'react':

// ...

class CartContainer extends React.Component {
  // ...
}

const withConnect = connect(mapStateToProps, mapDispatchToProps);

// REMOVE THIS LINES //
// const withReducer = injectReducer({ key: 'cartContainer', reducer });
// const withSaga = injectSaga({ key: 'cartContainer', saga });

export default compose(
 // withReducer,
 // withSaga,
 withConnect,
)(CartContainer);
like image 127
soupette Avatar answered Sep 19 '22 02:09

soupette


I guess you are using redux-sagas-injector which always looked weird to me. The saga has no place inside or around a React component. It looks like an anti-pattern to me. Your component should only dispatch actions and the saga that listens to these actions should simply handle the side effects. I'll suggest to initialize your redux store in a single place and do not use injectReducer or injectSaga.

P.S. Sorry but it looks weird to me seeing saga and reducer next to mapDispatchToProps and mapStateToProps.

like image 32
Krasimir Avatar answered Sep 21 '22 02:09

Krasimir