createAsyncThunk - changing the state on pending doesn't cause immediate rerendering - reactjs

I'm using typescript, useSelector and #reduxjs/toolkit...
...When I dispatch the computeConfidenceIntervalsAsync action (see the code below), I can see the code running the following line immediatly:
state.status = 'loading'
But the ViewComponent is re-rendered only after the payloadCreator has finished running the doSomeHeavyComputation function.
To explain it in another way: in the ViewComponent I would expect the 'loading' state rendered before the heavy computation but for some reason the heavy computation is run first, then I receive 'loading' and 'idle' in a row.
Any help on this one?
Reducer:
//...
export const computeConfidenceIntervalsAsync = createAsyncThunk(
'forecast/getConfidenceIntervals',
async (data: ConfidenceIntervalsParams) => {
const response = await getConfidenceIntervals(data.totalRuns, data.itemsTarget, data.throughputs, new Date(data.startingDate));
return [...response.entries()];
}
);
export const forecastSlice = createSlice({
name: 'forecast',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(computeConfidenceIntervalsAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(computeConfidenceIntervalsAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.confidenceIntervals = action.payload;
});
}
});
export const selectForecast = (state: RootState) => state.forecast;
//...
service:
//...
export function getConfidenceIntervals(totalRuns: number, itemsTarget: number, throughputs: number[], startingDate: Date) {
return new Promise<Map<string, number>>((resolve) => {
console.log('beginning');
const outcomes = doSomeHeavyComputation(totalRuns, itemsTarget, throughputs, startingDate);
console.log('ending');
resolve(outcomes);
});
}
//...
Component:
export function ViewComponent() {
const forecast = useAppSelector(selectForecast);
console.log(forecast)
if (forecast.status === 'loading') {
return (
<div>Loading...</div>
);
}
return (<div>...</div>);
useSelector hook
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '../redux';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Synchronous work is blocking - and not really what cAT is designed for.
You can make this work by doing something like
export async function getConfidenceIntervals(totalRuns: number, itemsTarget: number, throughputs: number[], startingDate: Date) {
await Promise.resolve() // this will defer this to the next tick, allowing React to render in-between
console.log('beginning');
const outcomes = doSomeHeavyComputation(totalRuns, itemsTarget, throughputs, startingDate);
console.log('ending');
return outcomes;
}
(I've taken the liberty of rewriting it to async/await)
Your promise would have started immediately with no pause in-before so I added an await Promise.resolve() which defers execution by a tick.

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

Redux toolkit thunk wait for state change

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 ?? []));
}
};

Redux with getStaticProps

I'm struggling to understand how I can get around this issue?
I want to get data that I can share globally using redux**(using redux as I'm using it for other use cases in my app)**. my problem is I'm using getStaticProps to try and dispatch my ReduxThunk but I can't use Hooks inside getStaticProps and I have no idea what the workaround would be if anyone could point me to some docs I would appreciate it
Slice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const fetchData = createAsyncThunk(
"fetchCoinData",
async (url, thunkApi) => {
const data = await fetch(url).then((res) => res.json());
return data;
}
);
const initialState = {
data: [],
status: null,
};
const getData = {};
export const dataSlice = createSlice({
name: "datafetch",
initialState,
extraReducers: {
[getData.pending]: (state) => {
state.status = "Loading!";
},
[getData.fulfilled]: (state, { payload }) => {
state.data = payload;
state.status = "Sucsess!";
},
[getData.rejected]: () => {
state.status = "Failed";
},
},
});
// Action creators are generated for each case reducer function
export const {} = dataSlice.actions;
export default dataSlice.reducer;
cardano.js
import React from "react";
import { useDispatch } from "react-redux";
import BasicCard from "../../Components/UI/Cards/BasicCard";
import { UsersIcon } from "#heroicons/react/outline";
import { fetchData } from "../../redux/slice/DataSlice";
const cardano = (props) => {
return (
<div>
<h1>Header</h1>
</div>
);
};
//PROBLEM IS HERE
export async function getStaticProps(context) {
const dispatch = useDispatch();
const priceQuery =
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin%2Ccardano%2Cethereum&vs_currencies=USD";
const res = await dispatch(fetchData(priceQuery));
return {
props: {
theData: res,
}, // will be passed to the page component as props
};
}
export default cardano;
To use Hooks, it has to be located at the highest level. Another alternative that you may try is the lifecycle of React. They are the same as Hooks.
For something like this, you probably want to use https://github.com/kirill-konshin/next-redux-wrapper .
Generally you have to be aware though: just putting something in the global state during SSR does not mean your client has it - your client will have it's own Redux state, and each page that is server-side-rendered also has their own isolated Redux state and you will think about what of that you will "hydrate" from the server to the client to make it available there.
The docs of next-redux-wrapper go more into this than I could possibly explain myself, so give those a read!

Redux: Call thunk action from slice reducer action

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

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