How To Use This.props.action Function In Typescript?
Solution 1:
If the function returns void, it means you're returning nothing, you can't get the property then
. You need to return a Promise
interface IProps {
auth: {
payload?: {
success? : boolean,
status?: number,
message?: string,
user?: {
id: string,
email: string
},
access_token?: string,
expires_in?: string
}
};
login: () =>Promise<void>;
}
Solution 2:
Since you're calling this.props.login(...).then(...)
, I assume login
should return a Promise. You just need to type it as such in your IProps
interface. Additionally, it looks like it's expected to take an argument containing email
and password
, so you should type that as well.
interface IProps {
login: (credentials: { email: string; password: string; }) =>Promise<void>;
}
Solution 3:
Your setup is illogical because you are using the type IProps
as the type for both:
- The props of the
Login
component - The root state of your Redux store passed as an argument to
mapStateToProps
You are expecting your component to take a prop userLogin
which is a function
. This function is not a property of your Redux store state and therefore it cannot possibly be true that the Redux state and the component props share the same type.
If you fix the IProps
to accurately represent the component props then you will have problems with mapStateToProps
and the react-redux connect
HOC because they are using (state: IProps)
. You have already discovered that in your replies to @Kakiz's answer.
You should not need to determine your redux state type in this component's file because that type is global. The react-redux docs recommend that you infer the RootState
type from your store
variable, like this:
exporttypeRootState = ReturnType<typeof store.getState>
You put that line in the file where you create the store import the RootState
type wherever you need it, like in mapStateToProps
.
Their excellent guide on usage with TypeScript includes a whole section on Typing the connect
higher order component, though they recommend using function components and hooks instead as these are much easier to type properly.
Don't be afraid to export
types and reuse them. The ILoginObject
defined in your actions file is identical to the IState
defined in your component. Your IProps
and ILoginDispatch
have major duplications as well.
I often find it easier to write simple event handlers inline because then you avoid needing to type the e
argument.
You need the TypeScript types to know that you have redux-thunk middleware installed in order to for it to know that dispatching an action will return a Promise
, since this is not the default behavior.
The userLogin
thunk itself can be simplified with the createAsyncThunk function from the official Redux Toolkit. This will also handle errors in the axios.post
request itself, which you aren't currently catching. It's also a good candidate for RTK Query, which just got integrated into Redux Toolkit.
Here's a better setup for your component, which is completely type-safe although there are no types anywhere. Everything is inferred.
importReact, { useState } from"react";
import { unwrapResult } from"@reduxjs/toolkit";
import { userLogin } from"../store/slice";
import { useSelector, useDispatch } from"../store";
// no props needed anymoreexportdefaultfunctionLogin() {
// not sure where you actually use thisconst auth = useSelector((state) => state.auth);
const dispatch = useDispatch();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
constonSubmit = () => {
dispatch(userLogin({ email, password }))
.then(unwrapResult) // explained here: https://redux-toolkit.js.org/api/createAsyncThunk#unwrapping-result-actions
.then((data) =>alert(`logged in user ${data.user}`))
.catch((error) =>alert(`login error: ${error.message}`));
};
return (
<React.Fragment><div><label>
Email
<inputtype="text"name="email"value={email}onChange={(e) => setEmail(e.target.value)}
/>
</label></div><div><label>
Password
<inputtype="password"name="password"value={password}onChange={(e) => setPassword(e.target.value)}
/>
</label></div><div><buttononClick={onSubmit}>Submit</button></div></React.Fragment>
);
}
That is based on the following setup of store:
import { AnyAction, configureStore } from"@reduxjs/toolkit";
import {
TypedUseSelectorHook,
useDispatch as _useDispatch,
useSelector as _useSelector
} from"react-redux";
import { logger } from"./middleware";
import auth from"./slice";
const store = configureStore({
reducer: {
auth
}
});
exporttypeRootState = ReturnType<typeof store.getState>;
exporttypeAppDispatch = typeof store.dispatch;
exporttypeAction = AnyAction;
exportconstuseDispatch: () =>AppDispatch = _useDispatch;
exportconstuseSelector: TypedUseSelectorHook<RootState> = _useSelector;
exportdefault store;
And reducer/actions:
import { createSlice, createAsyncThunk } from"@reduxjs/toolkit";
import axios from"axios";
exportinterfaceAuthData {
success?: boolean;
status?: number;
message?: string;
user?: {
id: string;
email: string;
};
access_token?: string;
expires_in?: string;
}
exportinterface ILoginObject {
email: string;
password: string;
}
exportconst userLogin = createAsyncThunk(
"auth/Login",
async (loginObject: ILoginObject, { rejectWithValue }) => {
const response = await axios.post<AuthData>(`/api/login`, loginObject);
if (response.data && response.data.access_token) {
// returned value is the success payloadreturn response.data;
} else {
// alternatively, can throw an ErrorreturnrejectWithValue(response.data);
}
}
);
constinitialState: AuthData = { success: false };
const authSlice = createSlice({
initialState,
name: "auth",
// reducers creates both actions and reducer casesreducers: {
logout: (state, action) => {
// clears the state and replaces with the initial statereturn initialState;
}
},
// extraReducers adds reducer cases for existing actionsextraReducers: (builder) =>
builder
.addCase(userLogin.fulfilled, (state, action) => {
// TODO - this replaces the entire state, probably not what you actually wantreturn action.payload;
})
.addCase(userLogin.rejected, (state, action) => {
// TODO
})
.addCase(userLogin.pending, (state, action) => {
// TODO - what is the correct number?
state.status = 0;
})
});
exportconst { logout } = authSlice.actions;
exportdefault authSlice.reducer;
You'll want to fill in some blanks in the reducer.
Post a Comment for "How To Use This.props.action Function In Typescript?"