In my React app, I have a list of orders which are supposed to be shown to the user for only 30 seconds so each order has a value of 30 seconds for its duration propery:
[
{
...,
...,
duration: 30
},
{
...,
...,
duration: 30
},
...
]
I'm using Redux Toolkit to store their data so that I can render the UI of these items in various components. I tried to create an action which gets dispatched every 1 second to decrement the duration by one:
decrementCountdown: (state, action) => {
const order = state.entities[action.payload];
if (order) order.duration -= 1;
}
Then, in App.jsx
, I dispatch the action using setInterval
inside a loop:
useEffect(() => {
let countdown;
for (order of orders) {
// Run until the duration reaches zero
if (order.duration > 1) {
countdown = setInterval(() => dispatch(decrementCountdown(order?.id)), 1000);
}
}
return () => clearInterval(countdown);
}, [orders])
The challenging part is that the timers have to be synched so that everywhere that the items are shown, the same remaining time is shown and decremented.
The method I have used didn't help me much. Especially when more that one order was present. In that case, the new order's duration wouldn't decrement and it caused an infinite loop inside the useEffect
.
Is there any way I can create a countdown for each one?
Thanks to @Alexey's response and comment I figured that I needed to create a separate Redux Slice dedicated to each order's duration. I was already using the Entity Adapter
of Redux Toolkit so the order and duration slices looked like this:
orderSlice.js
:
order: {
ids: [
0: 500,
1: 600
],
entities: {
500: {
id: 500,
...
},
600: {
id: 600,
...
},
}
}
orderDurationSlice.js
:
orderDurtaion: {
ids: [
0: 500,
1: 600
],
entities: {
500: {
id: 500,
duration: 30
},
600: {
id: 600,
duration: 30
},
}
}
After each order arrives via a Socket
connection, it gets added into both of the slices (Only the order id
gets added to the orderDuration
slice in order to correspond to the complete order object inside the order
slice).
The challenging part was to start the countdown timer for each one of the orders as soon as they arrive and also have it as a top-level service running independently of the component lifecycle so that each component that requires it could access it.
I was already using Redux Toolkit's Listener Middleware
to handle the Socket
connection and after someone on Reddit recommended the use of Redux's middleware
to handle this, I realized that I just had to create another listener
as a kind of countdown service.
This listener would only run if the orderDuration
slice is filled and the instance of setInterval
is undefined
.
socketListenerMiddleware.js
:
// Order countdown interval instance
let countdown;
// Create the middleware instance and methods
const socketListenerMiddleware = createListenerMiddleware();
// Handle each order's duration countdown after a new order arrives
socketListenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
// Check if the interval instance is undefined and the order duration slice is full
return !countdown && currentState.orderDuration?.ids.length;
},
effect: (action, listenerApi) => {
// Cancel any in-progress instances of this listener
listenerApi.cancelActiveListeners();
// Handle the countdown
countdown = setInterval(() => {
// Select all of the order durations
// These are being selected inside of the interval to always get the most-updated array
const orderDurations = orderDurationSelectors.selectAll(
listenerApi.getState(),
);
// Loop through each order duration to get each item
for (const order of orderDurations) {
// Start the countdown
listenerApi.dispatch(decrementDuration(order?.id));
// If the order duration state is less than or equal to zero, remove the order and its corresponding duration
if (order?.duration <= 0) {
listenerApi.dispatch(removeOrder(order?.id));
listenerApi.dispatch(removeDuration(order?.id));
}
}
// If the order duration slice gets empty, clear the interval and reset its instance
if (!orderDurations.length) {
clearInterval(countdown);
countdown = undefined;
}
}, 1000);
},
});
The main part which handles the countdown is here:
Here, only one instance of setInterval
is created which loops over the orderDuration
array to get each one of the items and dispatches an action which decrements each order's duration by 1.
If the order's duration gets to zero, two dispatched actions remove the order (in order
slice) and its corresponding duration (in orderDuration
slice).
Finally, outside of the loop an if
condition checks if the orderDuration
slice gets empty and if so, it clears the interval
and resets its instance.
// Handle the countdown
countdown = setInterval(() => {
// Select all of the order durations
// These are being selected inside of the interval to always get the most-updated array
const orderDurations = orderDurationSelectors.selectAll(
listenerApi.getState(),
);
// Loop through each order duration to get each item
for (const order of orderDurations) {
// Start the countdown
listenerApi.dispatch(decrementDuration(order?.id));
// If the order duration state is less than or equal to zero, remove the order and its corresponding duration
if (order?.duration <= 0) {
listenerApi.dispatch(removeOrder(order?.id));
listenerApi.dispatch(removeDuration(order?.id));
}
}
// If the order duration slice gets empty, clear the interval and reset its instance
if (!orderDurations.length) {
clearInterval(countdown);
countdown = undefined;
}
}, 1000);
I then access each order's duration via selector's inside components.
Hope this solution helps anyone dealing with this kind of question.