なぜ thunk は便利なのか?

Thunks Redux アクションの意味を拡張します。thunks が登場する前は、アクションは純粋に関数的で、データを返したり yield したりすることしかできませんでした。ストアとの相互作用やアクションから API データを要求するなどの一般的な使用例では、別の control を使用する必要がありました。よく見られるコードは次のようになります:

  1. export function* saveRecordAction( id ) {
  2. const record = yield controls.select( 'current-store', 'getRecord', id );
  3. yield { type: 'BEFORE_SAVE', id, record };
  4. const results = yield controls.fetch({ url: 'https://...', method: 'POST', data: record });
  5. yield { type: 'AFTER_SAVE', id, results };
  6. return results;
  7. }
  8. const controls = {
  9. select: // ...,
  10. fetch: // ...,
  11. };

ストア操作やフェッチ関数のような副作用は、アクションの外部で実装されていました。thunks はこのアプローチの代替手段を提供します。これにより、副作用をインラインで使用できるようになります。

  1. export const saveRecordAction = ( id ) => async ({ select, dispatch }) => {
  2. const record = select( 'current-store', 'getRecord', id );
  3. dispatch({ type: 'BEFORE_SAVE', id, record });
  4. const response = await fetch({ url: 'https://...', method: 'POST', data: record });
  5. const results = await response.json();
  6. dispatch({ type: 'AFTER_SAVE', id, results });
  7. return results;
  8. }

これにより、別のコントロールを実装する必要がなくなります。

thunks はストアヘルパーにアクセスできます

Gutenberg コアの例を見てみましょう。thunks が登場する前は、toggleFeature アクションは @wordpress/interface パッケージから次のように実装されていました:

  1. export function* toggleFeature( scope, featureName ) {
  2. const currentValue = yield controls.select(
  3. interfaceStoreName,
  4. 'isFeatureActive',
  5. scope,
  6. featureName
  7. );
  8. yield controls.dispatch(
  9. interfaceStoreName,
  10. 'setFeatureValue',
  11. scope,
  12. featureName,
  13. ! currentValue
  14. );
  15. }

コントロールは、ストアからアクションと select データを dispatch する唯一の方法でした。

thunks を使用すると、よりクリーンな方法があります。現在の toggleFeature の実装は次のようになります:

  1. export function toggleFeature( scope, featureName ) {
  2. return function ( { select, dispatch } ) {
  3. const currentValue = select.isFeatureActive( scope, featureName );
  4. dispatch.setFeatureValue( scope, featureName, ! currentValue );
  5. };
  6. }

selectdispatch 引数のおかげで、thunks はジェネレーターやコントロールを必要とせずにストアを直接使用できます。

thunks は非同期であることができます

サーモスタットの温度を設定できるシンプルな React アプリを想像してみてください。入力フィールドが 1 つとボタンが 1 つだけあります。ボタンをクリックすると、入力からの値を持つ saveTemperatureToAPI アクションがディスパッチされます。

コントロールを使用して温度を保存する場合、ストアの定義は次のようになります:

  1. const store = wp.data.createReduxStore( 'my-store', {
  2. actions: {
  3. saveTemperatureToAPI: function*( temperature ) {
  4. const result = yield { type: 'FETCH_JSON', url: 'https://...', method: 'POST', data: { temperature } };
  5. return result;
  6. }
  7. },
  8. controls: {
  9. async FETCH_JSON( action ) {
  10. const response = await window.fetch( action.url, {
  11. method: action.method,
  12. body: JSON.stringify( action.data ),
  13. } );
  14. return response.json();
  15. }
  16. },
  17. // reducers, selectors, ...
  18. } );

コードは比較的簡潔ですが、間接的なレベルがあります。saveTemperatureToAPI アクションは API に直接話しかけるのではなく、FETCH_JSON コントロールを通過する必要があります。

この間接性を thunks でどのように取り除けるか見てみましょう:

  1. const store = wp.data.createReduxStore( 'my-store', {
  2. actions: {
  3. saveTemperatureToAPI: ( temperature ) => async () => {
  4. const response = await window.fetch( 'https://...', {
  5. method: 'POST',
  6. body: JSON.stringify( { temperature } ),
  7. } );
  8. return await response.json();
  9. }
  10. },
  11. // reducers, selectors, ...
  12. } );

それはとてもクールです!さらに良いのは、リゾルバーもサポートされていることです:

  1. const store = wp.data.createReduxStore( 'my-store', {
  2. // ...
  3. selectors: {
  4. getTemperature: ( state ) => state.temperature
  5. },
  6. resolvers: {
  7. getTemperature: () => async ( { dispatch } ) => {
  8. const response = await window.fetch( 'https://...' );
  9. const result = await response.json();
  10. dispatch.receiveCurrentTemperature( result.temperature );
  11. }
  12. },
  13. // ...
  14. } );

thunks のサポートは、(現在はレガシーの)ジェネレーターやコントロールのサポートと同様に、すべてのデータストアにデフォルトで含まれています。

thunks API

thunk は、次のキーを持つ単一のオブジェクト引数を受け取ります:

select

ストアのセレクターが状態に事前バインドされたオブジェクトで、状態を提供する必要はなく、追加の引数のみを提供すればよいことを意味します。select は、関連するリゾルバーをトリガーしますが、完了を待ちません。現在の値を返すだけで、null であっても構いません。

セレクターが公開 API の一部である場合、select オブジェクトのメソッドとして利用可能です:

  1. const thunk = () => ( { select } ) => {
  2. // select is an object of the stores selectors, pre-bound to current state:
  3. const temperature = select.getTemperature();
  4. }

すべてのセレクターがストアに公開されているわけではないため、select は引数としてセレクターを渡すことをサポートする関数としても機能します:

  1. const thunk = () => ( { select } ) => {
  2. // select supports private selectors:
  3. const doubleTemperature = select( ( temperature ) => temperature * 2 );
  4. }

resolveSelect

resolveSelectselect と同じですが、関連するリゾルバーによって提供された値で解決されるプロミスを返します。

  1. const thunk = () => ( { resolveSelect } ) => {
  2. const temperature = await resolveSelect.getTemperature();
  3. }

dispatch

ストアのアクションを含むオブジェクト

アクションが公開 API の一部である場合、dispatch オブジェクトのメソッドとして利用可能です:

  1. const thunk = () => ( { dispatch } ) => {
  2. // dispatch is an object of the stores actions:
  3. const temperature = await dispatch.retrieveTemperature();
  4. }

すべてのアクションがストアに公開されているわけではないため、dispatch は引数として Redux アクションを渡すことをサポートする関数としても機能します:

  1. const thunk = () => async ( { dispatch } ) => {
  2. // dispatch is also a function accepting inline actions:
  3. dispatch({ type: 'SET_TEMPERATURE', temperature: result.value });
  4. // thunks are interchangeable with actions
  5. dispatch( updateTemperature( 100 ) );
  6. // Thunks may be async, too. When they are, dispatch returns a promise
  7. await dispatch( ( ) => window.fetch( /* ... */ ) );
  8. }

registry

レジストリは、dispatchselect、および resolveSelect メソッドを通じて他のストアへのアクセスを提供します。

これらは上記のものと非常に似ていますが、少し違いがあります。registry.select( storeName ) を呼び出すと、storeName からセレクターのオブジェクトを返す関数が返されます。これは、別のストアと対話する必要があるときに便利です。例えば:

  1. const thunk = () => ( { registry } ) => {
  2. const error = registry.select( 'core' ).getLastEntitySaveError( 'root', 'menu', menuId );
  3. /* ... */
  4. }