React Redux await until first action is complete - reactjs

I have an action that will take quite some time to complete. I want to inform the users that the action is currently taking place. To do this I am dispatching an action that updates a reducers state with a boolean.
The issue I am running into is that the larger action starts before the the state is updated. As a result, when a users clicks the generate button, they then experience the system lagging appose to UI informing them to wait.
How can I ensure the first action startGeneration() is completed before buildKeyMap() starts? Same questioned for reportProgress(). I have redux-thunk installed.
export const generateKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
dispatch(startGeneration()), // <-- this updates reducers state with a boolean, generatingMap: true
dispatch(buildKeyMap(mapFrom, mapTo)) // <-- this takes a long time to finish
};
};
export const buildKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
let keyMap = {};
for (let i = 0; i < mapFrom.length; i++) {
dispatch(reportProgress(i)); // <-- would like this to finish before moving on in the loop
let randomIndex = randomIntFromInterval(0, mapTo.length - 1);
let key = mapFrom[i].toString();
let value = mapTo[randomIndex].toString();
Object.assign(keyMap, { [key]: value });
mapTo.splice(randomIndex, 1);
}
dispatch(stopGeneration()); // <--- Would like this to finish before delivering the map
return { type: actionTypes.DELIVERKEYMAP, payload: keyMap };
};
};
export const startGeneration = () => {
return { type: actionTypes.STARTKEYMAPGENERATION };
};

Related

state not updated redux

so I made a notification system with react and redux and added a queue system for preventing to display to much notifications at the same time. It works how it should, but there is only one problem. If add 5 notifications the fifth one should go inside the queue array it does but the components do not relaize that the queue array has now a length of one. If i put again 5 notifications i have 4 on the active array and then 2 notifications inside the queue array, from here it knows that queue array length is now 2. Why is that? Is there a problem with the re-rendering?
My Code:
const notify = useSelector((state) => state.notify);
const handleClick = () => {
let notification = {
iconType: 0,
title: 'notification.title',
message: 'notification.message',
color: '03A65A',
width: 0,
};
showNotifications(notification);
};
const timeoutNotification = async (notification: any) => {
await dispatch(AddActiveItem(notification));
setTimeout(async () => {
await dispatch(RemoveActiveItem(notification.id));
if (notify.queue.length && notify.active.length < 4) {
let newQueue = notify.queue;
let pushItem = newQueue.shift();
await dispatch(SetNewQueue(newQueue));
timeoutNotification(pushItem);
}
}, 120 * notification.message.length);
};
const showNotifications = async (notification: any) => {
notification = {
id: notify.id,
iconType: notification.iconType,
title: notification.title,
message: notification.message,
color: notification.color,
width: notification.width,
};
let newId = notify.id + 1;
dispatch(SetId(newId));
if (notify.active.length > 3) {
await dispatch(AddQueueItem(notification));
} else {
timeoutNotification(notification);
}
};
Remember that Redux's dispatch is an async function, so the changes made to the Redux state are not visible immediately. If you're sure the state should/will update, simply await the changes..

How to avoid firing multiple redux actions with real time firestore listeners

Introduction
A Little Warning: I do use Redux Toolkit
I have bunch of lists, one of which should be active. And depending on some context, active list should be different. For example I have 3 lists (A, B, C) and let's look at following patterns:
List B is active and I decided to create a new list. After creating list D, list D should be active:
List D - active
List C
List B
List A
List B is active and I decided to change the page. When I come back, List B should be active as it was before changing the page.
The problem
As I initiate the setListsAction from the beginning, it always listens to the firestore and gets invoked every time I manipulate with the store (add, remove, update) and then pass all the data to the reducer. For this reason, I can't control which action was actually performed. For this case in my setListsReducer I check if there's already an active list, if so, I don't change it (covering my second pattern in the examples section). However, with such logic I can't set newly created list as active, because there'll be always an active list that's why in my createListAction I pas a newly created list to the payload and in createListReducer I set payload as the active list. However, the caveat of this approach is that both setListsAction and createListAction gets triggered, so redux state gets updated two times in a row, making my components rerender unnecessary. The cycle looks like that:
in createListAction I add list to the firestore
firestore was updated, so setListsAction gets triggered
createListAction dispatches fulfilled action.
My Code
Actions
setListsAction
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = await db.collection('lists');
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
const lists = querySnapshot.docs.map((doc) => {
const list = { ...doc.data(), id: doc.id };
return list;
});
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
});
};
};
createListAction
export const createListActionAsync = (list) => {
return async (dispatch: Dispatch<PayloadAction<any>>) => {
dispatch(listsLoadingActions.pending());
const docList = await db.collection('lists').add(list);
const fbList = { ...list, id: docList.id };
dispatch(listsLoadingActions.fulfilled(fbList));
};
};
Reducers
setListsReducer
builder.addCase(fetchLoadingActions.fulfilled, (state, { payload }) => {
state.lists = payload.lists;
const activeList = state.activeList
? payload.lists.find((l) => l.id === state.activeList.id)
: payload.lists[0];
state.activeList = activeList;
});
createListReducer
builder.addCase(listsLoadingActions.fulfilled, (state, { payload }) => {
state.activeList = payload;
});
Sum
So I would like you to propose a better way to handle my problem. I tried to solve it, using change type on docChanges but when I init setListsAction, all docs' changes are type of added and workarounds may damage further implementations of the app. Probably, I need to give up real time database and use get method instead.
If you eliminate the createListReducer and listLoadingActions, you should be able to do everything from inside the ListsAction hook. Using await db.collection('lists').add(list) should refire the listener on the lists collection once it's been added to the database successfully.
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = db.collection('lists'); // no need to await?
let firstLoad = true; // used to determine whether to use docs or docsChanges
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
if (firstLoad) {
const lists = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
firstLoad = false;
// Get and set initial active list?
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
} else {
// optionally fire dispatch(fetchLoadingActions.pending()) again?
const listsCopy = [...state.lists]; // copy the existing list to mutate it
let activeList = state.activeList; // store the current activeList
querySnapshot.docChanges().map((change) => {
if (change.type === "added") {
const thisList = { ...change.doc.data(), id: change.doc.id };
listsCopy.splice(change.newIndex, 0, thisList);
activeList = thisList;
} else if (change.type === "modified") {
listsCopy.splice(change.oldIndex, 1);
listsCopy.splice(change.newIndex, 0, { ...change.doc.data(), id: change.doc.id });
} else if (change.type === "removed") {
listsCopy.splice(change.oldIndex, 1);
if (activeList.id === change.doc.id) {
// the current active list was removed!
activeList = undefined;
}
}
});
dispatch(
fetchLoadingActions.fulfilled({
lists: listsCopy,
activeList: activeList || listsCopy[0] // use activeList or fallback to first list in listsCopy, could still be undefined if listsCopy is empty!
})
);
}
});
return unsubscribe;
};
};
Regarding the active list history, you could either use the URL ?list=some-id to store the selected list with the History API or you could store an array called activeListHistory in your state variable where you push() and pop() to it as necessary (make sure to handle cases where the old list no longer exists and where there are no entries in the array).

Show status of `For` loop to user

I have an action which will take quite some time to complete. It will be looping through an array with over 250k entries. With each entry it will be doing some logic. I want to inform the user of the actions progress, which would be:
let progress = (i / array.length) * 100
How can I add this to redux store or components state so that I can communicate the current progress in the UI?
I tried dispatching an action (as you can see below) which updates the stores progress with the current iterator but the reducer only executes when the for loop is created.
export const generateKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
await dispatch(startGeneration()); <-- This only runs when I use a setTimeout for the following action.
setTimeout(() => {
dispatch(buildKeyMap(mapFrom, mapTo));
}, 100);
};
};
export const buildKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
let keyMap = {};
for (let i = 0; i < mapFrom.length; i++) {
await dispatch(reportProgress(i)); // <-- This does not update in store until for loop is done.
let randomIndex = randomIntFromInterval(0, mapTo.length - 1);
let key = mapFrom[i].toString();
let value = mapTo[randomIndex].toString();
Object.assign(keyMap, { [key]: value });
mapTo.splice(randomIndex, 1);
}
await dispatch(stopGeneration());
return { type: actionTypes.DELIVERKEYMAP, payload: keyMap };
}
};

useEffect and redux a cache trial not working

I have a rather complex setup and am new to React with Hooks and Redux.
My setup:
I have a component, which when first mounted, should fetch data. Later this data should be updated at a given interval but not too often.
I added useRef to avoid a cascade of calls when one store changes. Without useEffect is called for every possible change of the stores linked in its array.
The data is a list and rather complex to fetch, as I first have to map a name to an ID and
then fetch its value.
To avoid doing this over and over again for a given "cache time" I tried to implement a cache using redux.
The whole thing is wrapped inside a useEffect function.
I use different "stores" and "reducer" for different pieces.
Problem:
The cache is written but during the useEffect cycle, changes are not readable. Even processing the same ISIN once again the cache returns no HIT as it is empty.
Really complex implementation. It dawns on me, something is really messed up in my setup.
Research so far:
I know there are libs for caching redux, I do want to understand the system before using one.
Thunk and Saga seem to be something but - same as above plus - I do not get the concept and would love to have fewer dependencies.
Any help would be appreciated!
Component - useEffect
const calculateRef = useRef(true);
useEffect(() => {
if (calculateRef.current) {
calculateRef.current = false;
const fetchData = async (
dispatch: AppDispatch,
stocks: IStockArray,
transactions: ITransactionArray,
cache: ICache
): Promise<IDashboard> => {
const dashboardStock = aggregate(stocks, transactions);
// Fetch notation
const stockNotation = await Promise.all(
dashboardStock.stocks.map(async (stock) => {
const notationId = await getXETRANotation(
stock.isin,
cache,
dispatch
);
return {
isin: stock.isin,
notationId,
};
})
);
// Fetch quote
const stockQuote = await Promise.all(
stockNotation.map(async (stock) => {
const price = await getQuote(stock.notationId, cache, dispatch);
return {
isin: stock.isin,
notationId: stock.notationId,
price,
};
})
);
for (const s of dashboardStock.stocks) {
for (const q of stockQuote) {
if (s.isin === q.isin) {
s.notationId = q.notationId;
s.price = q.price;
// Calculate current price for stock
if (s.quantity !== undefined && s.price !== undefined) {
dashboardStock.totalCurrent += s.quantity * s.price;
}
}
}
}
dispatch({
type: DASHBOARD_PUT,
payload: dashboardStock,
});
return dashboardStock;
};
fetchData(dispatch, stocks, transactions, cache);
}
}, [dispatch, stocks, transactions, cache]);
Action - async fetch action with cache:
export const getXETRANotation = async (
isin: string,
cache: ICache,
dispatch: AppDispatch
): Promise<number> => {
Logger.debug(`getXETRANotation: ${isin}`);
if (CACHE_ENABLE) {
const cacheTimeExceeding = new Date().getTime() + CACHE_NOTATION;
if (
isin in cache.notation &&
cache.notation[isin].created < cacheTimeExceeding
) {
Logger.debug(
`getXETRANotation - CACHE HIT: ${isin} (${cache.notation[isin].created} < ${cacheTimeExceeding} current)`
);
return cache.notation[isin].notationId;
}
}
// FETCH FANCY AXIOS RESULT
const axiosRESULT = ...
if (CACHE_ENABLE) {
Logger.debug(`getXETRANotation - CACHE STORE: ${isin}: ${axiosRESULT}`);
dispatch({
type: CACHE_NOTATION_PUT,
payload: {
isin,
notationId: axiosRESULT,
created: new Date().getTime(),
},
});
}
//FANCY AXIOS RESULT
return axiosRESULT;
}

Redux Action with For loop doesn't work as expected

TLDR : Weird Mutations. Solved with re-assigning variables. But not the most efficient...
PREFACE
I have a component called Feed connected to redux
export class Feed extends Component {
bookmark = (id) => {
this.props.bookmarkItem(id).then(s=>{
this.props.updateFeed(this.props.feed, this.props.bookmarks);
}); //redux action with redux-thunk
}
render(){
//renders card items with a bookmark button on each card
//onPress button call this.bookmark(item.id)
}
}
const mapStateToProps = (state) => ({
feed: state.app.feed,
bookmarks: state.app.bookmarks
})
export default connect(mapStateToProps, { bookmarkItem, updateFeed })(Feed)
On pressing a button on the card, a this.bookmark() function is called and an action this.props.bookmarkItem(id) is called.
The bookmarkItem redux action looks like this.
export const bookmarkItem = (item_id) => async dispatch => {
try {
let r = await axios.post(`/bookmark`); //api call
let { bookmarks } = r; //get updated bookmarked items from api
dispatch({
type: cons.UPDATE_BOOKMARKED,
payload: { bookmarked }
});
return true;
}
catch (err) {
throw new Error(err);
}
}
Redux state looks like this
let app = {
feed: [{id:1}, {id:2}, {id:3}, {id:4}],
bookmarks:[{id:1}, {id:2}]
}
AIM
Now I gotta update the feed array in Redux State with property isBookmarked for that each item in feed.
(So that we can display that item is already bookmarked or not)
So for that I am calling another updateFeed() redux action
export const updateFeed = (feed, bookmarks) => dispatch => {
console.log('checkpoint 1', feed); //CHECKPOINT 1
for (let i = 0; i < feed.length; i++) {
if (bookmarks.length == 0) {
feed[i].isBookmarked = false; //mark item as NOT bookmarked
} else {
//check all bookmarks
for (let p = 0; p < bookmarks.length; p++) {
//if bookmark exists, set item in feed as isBookmarked:true
if (bookmarks[p].id === feed[i].id)
feed[i].isBookmarked = true; //mark item as bookmarked
}
}
}
dispatch({
type: cons.UPDATE_FEED,
payload: {
feed
}
});
};
PROBLEM
So there are a few problems I'm facing here:
At CHECKPOINT 1 the feed object logs a mutated feed with isBookmarked property in Google Chrome Console, even before the loop is started.
The payload that goes at UPDATE_FEED action is the with isBookmarked property, but the re-render of the feed never takes place.
HACKY WORKAROUND
Assigning feed to a new variable t in UPDATE_FEED action correctly mutates and then sends in the dispatch call.
But I'm not sure if this is the most efficient way to do it.
export const updateFeed = (feed, bookmarks) => dispatch => {
let t = JSON.parse(JSON.stringify(feed));
console.log('checkpoint 1', feed); //CHECKPOINT 1
for (let i = 0; i < t.length; i++) {
if (bookmarks.length == 0) {
t[i].isBookmarked = false; //mark item as NOT bookmarked
} else {
//check all bookmarks
for (let p = 0; p < bookmarks.length; p++) {
//if bookmark exists, set item in feed as isBookmarked:true
if (bookmarks[p].id === t[i].id)
t[i].isBookmarked = true; //mark item as isBookmarked
}
}
}
dispatch({
type: cons.UPDATE_FEED,
payload: {
feed : t //THIS WORKS CORRECTLY
}
});
};
Is waiting until that for loop is finished asynchronously a better way to do it ?
EXTRAS
Reducer looks like this. Pretty simple, only updates with new values, all cases handled by single switch method.
const initialState = {
feed: [],
bookmarks:[]
}
const app = (state = initialState, action) => {
console.log('appReducer', action, JSON.stringify(state));
switch (action.type) {
case cons.UPDATE_BOOKMARKS:
case cons.UPDATE_FEED:
{
state = {
...state,
...action.payload,
}
break;
}
}
return state;
}
export default app;
Assigning feed to a new variable t in UPDATE_FEED action correctly mutates
It simply means,your reducers not creating new state object.Its just mutate the state in place.
It uses === to check state update in state tree.
...action.payload it coping the same reference for feed object and nested element as it is in the new state.So,there is no state update.
Change cons.UPDATE_FEED like this.
state = {
...state,
...action.payload.feed,
}

Resources