简体   繁体   中英

Redux-Saga is running twice for a single action

I have a saga to handle like requests. The user clicks a button to toggle the liked status of a photo.

The saga listens for an action of type SUBMIT_LIKE . My problem is that the submitLikeSaga is running twice for each SUBMIT_LIKE action. eg In the api-error case one SUBMIT_LIKE action triggers two api calls and four RECEIVE_LIKE actions.

(using react-boilerplate if that helps.)

export function* submitLikeSaga(action) {

  // optimistically update the UI
  // action shape: {
  //    type: RECEIVE_LIKE,
  //    like: {id: 1, liked: true}
  //  }
  yield put(receiveLike(action.like));

  // POST like data to api
  const response = yield call(
    request,
    `${API_ENDPOINT}/user_likes.json`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${action.token}`,
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(action.like),
    }
  );

  // if api call failed, reverse change made to UI
  if (response.err) {
    yield put(receiveLike({
      id: action.like.id,
      liked: !action.like.liked,
    }));
  }
}

export function* watchSubmitLike() {
  yield* takeEvery(SUBMIT_LIKE, submitLikeSaga);
}

// All sagas to be loaded
export default [
  watchFetchUsers,
  watchSubmitLike,
];

EDIT: Add middleware and view code.

ProfileGrid.js

const ProfileGrid = ({
  users,
  submitLike,
  token,
}) =>
  <div className={styles.profileGrid}>
    {users.map((user, i) => (
      <div key={i} className={styles.gridTile}>
        <GridTile
          title={user.username}
          actionIcon={<ActionIcon
            onIconClick={() => { submitLike(user.id, !user.liked, token); }}

            isActive={user.liked}
            activeColor="yellow"
            defaultColor="white"
          />}
        >
          <img style={{ width: '100%', height: 'auto' }} src={user.avatar} alt="profile" />
        </GridTile>
      </div>
    ))}
  </div>;

ActionIcon.js

const ActionIcon = ({

  onIconClick,
  isActive,
  activeColor,
  defaultColor,
}) =>
  <IconButton onClick={onIconClick} >
    <StarBorder
      color={isActive ? activeColor : defaultColor}
    />
  </IconButton>;

store.js

/**
 * Create the store with asynchronously loaded reducers
 */

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

const sagaMiddleware = createSagaMiddleware();
const devtools = window.devToolsExtension || (() => (noop) => noop);

export default function configureStore(initialState = {}, history) {
  // 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),
    devtools(),
  ];

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

  // Extensions
  store.runSaga = sagaMiddleware.run;
  store.asyncReducers = {}; // Async reducer registry

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      System.import('./reducers').then((reducerModule) => {
        const createReducers = reducerModule.default;
        const nextReducers = createReducers(store.asyncReducers);

        store.replaceReducer(nextReducers);
      });
    });
  }

  return store;
}

asyncInjectors.js

import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash';
import invariant from 'invariant';
import warning from 'warning';
import createReducer from '../reducers';

/**
 * Validate the shape of redux store
 */
export function checkStore(store) {
  const shape = {
    dispatch: isFunction,
    subscribe: isFunction,
    getState: isFunction,
    replaceReducer: isFunction,
    runSaga: isFunction,
    asyncReducers: isObject,
  };
  invariant(
    conformsTo(store, shape),
    '(app/utils...) asyncInjectors: Expected a valid redux store'
  );
}

/**
 * Inject an asynchronously loaded reducer
 */
export function injectAsyncReducer(store, isValid) {
  return function injectReducer(name, asyncReducer) {
    if (!isValid) checkStore(store);

    invariant(
      isString(name) && !isEmpty(name) && isFunction(asyncReducer),
      '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function'
    );

    store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign
    store.replaceReducer(createReducer(store.asyncReducers));
  };
}

/**
 * Inject an asynchronously loaded saga
 */
export function injectAsyncSagas(store, isValid) {
  return function injectSagas(sagas) {
    if (!isValid) checkStore(store);

    invariant(
      Array.isArray(sagas),
      '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions'
    );

    warning(
      !isEmpty(sagas),
      '(app/utils...) injectAsyncSagas: Received an empty `sagas` array'
    );

    sagas.map(store.runSaga);
  };
}

/**
 * Helper for creating injectors
 */
export function getAsyncInjectors(store) {
  checkStore(store);

  return {
    injectReducer: injectAsyncReducer(store, true),
    injectSagas: injectAsyncSagas(store, true),
  };
}

Your app will work fine if you make following

yield* takeEvery(SUBMIT_LIKE, submitLikeSaga); should be

yield takeEvery(SUBMIT_LIKE, submitLikeSaga);

You should use yield* only when you are sequencing sagas

According to documentation : https://redux-saga.js.org/docs/api/index.html#takelatestpattern-saga-args this will take your latest call and and will fire only one action

import { fork, takeLatest } from 'redux-saga/effects';

export function* watchSubmitLike() {
  yield fork(takeLatest, SUBMIT_LIKE, submitLikeSaga);

}

I had this happen recently. In my case the saga was getting created twice. We we withSaga HOC (generally in a container)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM