I'm new to redux and I find it hard to find a good guide that both uses async calls and typescript. I've tried to figure it out myself but I'm a little bit stuck. If someone could take a look at my code and maybe give me feedback and/or suggestions as to how I can solve this I would be very grateful!
// types.ts file
export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
^ Here I define the constants for consistency.
// userActions.ts
import { FETCH_USERS_FAILURE, FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS } from './types';
import { Dispatch } from 'redux';
import axios, { AxiosResponse } from 'axios';
export interface UserProps {
id: number
name: string
email: string
}
export const fetchUsersRequest = () => {
return {
type: FETCH_USERS_REQUEST,
};
};
export const fetchUsersSuccess = (users: Array<UserProps>) => {
return {
type: FETCH_USERS_SUCCESS,
users: users,
};
};
export const fetchUsersFailure = (error: string) => {
return {
type: FETCH_USERS_FAILURE,
error: error,
};
};
export const fetchUsers = () => {
return (dispatch: Dispatch): Promise<unknown> => {
dispatch(fetchUsersRequest());
return axios.get('https://jsonplaceholder.typicode.com/users')
.then((response: AxiosResponse<UserProps[]>) => {
dispatch(fetchUsersSuccess(response.data));
return response.data;
})
.catch((error: string) => {
dispatch(fetchUsersFailure(error));
});
};
};
userReducer
import { UserProps } from '../actions/userActions';
import { FETCH_USERS_FAILURE, FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS } from '../actions/types';
interface Action {
type: string
payload: Array<UserProps> | string,
}
export interface initialStateProps {
loading: boolean,
users: Array<UserProps>
error: string
}
const initialState: initialStateProps = {
loading: false,
users: [],
error: '',
};
export const userReducer = (state = initialState, action: Action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return {
...state,
loading: true,
};
case FETCH_USERS_SUCCESS:
return {
loading: false,
users: action.payload,
};
case FETCH_USERS_FAILURE:
return {
loading: false,
users: [],
error: action.payload,
};
default: {
return state;
}
}
};
export const getUsers = (state: initialStateProps) => state.users;
export const getUsersRequest = (state: initialStateProps) => state.loading;
export const getUsersFailure = (state: initialStateProps) => state.error;
And then for my project I use connected-router to help with keeping track of the routes.
import { combineReducers } from 'redux';
import { History } from 'history';
import { connectRouter } from 'connected-react-router';
import { userReducer } from './userReducers';
export const createRootReducer = (history: History) => combineReducers({
router: connectRouter(history),
userReducer,
});
The store:
import { createBrowserHistory } from 'history';
import { applyMiddleware, compose, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger';
import { routerMiddleware } from 'connected-react-router';
import { createRootReducer } from './reducers';
export const history = createBrowserHistory();
const loggerMiddleware = createLogger();
export const configureStore = (preloadedState?: any) => {
const composeEnhancer: typeof compose = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
return createStore(
createRootReducer(history),
preloadedState,
composeEnhancer(
applyMiddleware(
routerMiddleware(history),
thunkMiddleware,
loggerMiddleware,
),
),
);
};
And in the application I use this:
const mapStateToProps = (state: initialStateProps) => ({
error: getUsersFailure(state),
users: getUsers(state),
loading: getUsersRequest(state)
});
const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
fetchUsers: fetchUsers
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TopbarNav);
But when I console.log(props); I get:
users: undefined
loading: undefined
error: undefined
EDIT:
Here I run the fetchUsers:
const TopbarNav: React.FC = (props: any) => {
const [loading, setLoading] = React.useState(true);
const classes = useStyles();
useEffect(() => {
props.fetchUsers();
});
const handler = () => {
console.log({props});
};
};
I left the render method out in this question.
EDIT 2 updated useEffect method:
const [users, setUsers] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const classes = useStyles();
useEffect(() => {
setUsers(props.fetchUsers());
}, [users, props, loading]);
const handler = () => {
console.log(props.loading);
};
By using combineReducers that way I think your state will look like this
{
router: ...,
userReducer: ...,
}
so you could fix it by modifying the selectors to look for the correct props
state.userReducer.users
state.userReducer.loading
...
Related
I have an error : Uncaught TypeError: Cannot destructure property 'users' of '(0 , _redux_hooks__WEBPACK_IMPORTED_MODULE_11__.useAppSelector)(...)' as it is undefined. which happens in app component on line const { users } = useAppSelector(state => state.users);
When hovering on { user } , it shows its type User[], which it inferes from slice file correctly. All parentesis also seems to be in place. Here is store File.
import { configureStore } from '#reduxjs/toolkit';
import usersReducer from './features/users';
const store = configureStore({
reducer: {
users: usersReducer,
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Here is hooks file where I define types of useDispatch() and useSelector()
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';
import { AppDispatch, RootState } from './store';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppDispatch: () => AppDispatch = useDispatch;
Here is users Slice file
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
import { User } from '../../types/User';
const GET_URL = '***********************************';
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await axios.get(GET_URL);
return response.data;
});
type InitialState = {
users: User[];
status:'idle' | 'failed' | 'pending' | 'fullfilled';
error: string | null;
};
const initialState: InitialState = {
users: [],
status: 'idle',
error: 'error',
};
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'pending';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'fullfilled';
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state) => {
state.status = 'failed';
state.error = 'error';
});
},
});
export default usersSlice.reducer;
and here is app file
import { useAppDispatch, useAppSelector } from './redux/hooks';
import { fetchUsers } from './redux/features/users';
export const App: React.FC = () => {
const dispatch = useAppDispatch();
const { users } = useAppSelector(state => state.users); // here shows an error
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
...
May be somebody encountered similar error before. Will be glad for any suggestion.
You're trying to access state.users.users here by destructuring the result of your useAppSelector
const { users } = useAppSelector(state => state.users);
If you want to access state.users you should change your declaration to match your needs
const users = useAppSelector(state => state.users);
So hi guys, this is my first project where I try to use react-typescript with redux-toolkit and am getting an error:
Argument of type 'AppThunk' is not assignable to parameter of type 'AnyAction'. Property 'type' is missing in type 'AppThunk' but required in type 'AnyAction'.
Basically I just want to get data from cats public API, but it doesn't seem to work and I would really appriciate some help.
src/app/rootReducer.tsx
import { combineReducers } from "#reduxjs/toolkit";
import catsReducer from "../features/cat/catsSlice";
const rootReducer = combineReducers({
cats: catsReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
src/app/store.tsx
import { configureStore, Action } from "#reduxjs/toolkit";
import { useDispatch } from "react-redux";
import { ThunkAction } from "redux-thunk";
import rootReducer, { RootState } from "./rootReducer";
const store = configureStore({
reducer: rootReducer,
});
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch();
export type AppThunk = ThunkAction<void, RootState, unknown, Action>;
export default store;
src/features/cat/catsSlice.tsx
import axios from "axios";
import { createSlice, PayloadAction } from "#reduxjs/toolkit";
import { AppThunk } from "../../app/store";
import { RootState } from "../../app/rootReducer";
export interface CatsState {
cats: Array<Cat>;
isLoading: boolean;
error: CatsError;
}
export interface Cat {
id: string;
categories?: Array<any>;
breeds: Array<any>;
url: string;
width: number;
height: number;
}
export interface CatsError {
message: string;
}
export const initialState: CatsState = {
cats: [],
isLoading: false,
error: { message: "" },
};
export const catsSlice = createSlice({
name: "cat",
initialState,
reducers: {
setLoading: (state, { payload }: PayloadAction<boolean>) => {
state.isLoading = payload;
},
setCatsSuccess: (state, { payload }: PayloadAction<Array<Cat>>) => {
state.cats = payload;
},
setCatsFailed: (state, { payload }: PayloadAction<CatsError>) => {
state.error = payload;
},
},
});
export const { setCatsSuccess, setCatsFailed, setLoading } = catsSlice.actions;
export const getCats = (): AppThunk => async (dispatch) => {
try {
dispatch(setLoading(true));
const catsResponse = await axios.get(
"https://api.thecatapi.com/v1/images/search?limit=8&size=small&order=random"
);
dispatch(setCatsSuccess(catsResponse.data));
} catch (error) {
dispatch(setCatsFailed({ message: "An Error occurred" }));
} finally {
dispatch(setLoading(false));
}
};
export const catsSelector = (state: RootState) => state.cats;
export default catsSlice.reducer;
src/components/game/GameView.tsx
import React, { useEffect } from "react";
import { getCats, catsSelector, Cat } from "../../features/cat/catsSlice";
import { useSelector, useDispatch } from "react-redux";
const GameView: React.FC = (): JSX.Element => {
const dispatch = useDispatch();
const { cats, isLoading, error } = useSelector(catsSelector);
useEffect(() => {
dispatch(getCats());
}, [dispatch]);
return <>GameView</>;
};
export default GameView;
So as you can see I call the getCats() function in the useEffect hook, but it gets underlined with an error.
Any help would be appriciated! Thanks in advance!
in 'store.tsx' useDispatch() doesn't take any
export const useAppDispatch = () => useDispatch();
argument but you passed one in 'GameView.tsx':
dispatch(getCats());
So you can fix this by adding an optional argument to useDispatch in 'store.tsx', so instead it will be :
export const useAppDispatch: (arg?:unknown) => AppDispatch = useDispatch
I'm trying to use useSelector hook from react-redux with a selector from redux-toolkit slice in my custom hook and i'm getting the following error.
TypeError: Cannot read property 'isLoggedIn' of undefined
store.ts:
import { configureStore, ThunkAction, Action } from '#reduxjs/toolkit';
import authReducer from './authSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
authSlice.ts:
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import authService from '../services/authService';
import { UserWithDetails } from '../types/User';
import { AppThunk, RootState } from './store';
import toast from 'react-hot-toast';
interface AuthState {
isLoggedIn: boolean;
user: UserWithDetails | null;
}
const initialState: AuthState = {
isLoggedIn: localStorage.getItem('accessToken') ? true : false,
user: localStorage.getItem('authenticatedUser')
? JSON.parse(localStorage.getItem('authenticatedUser') as string)
: null,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
login: (state, action: PayloadAction<UserWithDetails>) => {
state.user = action.payload;
state.isLoggedIn = true;
},
logout: (state) => {
state.user = null;
state.isLoggedIn = false;
},
},
});
const { login, logout } = authSlice.actions;
export const loginUser = (accessToken: string): AppThunk => async (
dispatch
) => {
localStorage.setItem('accessToken', accessToken);
const user = await authService.me();
localStorage.setItem('authenticatedUser', JSON.stringify(user));
dispatch(login(user));
toast.success('You have been logged in.');
};
export const logoutUser = (): AppThunk => (dispatch) => {
localStorage.removeItem('accessToken');
localStorage.removeItem('authenticatedUser');
dispatch(logout());
toast.success('You have been logged out.');
};
export const selectIsLoggedIn = (state: RootState) => state.auth.isLoggedIn;
export const selectUser = (state: RootState) => state.auth.user;
export default authSlice.reducer;
useAuth.ts
import { useSelector } from 'react-redux';
import { selectIsLoggedIn, selectUser } from '../store/authSlice';
const useAuth = () => {
const isLoggedIn = useSelector(selectIsLoggedIn);
const user = useSelector(selectUser);
return {
isLoggedIn,
user,
};
};
export default useAuth;
When i'm using inline selector it works fine:
const isLoggedIn = useSelector((state: RootState) => state.auth.isLoggedIn);
I don't understand why a selector from a slice doesn't work. Please help. Thank you.
Error
I created a simple Redux app with following parts
import { useDispatch, useSelector } from "react-redux";
import { buyCakes } from "../redux/cake/cake.actions";
import { ICakeState } from "../redux/cake/cake.reducer";
import { changeEventType, formEventType } from "../models/events.model";
const CakeContainer: React.FC = () => {
const [buyCakeAmount, setBuyCakeAmount] = useState(1);
const numberOfCakes = useSelector<ICakeState, ICakeState["numberOfCakes"]>(
(state) => {
console.log(state);
return state.numberOfCakes;
}
);
const dispatch = useDispatch();
const changeHandler = (event: changeEventType) => {
setBuyCakeAmount(parseInt(event.target.value));
};
const submitHandler = (event: formEventType) => {
event.preventDefault();
dispatch(buyCakes(buyCakeAmount));
setBuyCakeAmount(1);
};
return (
<>
<h2>Number of cakes left in the shop: {numberOfCakes}</h2>
<form onSubmit={submitHandler}>
<input type="number" value={buyCakeAmount} onChange={changeHandler} />
<button type="submit">Buy Cakes</button>
</form>
</>
);
};
export default CakeContainer;
Reducer
import { BUY_CAKE, BUY_CAKES } from "./cake.types";
import { CakeActionTypes } from "./cake.types";
export interface ICakeState {
numberOfCakes: number;
}
const INITIAL_STATE: ICakeState = {
numberOfCakes: 10,
};
const cakeReducer = (
state: ICakeState = INITIAL_STATE,
action: CakeActionTypes
) => {
switch (action.type) {
case BUY_CAKE:
return {
...state,
numberOfCakes: state.numberOfCakes - 1,
};
case BUY_CAKES:
return {
...state,
numberOfCakes: state.numberOfCakes - action.payload,
};
default:
return state;
}
};
export default cakeReducer;
Actions
import { BUY_CAKE, BUY_CAKES } from "./cake.types";
import { CakeActionTypes } from "./cake.types";
import { numberOfCakes } from "../../models/cake.model";
export const buyCake = (): CakeActionTypes => ({
type: BUY_CAKE,
});
export const buyCakes = (numberOfCakes: numberOfCakes): CakeActionTypes => ({
type: BUY_CAKES,
payload: numberOfCakes,
});
Types
import { numberOfCakes } from "../../models/cake.model";
export const BUY_CAKE = "BUY_CAKE";
export const BUY_CAKES = "BUY_CAKES";
interface BuyCakeAction {
type: typeof BUY_CAKE;
}
interface BuyCakesAction {
type: typeof BUY_CAKES;
payload: numberOfCakes;
}
export type CakeActionTypes = BuyCakeAction | BuyCakesAction;
Store
import { combineReducers, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import cakeReducer from "./cake/cake.reducer";
export default createStore(
combineReducers([cakeReducer]),
composeWithDevTools()
);
The log in the useSelector logs this object
{"0":{"numberOfCakes":10}}
Should it not just return the state without putting it in an object with the key '0'?
Also if I just return the state like this
state['0'].numberOfCakes
I get a Typescript error
Can someone please explain to me why this is happening and how to fix it, thanks.
First issue: useSelector is going to give you back your root state, so you are passing the wrong types to your generic here:
const numberOfCakes = useSelector<ICakeState, ICakeState["numberOfCakes"]>(
(state) => {
console.log(state);
return state.numberOfCakes;
}
);
Second issue is that combineReducers takes an object with the different reducers, including their names, not an array, so you need to change it to:
export default createStore(
combineReducers({cakeReducer}),
composeWithDevTools()
);
export type RootState = ReturnType<typeof rootReducer>
and then change:
const numberOfCakes = useSelector<RootState>(
(state) => {
console.log(state);
return state.cakeReducer.numberOfCakes;
}
);
The problem is:
I'm trying to use redux-saga in my react app, but i still has this error: Actions must be plain objects. Use custom middleware for async actions. Code it seems correct but no idea why gives that error. I'll be glad for all the help. I'm fighting with it for about two days and still doesn't have a solution. I tried to look up, but I still have this error.
action...
import { GET_DISTRICTS} from '../../constants';
const getAdres = async (url) => {
let response = await fetch(url);
let data = await response.json();
let list = [];
data.AdresList.Adresler.Adres.forEach((item) => {
console.info(item);
list.push({
label: item.ADI,
value: item.ID
});
});
return list;
};
export const actions = {
handleGetDistrictsData: async () => {
let districts = await getAdres(`url is here`);
return {
type: GET_DISTRICTS,
payload: districts
};
},
reducer...
import { GET_DISTRICTS } from '../../constants';
export const initialState = {
districts: [],
quarters: [],
streets: [],
doors: [],
districtSelected: false,
districtSelectedID: null,
quarterSelected: false,
quarterSelectedID: null,
streetSelected: false,
streetSelectedID: null,
doorSelected: false,
doorSelectedID: null
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_DISTRICTS:
return {
...state,
districts: action.payload
};
default:
return state;
}
};
component...
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actions as addressActions } from '../../../../redux/actions/address';
import Select from 'react-select';
const Districts = (props) => {
let [ fetchedData, setFetchedData ] = useState(false);
useEffect(() => {
props.handleGetDistrictsData();
setFetchedData(true);
});
return (
<React.Fragment>
<Select
name='adresSelect'
options={props.address.districts}
onChange={props.handleDistrictChange}
placeholder='Please Select'
/>
</React.Fragment>
);
};
const mapStateToProps = (state) => ({
address: state.address
});
const mapDispatchToProps = function(dispatch) {
return bindActionCreators({ ...addressActions }, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(Districts);
-------------
import React from 'react';
import Districts from './Districts';
const AddressSearchWidget = (props) => {
return (
<React.Fragment>
<Districts />
</React.Fragment>
);
};
export default AddressSearchWidget
store...
import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas/index';
import * as reducers from './';
export function initStore() {
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers(reducers);
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, composeEnhancer(applyMiddleware(sagaMiddleware)));
// Run sagas
sagaMiddleware.run(rootSaga);
return store;
}
handleGetDistrictsData returns a promise (all async functions return promises). You cannot dispatch a promise in plain redux saga, and redux-saga does not change this. Instead, dispatch a normal action, and have that action run a saga. The saga can then do async things, and when it's done dispatch another action. The reducer listens only for that second action.
// Actions:
export const getDistrictsData = () => ({
type: GET_DISTRICTS,
})
export const districtsDataSuccess = (districts) => ({
type: DISTRICTS_DATA_SUCCESS,
payload: districts
})
// Sagas:
export function* watchGetDistricts () {
takeEvery(GET_DISTRICTS, getDistricts);
}
function* getDistricts() {
let response = yield fetch(url);
let data = yield response.json();
let list = [];
data.AdresList.Adresler.Adres.forEach((item) => {
console.info(item);
list.push({
label: item.ADI,
value: item.ID
});
});
yield put(districtsDataSuccess(list));
}
// reducer:
export default (state = initialState, action) => {
switch (action.type) {
case DISTRICTS_DATA_SUCCESS:
return {
...state,
districts: action.payload
};
default:
return state;
}
};