使用 TypeScript 在 React 组件外部调度 Redux Thunk 操作

Posted

技术标签:

【中文标题】使用 TypeScript 在 React 组件外部调度 Redux Thunk 操作【英文标题】:Dispatching a Redux Thunk action outside of a React component with TypeScript 【发布时间】:2019-10-23 04:34:49 【问题描述】:

我正在将 React Native 应用程序转换为 TypeScript,但在商店外调度 thunk 操作时遇到问题。以下是我的商店目前的设置方式:

store/index.ts

import  createStore, applyMiddleware, combineReducers, Reducer, Store  from 'redux';
import thunk,  ThunkMiddleware  from 'redux-thunk';

export interface State  ... 
export interface ActionTypes  ...  // All of the non-thunk actions

const reducer: Reducer<State> = combineReducers( ... );

export default (): Store<State> => 
  return applyMiddleware(
    thunk as ThunkMiddleware<State, ActionTypes>
  )(createStore)(reducer);

index.tsx

import  Provider  from 'react-redux';
import createStore from './store/index';
import  registerStore  from './store/registry'; 

const store = createStore();

registerStore(); // Registers the store in store/registry.ts

AppRegistry.registerComponent(appName, () => () => (
  <Provider store=store>
    <App />
  </Provider>
));

store/registry.ts

import  Store  from 'redux';
import  State  from './index';

let store: Store<State>;

export const registerStore = (newStore: Store<State>) => 
  store = newStore;
;

export const getStore = () => store;

所以当商店创建时,我将它存储在商店注册表中,这样我就可以从任何地方调用getStore()


这在组件(我不使用注册表)中运行良好,例如在我的 App.tsx 中:

import  connect  from 'react-redux';
import  ThunkDispatch  from 'redux-thunk';
import  checkAuthStatus as checkAuthStatusAction  from './store/modules/auth/actions';
import  ActionTypes, State as AppState  from './store/index';

interface State =  ... 
interface StateProps  ... 
interface DispatchProps 
  checkAuthStatus: () => Promise<boolean>;

type Props = StateProps & DispatchProps;

class App extends Component<Props, State> 
  async componentDidMount() 
    const promptSkipped: boolean = await checkAuthStatus(); // Thunk action, works fine!
  
  ...


const mapStateToProps = ...;
const mapDispatchToProps = (dispatch: ThunkDispatch<AppState, null, ActionTypes>): DispatchProps => (
  checkAuthStatus: () => dispatch(checkAuthStatusAction()),
);

export default connect<StateProps, DispatchProps, , AppState>(
  mapStateToProps,
  mapDispatchToProps,
)(App);

当我想使用注册表来调度一个 thunk 动作时,问题就来了:

lib/notacomponent.ts

import  getStore  from '../store/registry';
import  checkAuthStatus, setLoggedIn  from '../store/modules/auth/actions'

const someFunction = () => 
  const store = getStore();

  const  auth  = store.getState(); // Accessing state works fine!

  store.dispatch(setLoggedIn(true)); // NON-thunk action, works fine!

  store.dispatch(checkAuthStatus()); // Uh-oh, thunk action doesn't work.

这给了我错误:

Argument of type 'ThunkAction<Promise<boolean>, State, null, Action<any>>' is 
not assignable to parameter of type 'AnyAction'.

Property 'type' is missing in type 'ThunkAction<Promise<boolean>, State, null, Action<any>>'
but required in type 'AnyAction'. ts(2345)

据我所知,使用 thunk as ThunkMiddleware&lt;State, ActionTypes&gt; 作为中间件允许 Redux Thunk 将 stores 调度方法替换为一种可以调度 thunk 动作普通动作的方法。

我认为我需要以某种方式键入注册表,以便 TypeScript 可以看到调度方法 不是 只允许正常操作的默认方法。但是,我不知道如何做到这一点。我找不到任何其他人做同样事情的例子。

感谢任何帮助。


编辑:How to dispatch an Action or a ThunkAction (in TypeScript, with redux-thunk)? 的建议副本不能解决我的问题。我可以在组件内很好地调度 thunk 动作。使用上面定义的商店注册表,我只在组件之外遇到问题。


编辑 2:看来我可以在调度 thunk 操作时使用以下类型断言来消除错误:

(store.dispatch as ThunkDispatch<State, void, ActionTypes>)(checkAuthStatus())

但这是非常不切实际的。我还没有找到一种方法来做到这一点,所以 TypeScript 知道 dispatch 方法应该总是能够调度一个 thunk 动作。

【问题讨论】:

How to dispatch an Action or a ThunkAction (in TypeScript, with redux-thunk)?的可能重复 在查看 redux github 的问题后找到了该主题(github.com/reduxjs/redux-thunk/issues/135 - 看起来与您的情况相同) @skyboyer 不幸的是,我认为这没有帮助。它在组件内工作正常。我尝试在 pierpytom 的回答中添加(store.dispatch as ThunkDispatch&lt;State, void, ActionTypes&gt;),但这并没有改变任何东西。 嗯,这很奇怪。所以完整的调用看起来像(store.dispatch as ThunkDispatch&lt;State, void, ActionTypes&gt;)(checkAuthStatus()),这给出了关于幸运type 属性的相同错误,对吧?如果是的话,如何强制类型转换为(...args: any[]) =&gt; any 之类的东西? @skyboyer 实际上,(store.dispatch as ThunkDispatch&lt;State, null, ActionTypes&gt;)(checkAuthStatus()) 确实有效。我第一次没有将ThunkDispatch 的第二个参数更改为null。我可以对商店注册表做些什么来使情况始终如此吗?对每个 thunk 动作都这样做有点 hacky 【参考方案1】:
const boundActions = bindActionCreators(  checkAuthStatus , store.dispatch);
boundActions.checkAuthStatus();

这很有效,看起来不像“Edit2”那么老套

【讨论】:

【参考方案2】:

您的代码几乎是正确的。您错误地设置了默认导出的返回类型。

export default (): Store<State> => 
    return applyMiddleware(
        thunk as ThunkMiddleware<State, ActionTypes>
    )(createStore)(reducer);

在使用中间件的情况下,上面的函数不应返回Store&lt;State&gt;,而是Store&lt;S &amp; StateExt, A&gt; &amp; Ext,其中Ext 将被重载dispatch 将能够调度functions(就像redux-thunk 一样)。

为了简化,只需删除确切的返回类型,让 TypeScript 自行推断类型

export default () => 
    return applyMiddleware(
        thunk as ThunkMiddleware<State, ActionTypes>
    )(createStore)(reducer);

这就解决了你的问题。

或者,您可以使用更经典的方法来创建商店。

export default () => 
    return createStore(reducer, applyMiddleware(thunk as ThunkMiddleware<State, ActionTypes>));

这里的基本内容:

    按照Redux official doc 的建议使用createStorecreateStore 以中间件作为第二个参数调用本身将是 call it。但是 redux 和 redux-thunk 的 TypeScript 声明文件已预先配置为使用 createStore。所以返回的商店将有modified version 和dispatch。 (注意StoreEnhancer&lt;Ext, StateExt&gt;Ext and StateExt 类型参数。它们将与结果存储相交并添加dispatch 的重载版本,它将接受函数作为参数)。

    默认导出函数的返回类型也会从createStore的返回类型中推断出来。不会是Store&lt;State&gt;

【讨论】:

从导出中删除 Store&lt;State&gt; 并不能解决问题本身(我已经尝试删除它),因为 store 仍然被 store/registry 中的 TypeScript 推断为 any . 但是,切换到创建商店的“经典方法”让我可以在 VSCode 中准确查看返回的类型,但由于某种原因,我无法使用原始方法。所以现在我将store 输入为Store&lt;State, ActionTypes&gt; &amp; dispatch: `ThunkDispatch&lt;State, undefined, ActionTypes&gt;` 。不确定这是否是最好的方法,但它解决了问题。

以上是关于使用 TypeScript 在 React 组件外部调度 Redux Thunk 操作的主要内容,如果未能解决你的问题,请参考以下文章

使用 Typescript 创建 React 组件

如何在 TypeScript 中使用带有 React 无状态功能组件的子组件?

使用类型别名与接口在 Typescript 中注释 React 功能组件

在 TypeScript 中使用高阶组件在使用时省略 React 属性

如何在 React 上正确使用带有样式组件画布的 TypeScript 类型

使用react搭建组件库:react+typescript