redux-thunk vs redux-saga vs redux-observable
状態管理ミドルウェア
redux-thunkredux-sagaredux-observable類似パッケージ:
状態管理ミドルウェア

状態管理ミドルウェアは、Reduxストアのアクションを処理するためのライブラリであり、非同期処理や副作用の管理を容易にします。これらのライブラリは、アプリケーションの状態を管理し、アクションの流れを制御するための異なるアプローチを提供します。各ライブラリは、特定のユースケースや開発者の好みに応じて選択されるべきです。

npmのダウンロードトレンド
3 年
GitHub Starsランキング
統計詳細
パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
redux-thunk12,262,90217,72526.8 kB12年前MIT
redux-saga1,383,43722,4886.25 kB454ヶ月前MIT
redux-observable266,2397,82568.4 kB552ヶ月前MIT
機能比較: redux-thunk vs redux-saga vs redux-observable

非同期処理の管理

  • redux-thunk:

    Redux-Thunkは、関数をアクションとして返すことができるシンプルなアプローチを提供します。これにより、非同期処理を簡単に実装でき、API呼び出しやタイマーなどの非同期アクションを容易に扱えます。

  • redux-saga:

    Redux-Sagaは、Generator関数を使用して非同期処理を管理します。これにより、非同期処理のフローを直感的に記述でき、複雑なビジネスロジックを簡単に管理できます。Sagaは、アクションの発行や条件分岐、遅延処理などを簡単に実装できます。

  • redux-observable:

    Redux-Observableは、RxJSを使用して非同期処理をストリームとして扱います。これにより、複数のアクションを組み合わせたり、時間の経過に応じた処理を行ったりすることが可能です。ストリームの概念を利用することで、非同期処理のロジックを簡潔に表現できます。

テストの容易さ

  • redux-thunk:

    Redux-Thunkは、シンプルな構造のため、テストが比較的容易です。アクションクリエイターが関数を返すため、モック関数を使用して非同期処理をテストできます。ただし、複雑な非同期ロジックの場合は、テストが難しくなることがあります。

  • redux-saga:

    Redux-Sagaは、Generator関数を使用しているため、テストが容易です。Sagaをテストする際は、実行の進行状況を追跡できるため、非同期処理の各ステップを簡単に検証できます。特に、Sagaのロジックを分離してテストすることが可能です。

  • redux-observable:

    Redux-Observableは、RxJSのストリームを使用するため、テストが容易です。ストリームの出力を検証することで、非同期処理の結果を簡単に確認できます。テストの際に、モックやスパイを使用してストリームを操作することが可能です。

学習曲線

  • redux-thunk:

    Redux-Thunkは、最もシンプルなアプローチであり、学習曲線は緩やかです。非同期アクションを簡単に実装できるため、初心者にも適しています。特に、Reduxに慣れている開発者にはすぐに理解できるでしょう。

  • redux-saga:

    Redux-Sagaは、Generator関数の理解が必要であり、学習曲線は中程度です。非同期処理のフローを明示的に記述できるため、複雑なロジックを扱う際には有用ですが、最初は戸惑うこともあります。

  • redux-observable:

    Redux-Observableは、RxJSの概念を理解する必要があるため、学習曲線がやや急です。リアクティブプログラミングに慣れていない開発者には、最初は難しいと感じるかもしれませんが、慣れると強力なツールとなります。

設計原則

  • redux-thunk:

    Redux-Thunkは、シンプルさと柔軟性を重視し、非同期アクションを簡単に実装できるように設計されています。特に、軽量なアプリケーションや小規模なプロジェクトに適しています。

  • redux-saga:

    Redux-Sagaは、非同期処理をサガとして管理し、ビジネスロジックを分離することを重視しています。これにより、アプリケーションのロジックを明確にし、テスト可能なコードを実現します。

  • redux-observable:

    Redux-Observableは、リアクティブプログラミングの原則に基づいて設計されています。アクションをストリームとして扱い、非同期処理を宣言的に記述することができます。これにより、複雑な非同期ロジックを簡潔に表現できます。

拡張性

  • redux-thunk:

    Redux-Thunkは、シンプルな構造のため、拡張性は限られていますが、必要に応じて他のミドルウェアと組み合わせることで機能を追加できます。特に、軽量なアプリケーションには適しています。

  • redux-saga:

    Redux-Sagaは、非同期処理をサガとして管理するため、ビジネスロジックを明確に分離でき、拡張性が高いです。新しいサガを追加することで、アプリケーションの機能を簡単に拡張できます。

  • redux-observable:

    Redux-Observableは、RxJSの機能を活用することで、高度な非同期処理やストリームの組み合わせを容易に実現できます。これにより、複雑なアプリケーションでも拡張性が高くなります。

選び方: redux-thunk vs redux-saga vs redux-observable
  • redux-thunk:

    Redux-Thunkは、シンプルな非同期アクションを簡単に実装できるため、軽量なアプリケーションや小規模なプロジェクトに適しています。学習コストが低く、すぐに使い始めることができます。

  • redux-saga:

    Redux-Sagaは、Generator関数を使用して非同期処理を管理します。複雑な副作用や非同期フローを扱う必要がある場合に適しており、テストが容易です。特に、ビジネスロジックが複雑なアプリケーションに向いています。

  • redux-observable:

    Redux-Observableは、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