简体   繁体   中英

Dispatching a Redux Thunk action outside of a React component with TypeScript

I'm in the process of converting a React Native app into TypeScript and I'm having trouble with dispatching thunk actions outside of the store. Here is how my store is set up currently:

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;

So when the store is created, I'm storing it in the store registry so I can call getStore() from anywhere.


This works fine in components (where I'm not using the registry), for example in my 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);

The problem comes when I want to use the registry to dispatch a thunk action:

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.
}

This gives me the error:

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)

As far as I am aware, using thunk as ThunkMiddleware<State, ActionTypes> as a middleware allows Redux Thunk to replace the stores dispatch method with one that makes it possible to dispatch thunk actions and normal actions.

I think I need to somehow type the registry in a way that TypeScript can see that the dispatch method is not the default one that only allows normal actions. I am, however, at a loss on how to do this. I can't find any examples of anyone else doing the same thing.

Any help is appreciated.


Edit : The suggested duplicate of How to dispatch an Action or a ThunkAction (in TypeScript, with redux-thunk)? doesn't solve my issue. I can dispatch thunk actions fine inside components. I'm only having issues outside components, using the store registry defined above.


Edit 2 : So it seems I can use the following type assertion when dispatching the thunk action to get rid of the error:

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

That is very impractical though. I'm yet to find a way to make it so TypeScript knows that the dispatch method should always be able to dispatch a thunk action.

Your code is almost correct. You incorrectly set return type of your default export.

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

In case of middleware usage, function above should return not Store<State> , but Store<S & StateExt, A> & Ext where Ext will be overloaded dispatch which will be able to dispatch functions (as redux-thunk do).

To simplify, just remove exact return type and let TypeScript infer type itself

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

Thats solve your problem.

Alternatively, you may use more classical approach to store creation.

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

Essential things here:

  1. Use createStore as recommended by Redux official doc . createStore called with middleware as second argument will itself call it . But TypeScript declaration files for redux and redux-thunk are preconfigured for such use of createStore . So returned store will have modified version of dispatch . (Make note of Ext and StateExt type parameters of StoreEnhancer<Ext, StateExt> . They will be intersected with resulting store and add overloaded version of dispatch which will accepts functions as arguments).

  2. Also return type of default exported fucntion will be inferred from createStore 's return type. It will NOT be Store<State> .

const boundActions = bindActionCreators( { checkAuthStatus }, store.dispatch);
boundActions.checkAuthStatus();

This works and does not look as hacky as 'Edit2'

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM