Redux Thunk with Typescript - javascript

I am learning Typescript and I am trying to implement a simple React/Redux app. When I use sync actions it works fine, but the problems are with the async action. I am following the official redux tutorial.
First I declare the state for the session
export interface UserSessionState {
loggedIn: boolean;
}
Then I declare the interface for the action
interface UpdateSessionAction {
type: 'USER_LOGIN';
payload: boolean;
}
I export them with Union Types
export type UserActionTypes = UpdateSessionAction;
Then I have the actual Action
export function updateSession(loggedIn: UserSessionState) {
return {
type: 'USER_LOGIN',
payload: loggedIn,
};
}
I have a fake api call
function api() {
return Promise.resolve(true);
}
And finally the login
export const userLogin = (): ThunkAction<
void,
{},
{},
AnyAction
> => async (dispatch: ThunkDispatch<{}, {}, AnyAction>) => {
const res = await api();
dispatch(updateSession({ loggedIn: res }));
};
In the reducer I simply initialize the state
initialState: UserSessionState = {loggedIn: false}
Then I do the normal redux stuff for the reducer.
Finally in my store I call the initial action for checking the state
store.dispatch(userLogin());
I keep getting this error:
Argument of type 'ThunkAction<Promise<void>, {}, {}, AnyAction>' is not assignable to parameter of type 'AnyAction'.
Property 'type' is missing in type 'ThunkAction<Promise<void>, {}, {}, AnyAction>' but required in type 'AnyAction'.
I am missing a type but I have no idea what I do wrong.

In short:
You get this error because what returned from your userLogin() function is a ThunkAction, which is missing type
Why this is happening?
dispatch should accept parameter of type AnyAction.
AnyAction is a redux type, which extends Action (which have a mandatory property type).
This is from the current redux types file
export interface Action<T = any> {
type: T
}
/**
* An Action type which accepts any other properties.
* This is mainly for the use of the `Reducer` type.
* This is not part of `Action` itself to prevent users who are extending `Action.
*/
export interface AnyAction extends Action {
// Allows any extra properties to be defined in an action.
[extraProps: string]: any
}
How to fix it?
Use ThunkDispatch type instead of redux's standard Dispatch. The following example and more can be found on this Gist
const mapDispatchToProps = (dispatch: ThunkDispatch<MyState, void, Action>) => {
return {
onRequestClick: (arg: any) => dispatch(myAsyncAction(arg)),
};
}
Also, see this article, section Map Dispatch to Props

Not exactly answering the same thing the question was about, but I stumbled upon the same problem using Redux useDispatch() hook. And again not such a good support for the async action types.
What I eventualy did was to add an AsyncAction type and created a new hook for the typing:
// hooks.ts
export type AsyncAction = (dispatch: (action: Action) => any) => void;
export type Dispatcher = (action: AsyncAction | Action) => void
export const useAppDispatch: () => Dispatcher = useDispatch as any;
// async-actions.ts
import {AsyncAction} from "./hooks"
export const myAsyncAction: () => AsyncAction = () => (dispatch: (action:
Action) => any) => {
// async logic with dispatching goes here:
// dispatch(...)
}
//my-component.tsx
import {myAsyncAction} from "./async-actions"
import {useAppDispatch} from "./hooks"
export const MyComponent = () => {
const dispatch = useAppDispatch();
syncHandler () {
disaptch ({type: MY_SYNC_ACTION})
}
asyncHandler () {
disaptch (myAsyncAction())
}
...
}
Even though a bit Typescrip "hacky" the advantages of this approach are that you get full type check support for the sync and async (Thunk) actions, and from the client (component) point of view there is no difference - no need to use a different type system.

Related

Redux + Typescript + funtional component: imported component requires props that come from Redux

I have a component that looks something like this:
it has an interface with "alerts" property
it is connected to Redux and gets "alerts" from props.
interface IAlert {
alerts: { id: string; type: string; msg: string }[];
}
const Alert: FC<IAlert> = ({ alerts }) => {
return (
//does something with alerts
);
};
Alert.propTypes = {
alerts: PropTypes.array.isRequired
};
const mapStateToProps = (state: any): object => ({
alerts: state.alerts
});
export default connect(mapStateToProps, {})(Alert);
The Problem is:
When I import this component (that creates the Alerts) into another components I get this:
Property 'alerts' is missing in type '{}' but required in type 'Pick<IAlert, "alerts">'.ts(2741)
I don't want to pass "alerts" into the imported elemnt but just get it from Redux.
Thanks for the help!
It's a lot easier to get typescript support by using the useSelector hook like in #Jannis's answer.
You can get proper typing with connect. The reason that you aren't getting it here is because the types of your mapStateToProps function are incorrect.
const mapStateToProps = (state: any): object => ({
alerts: state.alerts
});
connect makes it so that the connected version of the component no longer requires the props which are added by mapStateToProps or mapDispatchToProps. But what props are those?. The type definition for your mapStateToProps doesn't say that it returns a prop alerts. It just returns object.
Returning IAlert makes your error go away because now connect knows that the alerts prop has already been provided.
const mapStateToProps = (state: any): IAlert => ({
alerts: state.alerts
});
If you have a proper type for your state instead of any then you shouldn't need any return type at all. For this particular component, your IAlert props type actually describes both the required state and the return.
const mapStateToProps = (state: IAlert) => ({
alerts: state.alerts
});
But generally you want to get the state type from your store or reducer as described here.
export type RootState = ReturnType<typeof rootReducer>
export type RootState = ReturnType<typeof store.getState>
You'll need to use that RootState in useSelector, but there's a helpful shortcut so that don't need to assign the type every time that you use it. You can create your own typed version of the useSelector hook as explained here.
import { TypedUseSelectorHook, useSelector } from 'react-redux'
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
So now your component doesn't need any props, and the type of the alerts variable is known based on the type of the state
export default () => {
const alerts = useAppSelector(state => state.alerts);
return (
//does something with alerts
);
};

typescript react redux actions that return promises

in my react app, i like to hide redux implementation from View logic in its own package 'i call it sdk package', and i export set fo react's Hooks from sdk package so any client can use it.
Question in short.
const fetch = (params) => (dispatch) => api.get('/media', params);
how can i tell typescript to skip the thunk and deal with the return value of thunk function
i have an Record object, how to type it to skip middle function ?
Context
code:
// hook.tsx
import * as actions from './actions'; // thunks
import * as selectors from './selectors'; // reselect.
import {useSelector, useDispatch} from 'react-redux';
export function useMedia(): [Selectors<typeof selectors>,Actions<typeof actions>] {
// use ref to cache hook
return useRef([
bindSelectors(selectors, useSelector),
bindDispatch(actions, useDispatch()),
]).current;
}
now inside my views, whenever i need to use that media slice i use.
import { useMedia } from '#sdk/media'
// ....
const [media, mediaActions] = useMedia();
// dispatch an action
mediaActions.fetch({limit:10}).then(console.log);
// select a slice using selector
const photos = media.photosOfAlbum(1);
my views doesnot know/care about how useMedia works, this way i could really split responsiblities inside my codebase, and ease codesharing, testing, etc.. since switching implementation anytime without affecting consumers of sdk (mobile/webapp/even nodejs apps) no one is aware that redux is powering sdk underhood.
problem is i cannot properly type these hooks .. (useMedia).
so we have 2 things to type here. the bindSelectors function and the bindDispatch function.
bindSelectors
code
// #sdk/utils
function bindSelectors <T extends object>(selectors:T, useSelector): CurriedFunctionObject<T>{
return new Proxy(selectors, {
get: (main: T, key: keyof T, ctx: any) => {
const fn = Reflect.get(main, key, ctx);
// always inject store as first prop of fn
// we get store arg from useSelector higher order (its already properly typed)
return (props?: any) => useSelector(store => fn(store, props));
},
}
this one i used to basicaly hack direct call on selector object asif first arg is always store.
export type CurriedFunctionObject<T> = {
[P in keyof T]: T[P] extends (
s: import('../rootReducer').AppState,
...p: infer Ps
) => infer R
? (...p: Ps) => R
: never;
};
now my selectors are bound and typed, my main problem is how to write Actions type..
bindDispatch
it works alot like bindSelectors. and i use ActionCreatorsMapObject from 'redux-thunk' to type it
export function bindDispatch<T extends Record<string, any>>(
obj: T,
dispatch: Dispatch,
): ActionCreatorsMapObject<T> {
return new Proxy(obj, {
get: (main: T, key: keyof T, ctx) => {
const fn = Reflect.get(main, key, ctx);
return (...props: any[]) => dispatch(fn(...props));
},
});
}
Problem with bindDispatch :-
if i dispatch a thunk that return a promise, its not typed correctly..
example above code media.fetch({limit:10}).then///.then is will give errorProperty 'catch' does not exist on type '(dispatch: any, getState: any, { }: { }) => any'`
so basically since fetch action looks like this
const fetch = (params) => (dispatch) => api.get('/media', params); so it expect a function (dispatch), so how can i tell typescript to skip the thunk and deal with the return value of thunk function
You could define something like this in your global.d.ts (or put it inside a separate redux.d.ts to separate it):
import { ThunkAction } from 'redux-thunk';
import { Action } from 'redux';
declare module 'redux' {
export interface Dispatch<A extends Action = AnyAction> {
<T extends ThunkAction<any, any, any, any>>(action: T): T extends ThunkAction<infer K, any, any, any> ? K : never;
}
}

Inferring mapped props when using TypeScript and React-Redux

I found a way to get type safety when using mapStateToProps from react-redux: as documented you can define an interface and parameterize React.Component<T> with your interface.
However, when I am defining mapStateToProps, I'm already defining a function where the types of the properties of the resulting object can be inferred. Eg,
function mapStateToProps(state: MyState) {
return {
counter: state.counter
};
}
Here, the prop counter can be inferred to be the same type as state.counter. But I still have to have boilerplate code like the following:
interface AppProps {
counter: number;
}
class App extends React.Component<AppProps> { ... }
export default connect(mapStateToProps)(App);
So the question is, is there any way to structure the code so that I can avoid writing the type of counter twice? Or to avoid parameterizing the type of React.Component -- even if I could have the props of the component inferred from an explicitly-hinted result type of the mapStateToProps function, that would be preferable. I am wondering if the duplication above is indeed the normal way to write typed components using React-Redux.
Yes. There's a neat technique for inferring the type of the combined props that connect will pass to your component based on mapState and mapDispatch.
There is a new ConnectedProps<T> type that is available in #types/react-redux#7.1.2. You can use it like this:
function mapStateToProps(state: MyState) {
return {
counter: state.counter
};
}
const mapDispatch = {increment};
// Do the first half of the `connect()` call separately,
// before declaring the component
const connector = connect(mapState, mapDispatch);
// Extract "the type of the props passed down by connect"
type PropsFromRedux = ConnectedProps<typeof connector>
// should be: {counter: number, increment: () => {type: "INCREMENT"}}, etc
// define combined props
type MyComponentProps = PropsFromRedux & PropsFromParent;
// Declare the component with the right props type
class MyComponent extends React.Component<MyComponentProps> {}
// Finish the connect call
export default connector(MyComponent)
Note that this correctly infers the type of thunk action creators included in mapDispatch if it's an object, whereas typeof mapDispatch does not.
We will add this to the official React-Redux docs as a recommended approach soon.
More details:
Gist: ConnectedProps - the missing TS helper for Redux
Practical TypeScript with React+Redux
DefinitelyTyped #31227: Connected component inference
DefinitelyTyped PR #37300: Add ConnectedProps type
I don't think so. You could make your setup more concise by using Redux hooks: https://react-redux.js.org/next/api/hooks
// Your function component . You don't need to connect it
const App: React.FC = () => {
const counter = useSelector<number>((state: MyState) => state.counter);
const dispatch = useDispatch(); // for dispatching actions
};
Edit: You can if you just use the same MyState type. But I don't think you would want that.
I would type mapped dispatch props, and component props separately and then combine the inferred type of the mapped state to props function. See below for a quick example. There might be a more elegant solution but hopefully, it will hopefully get you on the right track.
import * as React from "react";
import { Action } from "redux";
import { connect } from "react-redux";
// Lives in some lib file
type AppState = {
counter: number;
};
type MappedState = {
computedValue: number;
};
type MappedDispatch = {
doSomethingCool: () => Action;
};
type ComponentProps = {
someProp: string;
};
const mapStateToProps = (state: AppState) => ({
computedValue: state.counter
});
const mapDispatchToProps: MappedDispatch = {
doSomethingCool: () => {
return {
type: "DO_SOMETHING_COOL"
};
}
};
type Props = ReturnType<typeof mapStateToProps> &
MappedDispatch &
ComponentProps;
class DumbComponent extends React.Component<Props> {
render() {
return (
<div>
<h1>{this.props.someProp}</h1>
<div>{this.props.computedValue}</div>
<button onClick={() => this.props.doSomethingCool()}>Click me</button>
</div>
);
}
}
const SmartComponent = connect(
mapStateToProps,
mapDispatchToProps
)(DumbComponent);
export default SmartComponent;

how to type react-redux connect in flow?

This is what my mapStateToProps looks like.
const mapStateToProps = (state): StateProps => {
let status = false;
if (state.productsPage.currentProduct) {
if (state.productsPage.currentProduct.status === "ACTIVE") {
status = true;
}
}
return {
showModal: state.productsPage.showModal,
currentProduct: state.productsPage.currentProduct,
isLoading: state.status.loading.PRODUCTS_EDIT,
initialValues: {
name: state.productsPage.currentProduct ? state.productsPage.currentProduct.name : "",
status,
},
};
};
Here is the shape of StateProps
type StateProps = {
showModal: boolean,
currentProduct: Object,
isLoading: boolean,
initialValues: {
name: string,
status: boolean,
}
}
This is my usage of connect.
const connected = connect<React$StatelessFunctionalComponent<any>, StateProps>(mapStateToProps, mapDispatchToProps);
This produces the following error, and I have no idea what it means or how to go about solving it.
[flow] Cannot call connect because property currentProduct is
missing in React.StatelessFunctionalComponent [1] in the indexer
property's key of type argument ST. (References: [1])
It is recommended that you upgrade Flow to at least past 0.89, and use Flow Typed's newest React Redux library definition.
Then, you can annotate the connected components with Props and OwnProps
import * as React from 'react'
type OwnProps = {|
passthrough: string,
forMapStateToProps: string
|}
type Props = {|
...OwnProps,
fromMapStateToProps: string,
dispatch1: () => void
|}
const MyComponent = (props: Props) => (
<div onClick={props.dispatch1}>
{props.passthrough}
{props.fromMapStateToProps}
</div>
)
export default connect<Props, OwnProps, _, _, _, _>(
mapStateToProps,
mapDispatchToProps
)(MyComponent)
Here is a more detailed guide that includes a couple more use cases.
React$StatelessFunctionalComponent is a generic that expects prop types. Instead of <any>, you would want the props that that function expects to receive -- most importantly, it expects to receive your StateProps returned by mapStateToProps (though it may expect others in addition).
It may change depending on version of react-redux, but I am seeing some documentation that suggests that the generics for connect are the return value of mapStateToProps, not a React element. - Source
You may be needing connect<StateProps, DispatchPropsIfAny> instead of providing an Element type.

'ItemIsLoading' on refers to a type, but is being used as a value here

I am in the process of building a typescripted Redux store for a react project. The Interfaces seem to be working fine, but when I try to pass them within the exports themselves. I receive the following error "'ItemIsLoading' on refers to a type, but is being used as a value here"
Actions:
import * as constants from '../constants/constantsIndex';
export interface ItemHasErrored {
type: constants.ITEMS_HAS_ERRORED;
}
export interface ItemIsLoading {
type: constants.ITEMS_IS_LOADING;
}
export interface ItemFetchDataSuccess {
type: constants.ITEMS_FETCH_DATA_SUCCESS;
}
export function itemsFetchData(url: any) {
return (dispatch) => {
dispatch(ItemIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(ItemIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(ItemFetchDataSuccess(items)))
.catch(() => dispatch(ItemHasErrored(true)));
};
}
Constants:
export const ITEMS_HAS_ERRORED = 'ITEMS_HAS_ERRORED';
export type ITEMS_HAS_ERRORED = typeof ITEMS_HAS_ERRORED;
export const ITEMS_IS_LOADING = 'ITEMS_IS_LOADING';
export type ITEMS_IS_LOADING = typeof ITEMS_IS_LOADING;
export const ITEMS_FETCH_DATA_SUCCESS = 'ITEMS_FETCH_DATA_SUCCESS';
export type ITEMS_FETCH_DATA_SUCCESS = typeof ITEMS_FETCH_DATA_SUCCESS;
For the moment I am only attempting to build the typescript/redux store, but cannot seem to find any information on the nature of this issue. How would someone go about resolving this error?
Interface in typescript can't be instantiated, they can be extended though. So, if you wish to use an instance that matches the type signature of an interface, you should extend it.
You can implement the interfaces using a class
class ItemIsLoadingAction implements ItemIsLoading {
payload: boolean;
constructor(payload) {
this.payload = payload;
}
}
Then, you can use it's instance to dispatch the action like
dispatch(new ItemIsLoadingAction(false))
This can be applicable for the rest of the interfaces.

Categories

Resources