I'm wrapping my head around this and cannot figure out why it's not working.
I have a simple action:
export const GET_EXPENSE_LIST = "GET_EXPENSE_LIST"
export const getExpenseList = () => {
return async dispatch => {
console.log('Action called')
dispatch({ type: GET_EXPENSE_LIST, expenseList: []})
}
}
And a reducer:
import { GET_EXPENSE_LIST } from "../actions/expenses"
const initialState = {
expenseList = []
}
export default (state = initialState, action) => {
console.log("reducer called")
}
I'm calling the action from a component like so (if it matters):
useEffect(() => {
dispatch(expensesActions.getExpenseList())
}, [dispatch]);
In my console I do see the "action called" but I don't see the "reducer called". Why the reducer is not being called?
Did you add thunk middleware to the config of redux? In you action you use redux-thunk
Related
I am using redux toolkit with thunk to receive data from api.
I need to fetch data from 2 apis in consecutive order using data I got from the first api call as a argument of second api call (search1 first, then search2)
In order to do that, I need to wait for the first dispatch to fully complete its job from calling getSearch1 to updating the state.
Please help!
// store
import { configureStore } from "#reduxjs/toolkit";
import searchReducer from "./slice/searchSlice";
export const store = configureStore({
reducer: {
search: searchReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
// slice
export const getSearch1 = createAsyncThunk(
"search/getSearch1",
async (args: string[]) => {
const result = await ApiUtil.search1(args);
return result;
}
);
export const getSearch2 = createAsyncThunk(
"search/getSearch2",
async (ids: string[]) => {
const result = await ApiUtil.search2(ids);
return result;
}
);
export const searchSlice = createSlice({
name: "search",
initialState,
reducers: {...},
extraReducers: (builder) => {
builder
.addCase(getSearch1.fulfilled, (state, action) => {
state.search1 = action.payload;
})
.addCase(getSearch2.fulfilled, (state, action) => {
state.search2 = action.payload;
});
},
});
// home page
import {
...
getSearch1,
getSearch2,
} from "../../redux/slice/searchSlice";
const handleSearch = () => {
dispatch(getSearch1(args));
const ids = search1?.map((item) => item.id.toString());
dispatch(getSearch2(ids ?? []));
history.push(ROUTES.RESULT_PAGE, search1);
};
You can use .unwrap() method to achieve that,see the documentation
:
try {
const { data } = await dispatch(getSearch1(args)).unwrap()
await dispatch(getSearch2(data ?? []));
// handle result here
} catch (rejectedValueOrSerializedError) {
// handle error here
}
I solved it just as slideshowp2's shared link.
useEffect(() => {
getResult();
}, [dispatch]); // listen for dispatch(), should run getResult() twice
const getResult = async () => {
let action;
if (!search1) { // skip below when called twice
action = await dispatch(getSearch1(args));
}
if (isFulfilled(action)) {
const id = action.payload.map((item) => item.id.toString());
dispatch(getSearch2(id ?? []));
}
};
I have a tree structure which is loading children on demand, this is my reducer. The problem I have is that when I want to call my thunk action from toggleExpandedProp I get exception (see bellow). What should I do?
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import { useDispatch } from 'react-redux';
import axios from 'axios';
const dispatch = useDispatch()
export const getRoot = createAsyncThunk('data/nodes/getRoot', async () => {
const response = await axios.get('http://localhost:5000/api/nodes/root');
const data = await response.data;
return data;
});
export const getChildren = createAsyncThunk('data/nodes/getRoot', async params => {
const response = await axios.get('http://localhost:5000/api/nodes/' + params.id + '/children');
const data = await response.data;
return data;
});
const initialState = {
data: [],
loading: 'idle'
};
// Then, handle actions in your reducers:
const nodesSlice = createSlice({
name: 'nodes',
initialState,
reducers: {
toggleExpandedProp: (state, action) => {
state.data.forEach(element => {
if(element.id === action.payload.id) {
element.expanded = !element.expanded;
dispatch(getChildren(element));
}
});
}
},
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[getRoot.fulfilled]: (state, action) => {
state.data = action.payload;
},
[getChildren.fulfilled]: (state, action) => {
state.data.push(action.payload);
}
}
})
export const { toggleExpandedProp } = nodesSlice.actions;
export default nodesSlice.reducer;
Exception has occurred.
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
const dispatch = useDispatch()
You can only use useDispatch inside of a function component or inside another hook. You cannot use it at the top-level of a file like this.
You should not call dispatch from inside a reducer. But it's ok to dispatch multiple actions from a thunk. So you can turn toggleExpandedProp into a thunk action.
You probably need to rethink some of this logic. Does it really make sense to fetch children from an API when expanding a node and then fetch them again when collapsing it?
export const toggleExpandedProp = createAsyncThunk(
"data/nodes/toggleExpandedProp",
async (params, { dispatch }) => {
dispatch(getChildren(params));
}
);
This is kind of a useless thunk since we don't actually return anything. Can you combine it with the getChildren action, or do you need to call that action on its own too?
const nodesSlice = createSlice({
name: "nodes",
initialState,
reducers: {
},
extraReducers: {
[toggleExpandedProp.pending]: (state, action) => {
state.data.forEach((element) => {
if (element.id === action.payload.id) {
element.expanded = !element.expanded;
}
});
},
[getRoot.fulfilled]: (state, action) => {
state.data = action.payload;
},
[getChildren.fulfilled]: (state, action) => {
state.data.push(action.payload);
}
}
});
I made a todo list a while ago as a way to practice react and redux. Now I'm trying to rewrite it with redux toolkit and having some trouble with the action creators.
Here is the old actions creator:
export const changeDescription = (event) => ({
type: 'DESCRIPTION_CHANGED',
payload: event.target.value })
export const search = () => {
return (dispatch, getState) => {
const description = getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
axios.get(`${URL}?sort=-createdAt${search}`)
.then(resp => dispatch({ type: 'TODO_SEARCHED', payload: resp.data }))
} }
export const add = (description) => {
return dispatch => {
axios.post(URL, { description })
.then(() => dispatch(clear()))
.then(() => dispatch(search()))
} }
export const markAsDone = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: true })
.then(() => dispatch(search()))
} }
export const markAsPending = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: false })
.then(() => dispatch(search()))
} }
export const remove = (todo) => {
return dispatch => {
axios.delete(`${URL}/${todo._id}`)
.then(() => dispatch(search()))
} }
export const clear = () => {
return [{ type: 'TODO_CLEAR' }, search()] }
Now this is the one that I'm working on, I'm trying to replicate the actions of the old one but using redux toolkit:
export const fetchTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
const response = await axios.get(`${URL}?sort=-createdAt${search}`)
return response.data
})
export const addTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const response = await axios.post(URL, {description})
return response.data
})
export const todoReducer = createSlice({
name: 'counter',
initialState: {
description: '',
list: []
},
reducers: {
descriptionChanged(state, action) {
return {...state, dedescription: action.payload}
},
descriptionCleared(state, action) {
return {...state, dedescription: ''}
},
},
extraReducers: builder => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
const todo = action.payload
return {...state, list: action.payload}
})
.addCase(addTodos.fulfilled, (state, action) => {
let newList = state.list
newList.push(action.payload)
return {...state, list: newList}
})
}
})
The thing is, I can't find anywhere how to export my extra reducers so I can use them. Haven't found anything in the docs. Can someone help?
extraReducers
Calling createSlice creates a slice object with properties reducers and actions based on your arguments. The difference between reducers and extraReducers is that only the reducers property generates matching action creators. But both will add the necessary functionality to the reducer.
You have correctly included your thunk reducers in the extraReducers property because you don't need to generate action creators for these, since you'll use your thunk action creator.
You can just export todoReducer.reducer (personaly I would call it todoSlice). The reducer function that is created includes both the reducers and the extra reducers.
Edit: Actions vs. Reducers
It seems that you are confused by some of the terminology here. The slice object created by createSlice (your todoReducer variable) is an object which contains both a reducer and actions.
The reducer is a single function which takes the previous state and an action and returns the next state. The only place in your app when you use the reducer is to create the store (by calling createStore or configureStore).
An action in redux are the things that you dispatch. You will use these in your components. In your code there are four action creator functions: two which you created with createAsyncThunk and two which were created by createSlice. Those two will be in the actions object todoReducer.actions.
Exporting Individually
You can export each of your action creators individually and import them like:
import {fetchTodos, descriptionChanged} from "./path/file";
Your fetchTodos and addTodos are already exported. The other two you can destructure and export like this:
export const {descriptionChanged, descriptionCleared} = todoReducer.actions;
You would call them in your components like:
dispatch(fetchTodos())
Exporting Together
You might instead choose to export a single object with all of your actions. In order to do that you would combine your thunks with the slice action creators.
export const todoActions = {
...todoReducer.actions,
fetchTodos,
addTodos
}
You would import like this:
import {todoActions} from "./path/file";
And call like this:
dispatch(todoActions.fetchTodos())
I want to navigate to App screen or Auth screen, depending on the isUser prop after fetching it from the server and updating the redux store.
My first component AuthLoading.js which looks like this:
const AuthLoading = (props) => {
const isUser = useSelector((state) => state.authReducer.isUserExists);
const dispatch = useDispatch();
const fetchData = async () => {
const token = await TokensHandler.getTokenFromDevice();
dispatch(isTokenExists(token));
props.navigation.navigate(isUser ? "App" : "Auth");
};
useEffect(() => {
fetchData();
}, []);
My authActions.js looks like this:
export const isTokenExists = (token) => {
return (dispatch) => {
return HttpClient.get(ApiConfig.IDENTITY_PORT, "api/identity", {
userId: token,
}).then((response) => {
console.log(response);
dispatch({
type: IS_USER_EXISTS,
payload: response,
});
});
};
};
My authReducer.js looks like this:
const authReducer = (state = initialState, action) => {
switch (action.type) {
case IS_USER_EXISTS:
return {
...state,
isUserExists: action.payload,
};
default:
return state;
}
};
And the store:
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "../reducers";
const configureStore = () => {
return createStore(rootReducer, applyMiddleware(thunk));
};
export default configureStore;
Unfortunately, the code inside AuthLoading.js isn't asynchronous, and isn't waiting for the updated isUser value and running the next line without it.
I've tried to .then after dispatch which still doesn't work.
I've tried changing async states and still couldn't find the problem.
I have no idea how to fix this.
Thanks for the help.
You can use another useEffect hook that runs when isUser changes and navigate inside it.
This useEffect runs once the component mounts and every time isUser changes, so someCondition can be anything that determines whether the navigation should happen or not.
useEffect(() => {
if(someCondition) {
props.navigation.navigate(isUser ? "App" : "Auth");
}
}, [isUser]); // add all required dependencies
I have a React app that uses redux-thunk and axios to fetch an API. The action fires successfully, but returns multiple payloads which means it is firing multiple times.
How can I make it fire only one time?
Code
Actions
import Axios from "axios";
import { fetchEnglishLeagueTable } from "./ActionTypes";
export function fetchEnglishTable() {
var url = "https://api.football-data.org/v2/competitions/PL/matches";
var token = "52c8d88969d84ac9b17edb956eea33af";
var obj = {
headers: { "X-Auth-Token": token }
};
return dispatch => {
return Axios.get(url, obj)
.then(res => {
dispatch({
type: fetchEnglishLeagueTable,
payload: res.data
});
})
.catch(e => {
console.log("Cant fetch ", e);
});
};
}
Reducers
import { fetchEnglishLeagueTable } from "../actions/ActionTypes";
const initialState = {
EnglishTable: {}
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case fetchEnglishLeagueTable:
return {
...state,
EnglishTable: action.payload
};
default:
return state;
}
};
export default rootReducer;
Page
const League = props => {
useEffect(() => {
props.getLeagueTable();
}, [props.leagueTable]);
console.log(props.leagueTable);
return <p>ihi</p>;
};
const mapStateToProps = state => ({
leagueTable: state.EnglishTable
});
const mapDispatchToProps = dispatch => {
return { getLeagueTable: () => dispatch(fetchEnglishTable()) };
};
export default connect(mapStateToProps, mapDispatchToProps)(League);
Store
import rootReducer from "./Reducer";
import thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
Here is what it returns
Just remove leagueTable from useEffect's dependency array, so it'll fetch them only once component is mounted. Because now you have a loop:
Get leagues -> leagueTable updates -> useEffect sees that leagueTable changed in dependency array and calls to get leagues again and we've got a loop.
const League = props => {
useEffect(() => {
props.getLeagueTable();
}, []); // <~ no props.leagueTable here
console.log(props.leagueTable);
return <p>ihi</p>;
};
Hope it helps :)