title: "Thunks in Core-Data" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - How To Guides - Repos - Data


Thunks in Core-Data

Gutenberg 11.6 added support for thunks. You can think of thunks as functions that can be dispatched:

// actions.js
export const myThunkAction = () => ( { select, dispatch } ) => {
    return "I'm a thunk! I can be dispatched, use selectors, and even dispatch other actions.";
};

Thunk 为何有用?

Thunk 扩展了 Redux action 的含义。在 Thunk 出现之前,action 是纯功能性的,只能返回和产出数据。诸如与 store 交互或从 action 请求 API 数据等常见用例,需要使用单独的 control。你经常会看到这样的代码:

export function* saveRecordAction( id ) {
    const record = yield controls.select( 'current-store', 'getRecord', id );
    yield { type: 'BEFORE_SAVE', id, record };
    const results = yield controls.fetch({ url: 'https://...', method: 'POST', data: record });
    yield { type: 'AFTER_SAVE', id, results };
    return results;
}

const controls = {
    select: // ...,
    fetch: // ...,
};

像 store 操作和 fetch 函数这样的副作用需要在 action 外部实现。Thunk 为这种方法提供了替代方案。它允许你内联使用副作用,像这样:

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

这消除了实现单独 controls 的需要。

Thunks 可以访问 store 辅助函数

让我们来看一个来自 Gutenberg 核心的例子。在 thunks 出现之前,@wordpress/interface 包中的 toggleFeature 动作是这样实现的:

export function* toggleFeature( scope, featureName ) {
    const currentValue = yield controls.select(
        interfaceStoreName,
        'isFeatureActive',
        scope,
        featureName
    );

    yield controls.dispatch(
        interfaceStoreName,
        'setFeatureValue',
        scope,
        featureName,
        ! currentValue
    );
}

Controls 曾经是 dispatch 动作和从 store select 数据的唯一方式。

有了 thunks,就有了更简洁的方法。这是 toggleFeature 现在的实现方式:

export function toggleFeature( scope, featureName ) {
    return function ( { select, dispatch } ) {
        const currentValue = select.isFeatureActive( scope, featureName );
        dispatch.setFeatureValue( scope, featureName, ! currentValue );
    };
}

得益于 selectdispatch 参数,thunks 可以直接使用 store,而无需生成器和 controls。

Thunk 可以是异步的

想象一个简单的 React 应用,允许你设置恒温器的温度。它只有一个输入框和一个按钮。点击按钮会分发一个 saveTemperatureToAPI 动作,并携带输入框中的值。

如果我们使用 controls 来保存温度,store 的定义会如下所示:

const store = wp.data.createReduxStore( 'my-store', {
    actions: {
        saveTemperatureToAPI: function*( temperature ) {
            const result = yield { type: 'FETCH_JSON', url: 'https://...', method: 'POST', data: { temperature } };
            return result;
        }
    },
    controls: {
        async FETCH_JSON( action ) {
            const response = await window.fetch( action.url, {
                method: action.method,
                body: JSON.stringify( action.data ),
            } );
            return response.json();
        }
    },
    // reducers, selectors, ...
} );

虽然这段代码相当直观,但存在一层间接性。saveTemperatureToAPI 动作并不直接与 API 通信,而是必须通过 FETCH_JSON 控制。

让我们看看如何使用 thunk 来消除这种间接性:

const store = wp.data.createReduxStore( 'my-store', {
    actions: {
        saveTemperatureToAPI: ( temperature ) => async () => {
            const response = await window.fetch( 'https://...', {
                method: 'POST',
                body: JSON.stringify( { temperature } ),
            } );
            return await response.json();
        }
    },
    // reducers, selectors, ...
} );

这非常棒!更棒的是,resolvers 也同样支持:

const store = wp.data.createReduxStore( 'my-store', {
    // ...
    selectors: {
        getTemperature: ( state ) => state.temperature
    },
    resolvers: {
        getTemperature: () => async ( { dispatch } ) => {
            const response = await window.fetch( 'https://...' );
            const result = await response.json();
            dispatch.receiveCurrentTemperature( result.temperature );
        }
    },
    // ...
} );

对 thunk 的支持默认包含在每个数据 store 中,就像(现已过时的)对生成器和 controls 的支持一样。

Thunks API

A thunk receives a single object argument with the following keys:

select

An object containing the store’s selectors pre-bound to state, which means you don't need to provide the state, only the additional arguments. select triggers the related resolvers, if any, but does not wait for them to finish. It just returns the current value even if it's null.

If a selector is part of the public API, it's available as a method on the select object:

const thunk = () => ( { select } ) => {
    // select is an object of the store’s selectors, pre-bound to current state:
    const temperature = select.getTemperature();
}

Since not all selectors are exposed on the store, select doubles as a function that supports passing a selector as an argument:

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

resolveSelect

resolveSelectselect 相同,区别在于它返回一个 Promise,该 Promise 会解析为相关解析器提供的值。

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

dispatch

An object containing the store’s actions

If an action is part of the public API, it's available as a method on the dispatch object:

const thunk = () => ( { dispatch } ) => {
    // dispatch is an object of the store’s actions:
    const temperature = await dispatch.retrieveTemperature();
}

Since not all actions are exposed on the store, dispatch doubles as a function that supports passing a Redux action as an argument:

const thunk = () => async ( { dispatch } ) => {
    // dispatch is also a function accepting inline actions:
    dispatch({ type: 'SET_TEMPERATURE', temperature: result.value });

    // thunks are interchangeable with actions
    dispatch( updateTemperature( 100 ) );

    // Thunks may be async, too. When they are, dispatch returns a promise
    await dispatch( ( ) => window.fetch( /* ... */ ) );
}

registry

注册表通过其 dispatchselectresolveSelect 方法提供对其他存储的访问。 这些方法与上述方法非常相似,但略有不同。调用 registry.select( storeName ) 会返回一个函数,该函数返回来自 storeName 的选择器对象。这在需要与另一个存储交互时非常方便。例如:

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