Redux: Call thunk action from slice reducer action - reactjs

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);
}
}
});

Related

React and redux-tookit: How to dispatch action when other action completed?

I am trying to show a snackbar whenever a deletion succeeds. I created an action for that.
Where do I dispatch that action?
At the moment, my code looks like this:
export const deleteSelectedEntry = createAsyncThunk('entries/delete/selected', async (id: string) => {
const response = await BackendService.deleteEntry(id);
const dispatch = useAppDispatch();
dispatch(setSnackBarState({
state: true,
message: "SUCCESS DELETING"
}));
return response.data
})
This is an async thunk in one of the slice classes that you create when using the redux toolkit.
I created a hook for the dispatch method as per redux-toolkit's suggestion in the tutorial:
export const useAppDispatch: () => AppDispatch = useDispatch
But wherever I think I should be able to put the dispatch method, I get an error that I cannot use the react hook there.
My initial attempt was putting it in the extraReducers:
extraReducers(builder) {
builder
.addCase(deleteSelectedEntry.fulfilled, (state: MyState, action: PayloadAction<Entry>) => {
// do Stuff
})
How do you then dispatch actions from other actions in react redux? I have seen examples on StackOverflow where people used the useDispatch method in an asyncThunk.
Help and tipps appreciated!
If necessary, I'll post more code.
I think your initial intuition was correct, in using the extraReducers slice property. Here is how I have done for what I call a "notificationSlice":
import { createEntityAdapter, createSlice, isAnyOf } from '#reduxjs/toolkit'
import {
doStuffPage1
} from './page1.slice'
import {
doStuffPage2
} from './page2.slice'
const notificationsAdapter = createEntityAdapter()
const initialState = notificationsAdapter.getInitialState({
error: null,
success: null,
warning: null,
info: null,
})
const notificationsSlice = createSlice({
name: 'notifications',
initialState,
reducers: {},
extraReducers: builder => {
builder
// set success on fulfilled
.addMatcher(
isAnyOf(
doStuffPage1.fulfilled,
doStuffPage2.fulfilled,
),
(state, action) => {
state.error = null
state.warning = null
state.success = action.payload.message
}
)
// set error on rejections
.addMatcher(
isAnyOf(
doStuffPage1.rejected,
doStuffPage2.rejected,
),
(state, action) => {
state.error = action?.payload
state.warning = null
state.success = null
}
)
// reset all messages on pending
.addMatcher(
isAnyOf(
doStuffPage1.pending,
doStuffPage2.pending,
),
(state, action) => {
state.error = null
state.success = null
state.warning = null
}
)
},
})
export const {
clearNotifications,
setError,
setSuccess,
setWarning,
setInfo,
} = notificationsSlice.actions
export default notificationsSlice.reducer
export const getErrorMsg = state => state.notifications.error
export const getSuccessMsg = state => state.notifications.success
export const getInfoMsg = state => state.notifications.info
export const getWarningMsg = state => state.notifications.warning
Some things to note:
The selectors will be imported somewhere in a high level component, and additional snackbar logic will be used THERE
You need to ensure that your thunks (doStuffPage1/doStuffPage2) are returning messages with their success/error results

Adding function in Slice for redux

Please help me how I can introduce new function like getOrdersByCustomer in ordersSlice. I have provided full code of ordersSlice below. Please tell me what is extraReducers and how it works.
import { createSlice, createAsyncThunk, createEntityAdapter } from '#reduxjs/toolkit';
import axios from 'axios';
export const getOrders = createAsyncThunk('eCommerceApp/orders/getOrders', async () => {
const response = await axios.get('/api/e-commerce-app/orders');
const data = await response.data;
return data;
});
export const removeOrders = createAsyncThunk(
'eCommerceApp/orders/removeOrders',
async (orderIds, { dispatch, getState }) => {
await axios.post('/api/e-commerce-app/remove-orders', { orderIds });
return orderIds;
}
);
const ordersAdapter = createEntityAdapter({});
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
const ordersSlice = createSlice({
name: 'eCommerceApp/orders',
initialState: ordersAdapter.getInitialState({
searchText: ''
}),
reducers: {
setOrdersSearchText: {
reducer: (state, action) => {
state.searchText = action.payload;
},
prepare: event => ({ payload: event.target.value || '' })
}
},
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload)
}
});
export const { setOrdersSearchText } = ordersSlice.actions;
export default ordersSlice.reducer;
In Addition
Also can you please tell me what I will do with this following code for my custom function getOrdersByCustomer.
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
because, in my component I have used like
const orders = useSelector(selectOrders);
You can introduce new (async) functions as you already have (I used the customerId as part of the url -> you could access it through the params in your backend):
export const getOrdersByCustomer = createAsyncThunk('eCommerceApp/orders/getOrdersByCustomer', async (customerId) => {
const response = await axios.get(`/api/e-commerce-app/orders/${customerId}`);
const data = await response.data;
return data;
});
Then you can handle the response in your extraReducer:
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload),
[getOrdersByCustomer.fulfilled]: (state, action) =>
// set your state to action.payload
}
The extraReducers handle actions like async thunks. The createAsyncThunk function return 3 possible states (along with other things): pending, rejected or fulfilled. In your case you only handle the fulfilled response. You could also set your state with the other two options (in your case [getOrdersByCustomer.pending] or [getOrdersByCustomer.rejected]

Exporting extra reducers from redux toolkit

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())

createAsyncThunk and writing reducer login with redux-toolkit

I was reading createAsyncThunk documentation, and felt kind of confused with the flow. This is from the docs:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[fetchUserById.fulfilled]: (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
}
}
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
What do I have to write in the reducers and extraReducers? Standard reducer logic?
I have this CodeSandbox that I implemented the old redux way. Now, need to implement redux-toolkit in it.
The reducers property of createSlice allows you to make an action creator function and respond to those actions in one step. You use extraReducers to respond to an action that has already been created elsewhere, like in an async thunk. The extraReducer just responds to an action but does not create an action creator function.
The example is saying that you can have some regular reducers in addition to the extraReducers. But I looked at your CodeSandbox and in your case you do not need any other reducers because the only actions that you are responding to are the three actions from the async thunk.
Since your createSlice isn't going to make any action creators you don't really need to use createSlice. You can use it, but you can also just use createReducer.
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
export const fetchUserFromGithub = createAsyncThunk(
'users/fetch',
async (username) => {
const response = await axios.get(
`https://api.github.com/users/${username}`
);
return response.data
}
)
const usersSlice = createSlice({
name: 'users',
initialState: {
user: null,
fetchingUser: false,
fetchingError: null
},
reducers: {},
extraReducers: {
[fetchUserFromGithub.pending]: (state, action) => {
state.fetchingUser = true;
state.fetchingError = null;
},
[fetchUserFromGithub.rejected]: (state, action) => {
state.fetchingUser = false;
state.fetchingError = action.error;
}
[fetchUserFromGithub.fulfilled]: (state, action) => {
state.fetchingUser = false;
state.user = action.payload;
}
}
})

Redux - dispatch is not being called

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

Resources