Redux Thunk中Redux Saga的yield take等价实现方式
yield take with Redux Thunk Great question! Redux Saga's yield take is one of those saga-specific effects that doesn't have a direct 1:1 equivalent in Redux Thunk—since Thunk is built around async/await and imperative logic, rather than Saga's generator-based listening model. But don't worry, we can absolutely replicate its behavior using Promises and Redux's core APIs. Let's break this down by use case.
1. One-time wait for an action (equivalent to yield take(actionType))
First, let's recall how yield take works in Saga: it pauses the generator until a specific action is dispatched, then returns that action object. For Thunk, we can create a reusable helper that wraps a Promise around Redux's subscription system to wait for the action.
Step 1: Add a middleware to track dispatched actions
First, let's add a simple middleware to keep track of the last action dispatched to the store. This makes it easy to check if our target action has fired:
// Middleware to track the last dispatched action const lastActionMiddleware = store => next => action => { store.lastAction = action; return next(action); }; // Add this to your store setup import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; const store = createStore( rootReducer, applyMiddleware(thunk, lastActionMiddleware) );
Step 2: Create a reusable take helper
This helper will return a Promise that resolves when the target action (or any action in an array of types) is dispatched:
/** * Replicates Saga's yield take behavior in Thunk * @param {Object} store - Redux store instance * @param {string|string[]} actionTypes - Single action type or array of types to wait for * @returns {Promise<Object>} Resolves with the dispatched action */ const take = (store, actionTypes) => { return new Promise((resolve) => { // Check if the action was already dispatched before we started listening const checkForAction = () => { const lastAction = store.lastAction; if (!lastAction) return; const matches = Array.isArray(actionTypes) ? actionTypes.includes(lastAction.type) : lastAction.type === actionTypes; if (matches) { resolve(lastAction); unsubscribe(); // Clean up the subscription once we get our action } }; // Check immediately in case the action fired already checkForAction(); // Subscribe to store updates to listen for the action const unsubscribe = store.subscribe(checkForAction); // Optional: Clean up if the promise is rejected (e.g., component unmounts) return () => unsubscribe(); }); };
Step 3: Use the helper in your Thunk
Now you can use await take(...) just like you used yield take(...) in Saga:
// Saga version function* loginFlowSaga() { const loginSuccessAction = yield take('USER_LOGIN_SUCCESS'); console.log('User logged in:', loginSuccessAction.payload.userId); // Do post-login logic } // Thunk equivalent const loginFlowThunk = () => async (dispatch, getState, { store }) => { // Pass store via thunk's extraArgument (setup below) const loginSuccessAction = await take(store, 'USER_LOGIN_SUCCESS'); console.log('User logged in:', loginSuccessAction.payload.userId); // Do post-login logic (e.g., dispatch other actions) dispatch({ type: 'LOAD_USER_DATA', payload: loginSuccessAction.payload.userId }); }; // To access the store in thunks, pass it as an extra argument when setting up thunk const store = createStore( rootReducer, applyMiddleware(thunk.withExtraArgument({ store })) );
2. Continuous listening (equivalent to yield takeEvery or looping yield take)
If you need to keep listening for an action indefinitely (like a Saga watcher that loops with while (true)), you can use a recursive function with our take helper:
// Saga version function* loginWatcherSaga() { while (true) { const action = yield take('USER_LOGIN_SUCCESS'); yield call(handleLoginSuccess, action.payload); } } async function handleLoginSuccess(payload) { // Your reusable login success logic console.log('Processing login for:', payload.userId); } // Thunk equivalent const loginWatcherThunk = () => async (dispatch, getState, { store }) => { const listenForLogin = async () => { const action = await take(store, 'USER_LOGIN_SUCCESS'); await handleLoginSuccess(action.payload); // Recurse to listen for the next login action listenForLogin(); }; // Start the listener listenForLogin(); };
Key Notes & Best Practices
- Cleanup: Always unsubscribe from store listeners when you no longer need them (e.g., when a component unmounts). The
takehelper returns an unsubscribe function you can call for this. - Multiple Action Types: The helper supports arrays of action types (e.g.,
take(store, ['LOGIN_SUCCESS', 'LOGIN_FAILURE'])), just like Saga'syield take([...]). - Race Conditions: If waiting for multiple actions, the first one dispatched will resolve the Promise—matching Saga's behavior exactly.
- Avoid Overcomplicating: For simple cases, you might not need a reusable helper—you can inline the Promise logic directly in your Thunk. The helper is just for reusability across multiple thunks.
内容的提问来源于stack exchange,提问作者Jorge Guerreiro

