redux-thunk vs redux-saga vs redux-observable
Redux 中间件
redux-thunkredux-sagaredux-observable类似的npm包:
Redux 中间件

Redux 中间件是用于处理异步操作、增强 Redux 功能和简化副作用管理的工具。它们允许开发者在 Redux 的 dispatch 和 reducer 之间插入自定义逻辑,从而实现更复杂的状态管理。选择合适的中间件可以显著提高应用程序的可维护性和可扩展性。每种中间件都有其独特的设计理念和使用场景,适合不同类型的项目需求。

npm下载趋势
3 年
GitHub Stars 排名
统计详情
npm包名称
下载量
Stars
大小
Issues
发布时间
License
redux-thunk9,888,72217,74126.8 kB12 年前MIT
redux-saga1,402,11422,5076.25 kB441 个月前MIT
redux-observable265,5097,82669.1 kB732 年前MIT
功能对比: redux-thunk vs redux-saga vs redux-observable

异步处理

  • redux-thunk:

    redux-thunk 允许你在 action creator 中返回一个函数,从而可以在函数内部进行异步操作。它的使用非常简单,适合处理简单的异步请求和小型应用。

  • redux-saga:

    redux-saga 使用 generator 函数来管理副作用,使得异步代码更具可读性和可维护性。它提供了强大的控制流功能,允许你轻松处理复杂的异步逻辑,如并行、串行和取消任务。

  • redux-observable:

    redux-observable 使用 RxJS 的 Observables 来处理异步操作,允许你以声明式的方式处理事件流和异步请求。它支持复杂的操作组合和取消请求,适合需要处理多个并发请求的场景。

学习曲线

  • redux-thunk:

    redux-thunk 的学习曲线相对平缓,开发者只需了解如何在 action creator 中返回函数即可。它非常直观,适合初学者和快速开发。

  • redux-saga:

    redux-saga 的学习曲线也较陡,尤其是对于不熟悉 generator 函数的开发者。尽管它提供了强大的功能,但需要时间来掌握其复杂的控制流和效果管理。

  • redux-observable:

    redux-observable 的学习曲线相对较陡,因为它依赖于 RxJS 和响应式编程的概念。开发者需要理解 Observables、操作符和流的概念,适合有一定 RxJS 经验的开发者。

可扩展性

  • redux-thunk:

    redux-thunk 的可扩展性相对有限,主要适用于简单的异步操作。对于复杂的业务逻辑,可能需要结合其他中间件来实现。

  • redux-saga:

    redux-saga 也非常可扩展,允许你通过创建 saga 和使用 effects 来管理复杂的异步流程。它适合需要高度可定制的副作用管理的应用。

  • redux-observable:

    redux-observable 具有很高的可扩展性,允许你通过组合多个 Observables 来创建复杂的异步逻辑。它适合需要处理多个事件流和复杂交互的应用。

调试支持

  • redux-thunk:

    redux-thunk 的调试支持相对简单,主要依赖于 Redux DevTools 来查看 action 和 state 的变化,适合对调试要求不高的应用。

  • redux-saga:

    redux-saga 提供了强大的调试工具,可以通过 saga 中间件的监控功能来跟踪和调试异步操作,适合需要详细了解副作用执行过程的开发者。

  • redux-observable:

    redux-observable 提供了良好的调试支持,可以通过 RxJS 的调试工具来观察和调试 Observables 的行为,适合需要深入分析异步流程的开发者。

性能

  • redux-thunk:

    redux-thunk 的性能在于其简单性,适合处理简单的异步请求。对于复杂的异步操作,可能会导致代码变得难以维护。

  • redux-saga:

    redux-saga 的性能表现良好,尤其是在处理复杂的异步逻辑时。它通过 generator 函数的协作特性,能够有效地管理异步任务的执行顺序。

  • redux-observable:

    redux-observable 的性能在于其基于流的处理方式,能够高效地处理多个并发请求和复杂的事件流。适合需要高性能异步处理的应用。

如何选择: redux-thunk vs redux-saga vs redux-observable
  • redux-thunk:

    选择 redux-thunk 如果你需要一个简单易用的解决方案来处理异步操作,尤其是对于小型项目或简单的 API 调用。它允许你在 action creator 中返回一个函数,而不是一个 action,适合快速开发和小型应用。

  • redux-saga:

    选择 redux-saga 如果你需要更强大的副作用管理,尤其是在处理复杂的异步流程和任务时。它使用 generator 函数,使得异步代码看起来更像同步代码,适合需要复杂业务逻辑的应用。

  • redux-observable:

    选择 redux-observable 如果你的应用需要处理复杂的异步操作,尤其是需要与多个外部 API 交互的场景。它基于 RxJS,提供了强大的响应式编程能力,适合需要处理流和事件的应用。

redux-thunk的README

Redux Thunk

Thunk middleware for Redux. It allows writing functions with logic inside that can interact with a Redux store's dispatch and getState methods.

For complete usage instructions and useful patterns, see the Redux docs Writing Logic with Thunks page.

GitHub Workflow Status npm version npm downloads

Installation and Setup

Redux Toolkit

If you're using our official Redux Toolkit package as recommended, there's nothing to install - RTK's configureStore API already adds the thunk middleware by default:

import { configureStore } from '@reduxjs/toolkit'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
  reducer: {
    todos: todosReducer,
    filters: filtersReducer
  }
})

// The thunk middleware was automatically added

Manual Setup

If you're using the basic Redux createStore API and need to set this up manually, first add the redux-thunk package:

npm install redux-thunk

yarn add redux-thunk

The thunk middleware is the default export.

More Details: Importing the thunk middleware

If you're using ES modules:

import thunk from 'redux-thunk' // no changes here 😀

If you use Redux Thunk 2.x in a CommonJS environment, don’t forget to add .default to your import:

- const thunk = require('redux-thunk')
+ const thunk = require('redux-thunk').default

Additionally, since 2.x, we also support a UMD build for use as a global script tag:

const ReduxThunk = window.ReduxThunk

Then, to enable Redux Thunk, use applyMiddleware():

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers/index'

const store = createStore(rootReducer, applyMiddleware(thunk))

Injecting a Custom Argument

Since 2.1.0, Redux Thunk supports injecting a custom argument into the thunk middleware. This is typically useful for cases like using an API service layer that could be swapped out for a mock service in tests.

For Redux Toolkit, the getDefaultMiddleware callback inside of configureStore lets you pass in a custom extraArgument:

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducer'
import { myCustomApiService } from './api'

const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      thunk: {
        extraArgument: myCustomApiService
      }
    })
})

// later
function fetchUser(id) {
  // The `extraArgument` is the third arg for thunk functions
  return (dispatch, getState, api) => {
    // you can use api here
  }
}

If you need to pass in multiple values, combine them into a single object:

const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      thunk: {
        extraArgument: {
          api: myCustomApiService,
          otherValue: 42
        }
      }
    })
})

// later
function fetchUser(id) {
  return (dispatch, getState, { api, otherValue }) => {
    // you can use api and something else here
  }
}

If you're setting up the store by hand, the named export withExtraArgument() function should be used to generate the correct thunk middleware:

const store = createStore(reducer, applyMiddleware(withExtraArgument(api)))

Why Do I Need This?

With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. Middleware extends the store's abilities, and lets you write async logic that interacts with the store.

Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests.

For more details on why thunks are useful, see:

You may also want to read the Redux FAQ entry on choosing which async middleware to use.

While the thunk middleware is not directly included with the Redux core library, it is used by default in our @reduxjs/toolkit package.

Motivation

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.

An action creator that returns a function to perform asynchronous dispatch:

const INCREMENT_COUNTER = 'INCREMENT_COUNTER'

function increment() {
  return {
    type: INCREMENT_COUNTER
  }
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment())
    }, 1000)
  }
}

An action creator that returns a function to perform conditional dispatch:

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState()

    if (counter % 2 === 0) {
      return
    }

    dispatch(increment())
  }
}

What’s a thunk?!

A thunk is a function that wraps an expression to delay its evaluation.

// calculation of 1 + 2 is immediate
// x === 3
let x = 1 + 2

// calculation of 1 + 2 is delayed
// foo can be called later to perform the calculation
// foo is a thunk!
let foo = () => 1 + 2

The term originated as a humorous past-tense version of "think".

Composition

Any return value from the inner function will be available as the return value of dispatch itself. This is convenient for orchestrating an asynchronous control flow with thunk action creators dispatching each other and returning Promises to wait for each other’s completion:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'

// Note: this API requires redux@>=3.1.0
const store = createStore(rootReducer, applyMiddleware(thunk))

function fetchSecretSauce() {
  return fetch('https://www.google.com/search?q=secret+sauce')
}

// These are the normal action creators you have seen so far.
// The actions they return can be dispatched without any middleware.
// However, they only express “facts” and not the “async flow”.

function makeASandwich(forPerson, secretSauce) {
  return {
    type: 'MAKE_SANDWICH',
    forPerson,
    secretSauce
  }
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: 'APOLOGIZE',
    fromPerson,
    toPerson,
    error
  }
}

function withdrawMoney(amount) {
  return {
    type: 'WITHDRAW',
    amount
  }
}

// Even without middleware, you can dispatch an action:
store.dispatch(withdrawMoney(100))

// But what do you do when you need to start an asynchronous action,
// such as an API call, or a router transition?

// Meet thunks.
// A thunk in this context is a function that can be dispatched to perform async
// activity and can dispatch actions and read state.
// This is an action creator that returns a thunk:
function makeASandwichWithSecretSauce(forPerson) {
  // We can invert control here by returning a function - the "thunk".
  // When this function is passed to `dispatch`, the thunk middleware will intercept it,
  // and call it with `dispatch` and `getState` as arguments.
  // This gives the thunk function the ability to run some logic, and still interact with the store.
  return function (dispatch) {
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    )
  }
}

// Thunk middleware lets me dispatch thunk async actions
// as if they were actions!

store.dispatch(makeASandwichWithSecretSauce('Me'))

// It even takes care to return the thunk’s return value
// from the dispatch, so I can chain Promises as long as I return them.

store.dispatch(makeASandwichWithSecretSauce('My partner')).then(() => {
  console.log('Done!')
})

// In fact I can write action creators that dispatch
// actions and async actions from other action creators,
// and I can build my control flow with Promises.

function makeSandwichesForEverybody() {
  return function (dispatch, getState) {
    if (!getState().sandwiches.isShopOpen) {
      // You don’t have to return Promises, but it’s a handy convention
      // so the caller can always call .then() on async dispatch result.

      return Promise.resolve()
    }

    // We can dispatch both plain object actions and other thunks,
    // which lets us compose the asynchronous actions in a single flow.

    return dispatch(makeASandwichWithSecretSauce('My Grandma'))
      .then(() =>
        Promise.all([
          dispatch(makeASandwichWithSecretSauce('Me')),
          dispatch(makeASandwichWithSecretSauce('My wife'))
        ])
      )
      .then(() => dispatch(makeASandwichWithSecretSauce('Our kids')))
      .then(() =>
        dispatch(
          getState().myMoney > 42
            ? withdrawMoney(42)
            : apologize('Me', 'The Sandwich Shop')
        )
      )
  }
}

// This is very useful for server side rendering, because I can wait
// until data is available, then synchronously render the app.

store
  .dispatch(makeSandwichesForEverybody())
  .then(() =>
    response.send(ReactDOMServer.renderToString(<MyApp store={store} />))
  )

// I can also dispatch a thunk async action from a component
// any time its props change to load the missing data.

import { connect } from 'react-redux'
import { Component } from 'react'

class SandwichShop extends Component {
  componentDidMount() {
    this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson))
  }

  componentDidUpdate(prevProps) {
    if (prevProps.forPerson !== this.props.forPerson) {
      this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson))
    }
  }

  render() {
    return <p>{this.props.sandwiches.join('mustard')}</p>
  }
}

export default connect(state => ({
  sandwiches: state.sandwiches
}))(SandwichShop)

License

MIT