Redux Sagas firing multiple times if injected in different containers
Asked Answered
M

2

5

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);
Mistranslate answered 3/1, 2018 at 18:10 Comment(2)
Have the saga at page level?. That way you only will have one saga registered. You can use sagaMiddleware.run(defaultSaga); which can be exposed from export const sagaMiddleware = createSagaMiddleware(); from wherever you are including saga middleware. Does this work for u?Brominate
Is the problem still running?Boughten
B
5

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);
Boughten answered 24/1, 2018 at 20:24 Comment(1)
What if the problem happens even if I don't use the container multiple times? Example: I have an App container with a router. If I am in a specific route (eg: "/containerName") a container with inject Saga and Reducer is rendered. I have the same problem written above, but the problem happen even if the container is used just one time.Medina
I
2

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.

Indehiscent answered 3/1, 2018 at 18:20 Comment(2)
how can I inject the saga and the redux store into the component then? Like how do I get hold of the right store in my component to assign it to the variable withSaga and withConnect? compose(withReducer, withSaga, withConnect)(CartContainer);Mistranslate
In my head the component should wired to Redux only via the Redux's connect function and basically fetches state and dispatches actions. React is not aware of the saga existence. It knows only about Redux's dispatch. You should have a root saga that gets initialized where you initialize your Redux store.Indehiscent

© 2022 - 2024 — McMap. All rights reserved.