I have a Next.js application in which I use redux / redux saga.
Although I receive data from the backend (so there is a payload that I can see in the browsers network tab), it is sent to my reducer as undefined. I thought that it might be due to the payload type and set it to any. Still, it doesn't want to work.
Here is my setup (shown in simplified form):
actions.ts
export const getSomeStuff: ActionType<any> = (data: any) => ({
type: actionTypes.GET_SOME_STUFF,
payload: data,
});
export const getSomeStuffSuccess: ActionType<any> = (data: any) => ({
type: actionTypes.GET_SOME_STUFF_SUCCESS,
payload: data,
});
actionTypes.ts
export const actionTypes = {
GET_SOME_STUFF: 'GET_SOME_STUFF',
GET_SOME_STUFF_SUCCESS: 'GET_SOME_STUFF_SUCCESS',
};
reducers.ts
interface customInterface {
list: any;
}
export const initialState: customInterface = {
list: null,
};
function reducer(state = initialState, action: ReturnType<ActionType>) {
switch (action.type) {
case actionTypes.GET_SOME_STUFF_SUCCESS:
return {
list: action.payload,
};
default:
return state;
}
}
sagas.ts
function* getSomeStuffSaga(action: ReturnType<ActionType>) {
try {
const payload: any = yield call(api.getSomeStuff, action.payload);
yield put(getSomeStuffSuccess(payload));
} catch (error) {
console.error(error);
}
}
function* watchGetSomeStuffSaga() {
yield takeLatest(actionTypes.GET_SOME_STUFF, getSomeStuffSaga);
}
export default function* sagas() {
yield fork(watchGetSomeStuffSaga);
}
The api call
getSomeStuff = (data: any) => {
http.get(`my/custom/endpoint`) as Promise<any>;
};
The dispatch
dispatch(getSomeStuff('someStringParametersPassed'));
Simplified payload I get (in the browsers network tab)
{
"items": [
{
"id":"123456789",
"stuff":{
"generalStuff": {
...
},
"basicStuff": {
...
}
}
},
]
}
I guess you simply forgot to return in the api call:
getSomeStuff = (data: any) => {
return http.get(`my/custom/endpoint`) as Promise<any>;
};
:)
Related
I have a question on handling errors in createAsyncThunk with TypeScript.
I declared returned type and params type with generics. However I tried with handling erros typing I ended up just using 'any'.
Here's api/todosApi.ts...
import axios from 'axios';
export const todosApi = {
getTodosById
}
// https://jsonplaceholder.typicode.com/todos/5
function getTodosById(id: number) {
return instance.get(`/todos/${id}`);
}
// -- Axios
const instance = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com'
})
instance.interceptors.response.use(response => {
return response;
}, function (error) {
if (error.response.status === 404) {
return { status: error.response.status };
}
return Promise.reject(error.response);
});
function bearerAuth(token: string) {
return `Bearer ${token}`
}
Here's todosActions.ts
import { createAsyncThunk } from '#reduxjs/toolkit'
import { todosApi } from '../../api/todosApi'
export const fetchTodosById = createAsyncThunk<
{
userId: number;
id: number;
title: string;
completed: boolean;
},
{ id: number }
>('todos/getTodosbyId', async (data, { rejectWithValue }) => {
try {
const response = await (await todosApi.getTodosById(data.id)).data
return response
// typescript infer error type as 'unknown'.
} catch (error: any) {
return rejectWithValue(error.response.data)
}
})
And this is todosSlice.ts
import { createSlice } from '#reduxjs/toolkit'
import { fetchTodosById } from './todosActions'
interface todosState {
todos: {
userId: number;
id: number;
title: string;
completed: boolean;
} | null,
todosLoading: boolean;
todosError: any | null; // I end up with using any
}
const initialState: todosState = {
todos: null,
todosLoading: false,
todosError: null
}
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(fetchTodosById.pending, (state) => {
state.todosLoading = true
state.todosError = null
})
.addCase(fetchTodosById.fulfilled, (state, action) => {
state.todosLoading = false
state.todos = action.payload
})
.addCase(fetchTodosById.rejected, (state, action) => {
state.todosLoading = false
state.todosError = action.error
})
}
})
export default todosSlice.reducer;
In addition, it seems my code doesn't catch 4xx errors. Is it becasue I didn't throw an error in getTodosById in todosApi?
I don't have much experience with TypeScript so please bear with my ignorance.
UPDATE: I managed to handle errors not using 'any' type, but I don't know if I'm doing it right.
//todosActions..
export const fetchTodosById = createAsyncThunk<
{
userId: number;
id: number;
title: string;
completed: boolean;
},
number
>('todos/getTodosbyId', async (id, { rejectWithValue }) => {
const response = await todosApi.getTodosById(id);
if (response.status !== 200) {
return rejectWithValue(response)
}
return response.data
})
// initialState...
todosError: SerializedError | null;
This is described in the Usage with TypeScript documentation page:
const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
// Optional fields for defining thunkApi field types
rejectValue: YourAxiosErrorType
}
>('users/fetchById', async (userId, thunkApi) => {
// ...
})
I want to take an api data using typescript / react / redux_thunk and make a simple application using that data
here is my code:
type.tsx:
export interface Iproduct {
id: number;
title: string;
price: number;
description: string;
category: string;
image: string;
}
Actions.ts
import { Iproduct } from "../types/type";
export const getProduct = () => (dispatch: any) => {
axios
.get<Iproduct[]>("https://fakestoreapi.com/products")
.then((res) => dispatch({ type: "GET_PRODUCT", payload: res.data }))
.catch((err) => {
console.log(err);
});
};
Reducer :
import { Iproduct } from "../types/type";
interface Iproducs {
products: any[];
}
const INITALIZE_STATE: Iproducs = {
products: [],
};
type Action = { type: string; payload: any[] };
export const reducer = (state = INITALIZE_STATE, action: Action) => {
switch (action.type) {
case "GET_PRODUCT":
return { ...state, products: action.payload };
default:
return state;
}
};
App.tsx :
const state = useSelector((state: Iproduct[]) => state);
const dispatch = useDispatch();
const [products, setproducts] = useState<Iproduct[]>([]);
useEffect(() => {
dispatch(getProduct());
}, []);
useEffect(() => {
setproducts(state);
products.forEach((e: Iproduct) => console.log(e));
});
I want to map inside the data I received with useSelector in app.tsx file, but I'm getting an error
"TypeError: products.map is not a function"
I will be glad if you tell me where I even did it, I cannot solve such mistakes because I have just learned the typescript.
note: I use redux-thunk in the project
Try to replace below code in reducer
export const reducer = (state = INITALIZE_STATE, action: Action) => {
switch (action.type) {
case "GET_PRODUCT":
return { ...state, products: [action.payload] };
default:
return state;
}
};
You probably sent a different code but I think I understand. When the component is first loaded, it comes undefined because you did not make any requests and the products data did not change. The simplest way to solve this is by using
state => (state.products || []).map(value => ...)
I am using Redux for state management and saga as a middleware. For some reason my app is in some infinite loop state of calling API endpoint.
This is my actions:
export const GET_USERS = "GET_USERS";
export const getUsers = () => ({
type: GET_USERS,
});
export const GET_USERS_SUCCESS = `${GET_USERS}_SUCCESS`;
export const getUsersSuccess = (data) => ({
type: GET_USERS_SUCCESS,
payload: data,
});
export const GET_USERS_FAIL = `${GET_USERS}_FAIL`;
export const getUsersFail = (error) => ({
type: GET_USERS_FAIL,
payload: error,
});
This is saga:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.getUsers, getUsers$)]);
}
This is a reducer:
export default (state = initialState(), action) => {
const { type, payload } = action;
switch (type) {
case actions.GET_USERS:
return {
...state,
users: {
...state.users,
inProgress: true,
},
};
case actions.GET_USERS_SUCCESS:
return {
...state,
users: {
inProgress: false,
data: payload,
},
};
case actions.GET_USERS_FAIL:
return {
...state,
users: {
...state.users,
inProgress: false,
error: payload,
},
};
default:
return state;
}
};
And this is a component connected with redux:
const Home = (props) => {
useEffect(() => {
props.getUsers();
console.log('props', props.data);
}, []);
return(
<h1>Title</h1>
);
}
const mapStateToProps = ({
users: {
users: {
data
}
}
}) => ({data})
export default connect(mapStateToProps, {getUsers})(Home);
Why is this happening?
This is due to the fact that you misused the sagas in your example. As with any other effect creator as the first parameter must pass a pattern, which can be read in more detail in the documentation. The first parameter can also be passed a function, but in a slightly different way. View documentation (block take(pattern)).
In your case, you are passing a function there that will return an object
{
type: 'SOME_TYPE',
payload: 'some_payload',
}
Because of this, your worker will react to ALL events that you dispatch.
As a result, you receive data from the server, dispatch a new action to save data from the store. And besides the reducer, your getUsers saga will be called for this action too. And so on ad infinitum.
Solution
To solve this problem, just use the string constant actions.GET_USERS that you defined in your actions.
And your sagas will look like this:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.GET_USERS, getUsers$)]);
}
This should fix your problem.
Following problem: I've tried to write a generic typescript reducer the last few hours, and I feel like it's working fairly well already, but there's just one problem - They way I wired it with my store seems to have problems. It seems like the store does not properly update, as a component I tried to hook up with the data from the reducer does not receive new props.
This is the generic reducer. It's not fully complete yet, but the add functionality should work at least.
// Framework
import * as Redux from "redux";
// Functionality
import { CouldBeArray } from "data/commonTypes";
import { ensureArray } from "helper/arrayUtils";
type ReducerParams<T> = {
actionIdentifier: string;
key: keyof T;
}
export type ReducerState<T> = {
data: Array<T>;
}
type ReducerAction<T> = Redux.Action & {
payload: CouldBeArray<T>;
}
type Reducer<T> = {
add: (data: T) => ReducerAction<T>;
update: (data: T) => ReducerAction<T>;
delete: (data: T) => ReducerAction<T>;
replace: (data: T) => ReducerAction<T>;
reducer: Redux.Reducer<ReducerState<T>, ReducerAction<T>>;
}
export const createReducer = <T>(params: ReducerParams<T>): Reducer<T> => {
const ADD_IDENTIFIER = `${params.actionIdentifier}_ADD`;
const UPDATE_IDENTIFIER = `${params.actionIdentifier}_UPDATE`;
const DELETE_IDENTIFIER = `${params.actionIdentifier}_DELETE`;
const REPLACE_IDENTIFIER = `${params.actionIdentifier}_REPLACE`;
const initialState: ReducerState<T> = {
data: []
};
const reducer = (state = initialState, action: ReducerAction<T>): ReducerState<T> => {
switch (action.type) {
case ADD_IDENTIFIER:
const newState = { ...state };
const newData = [ ...newState.data ];
const payloadAsArray = ensureArray(action.payload);
payloadAsArray.forEach(x => newData.push(x));
newState.data = newData;
return newState;
case UPDATE_IDENTIFIER:
return {
...state,
};
case DELETE_IDENTIFIER:
return {
...state,
};
case REPLACE_IDENTIFIER:
return {
...state,
};
default:
return initialState;
}
}
const addAction = (data: T): ReducerAction<T> => {
return {
type: ADD_IDENTIFIER,
payload: data,
}
};
const updateAction = (data: T): ReducerAction<T> => {
return {
type: UPDATE_IDENTIFIER,
payload: data,
}
};
const deleteAction = (data: T): ReducerAction<T> => {
return {
type: DELETE_IDENTIFIER,
payload: data,
}
};
const replaceAction = (data: T): ReducerAction<T> => {
return {
type: REPLACE_IDENTIFIER,
payload: data,
}
};
return {
add: addAction,
update: updateAction,
delete: deleteAction,
replace: replaceAction,
reducer: reducer,
}
}
Next off, my store:
// Framework
import * as redux from "redux";
// Functionality
import { ReducerState } from "modules/common/Reducer/CrudReducer";
import { reducer as friendsReducer } from "modules/Friends/Reducer/FriendsReducer";
import { Friend } from "modules/Friends/types";
export type ReduxStore = {
friendsReducer: ReducerState<Friend>;
}
export const store: ReduxStore = redux.createStore(
redux.combineReducers({
friendsReducer: friendsReducer.reducer,
})
);
export default store;
and last but not least, the consuming component:
type Props = {
friends: Array<Friend>
}
export const FriendsList: React.FC<Props> = ({ friends }) => {
return (
<Flex className={"FriendsList"}>
Friends
</Flex>
);
}
const mapStateToProps = (store: ReduxStore): Props => {
return {
friends: store.friendsReducer.data,
};
}
export default connect(mapStateToProps)(FriendsList);
The problem usually unfolds in the following order:
Data is properly fetched from network
Update the store via store.dispatch(friendsReducer.add(payload))
With the debugger, I did step through the genericreducer and saw that the new state properly contains the new data.
This is where the problem occurs - The freshly generated state by the reducer is not transferred to my Friendslist component. It will only receive props once, while the data in there is still empty.
Where did I go wrong?
EDIT: By demand, the code for the friendsReducer:
import { createReducer } from "modules/common/Reducer/CrudReducer";
import { Friend } from "modules/friends/types";
export const reducer = createReducer<Friend>({
actionIdentifier: "FRIENDS",
key: "id"
});
export default reducer;
and for the dispatch:
const friendsResponse = await friendsCommunication.getFriends();
if (friendsResponse.success){
this.dispatch(friendsReducer.add(friendsResponse.payload));
}
...
protected dispatch(dispatchAction: Action){
store.dispatch(dispatchAction);
}
Found the problem - My generic reducer returned the following as default:
default:
return initialState;
while it should return state.
Otherwise it just did reset the state of all iterated reducers for every action.
I am new with reac-redux, I am trying to get collection from Firestore but now when firebase returns the data and I try to map the info to storage through redux-observable I get an error "Actions must be plain objects. Use custom middleware for async actions." I guess it must be about the epic configuration, then I leave the code
Epic
import { getFirestore } from "redux-firestore";
import {
GET_DOCUMENTS,
GET_COLLECTIONS_BY_DOCUMENT,
setStatus,
getDocumentsSuccess,
getDocumentsFailed
} from "../actions/dataActions";
import { switchMap } from "rxjs/operators";
import { ofType } from "redux-observable";
import { concat, of } from "rxjs";
export default function dataEpics(action$) {
const getFS = getFirestore();
return action$.pipe(
ofType(GET_DOCUMENTS, GET_COLLECTIONS_BY_DOCUMENT),
switchMap(action => {
if (action.type === GET_DOCUMENTS) {
return concat(
of(setStatus("pending")),
getFS
.collection("en")
.get()
.then(querySnapshot => {
let listDocumentIds = [];
querySnapshot.forEach(doc => {
listDocumentIds.push(doc.id);
getDocumentsSuccess(listDocumentIds);
});
})
.catch(err => of(getDocumentsFailed(err)))
);
}
})
);
}
Action
export const SET_STATUS = "SET_STATUS";
export const GET_DOCUMENTS = "GET_DOCUMENTS";
export const GET_DOCUMENTS_SUCCESS = "GET_COLLECTIONS_SUCCESS";
export const GET_DOCUMENTS_FAILED = "GET_COLLECTIONS_FAILED";
export function setStatus(status) {
return {
type: SET_STATUS,
payload: status
};
}
export function getDocumentsSuccess(documents) {
return {
type: GET_DOCUMENTS_SUCCESS,
payload: documents
};
}
reducer
import {
GET_DOCUMENTS_SUCCESS,
GET_DOCUMENTS_FAILED,
SET_STATUS
} from "../actions/dataActions";
const initState = {
status: "idle", // "idle" | "logout" | "pending" | "login" | "success" | "failure";
documents: [],
collections: []
};
const dataReducers = (state = initState, action) => {
switch (action.type) {
case SET_STATUS: {
return {
...state,
status: action.payload
};
}
case GET_DOCUMENTS_SUCCESS: {
return {
...state,
status: "success",
documents: action.payload
};
}
default:
return state;
}
};
export default dataReducers;
I think the error is in the epic, I have more code in a similar way
Thanks for help me.
I found the solution, the error was in the epic, I was trying to call the action inside querySnapshot, this is no possible, then I move the getDocumentsSuccess after
getFS
.collection(action.payload.language + "_" + "MachinesAndEquipment")
.get()
.then(querySnapshot => {
let listDocumentIds = [];
querySnapshot.forEach(doc => {
listDocumentIds.push(doc.id);
});
getDocumentsSuccess(listDocumentIds);