Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idiomatic Redux: Designing the Redux Toolkit Listener Middleware #51

Open
utterances-bot opened this issue Mar 19, 2022 · 10 comments
Open

Comments

@utterances-bot
Copy link

Idiomatic Redux: Designing the Redux Toolkit Listener Middleware · Mark's Dev Blog

https://blog.isquaredsoftware.com/2022/03/designing-rtk-listener-middleware/

Copy link

Great post as always! So glad to have this feature finally out there!

Copy link

Really awesome article, Mark. I thoroughly enjoyed the writeup, really awesome to see all the twists and turns this took you through.

This is something me and my colleague were complaining about and wishing for. Days later, 1.8 was released. Great work again!

Copy link

Nubuck commented Apr 5, 2022

Redux logic has done all this and more for years already, with the ability to work with both pre-reducer guards and post-reducer effects however you like sync, async and observables etc. Truly the most influential project I rely on. All this seems like alot of work to mimic a fraction of its power
https://github.com/jeffbski/redux-logic

Copy link
Owner

markerikson commented Apr 5, 2022

@Nubuck : yes, there's definitely a lot of overlap with redux-logic here. However, that library is basically unmaintained at this point, and also has had major bundle size issues. (In fact, I filed an issue two years ago pointing out the bundle size problems, and never got a response: jeffbski/redux-logic#172 ). There's also a complete lack of TS examples or documentation.

We wanted something we could build into RTK itself, and redux-logic simply was not a viable option here.

Copy link

janne-nylund commented Jun 23, 2023

Firstly, thank you for your excellent work with Redux Toolkit!

I'm dipping my toes into the listener middleware (I hate TS with sagas...), and I have a question regarding the effectScenarios examples. Could await listenerApi.job.delay(15) followed by listenerApi.subscribe() also be used to mimic throttling instead of setTimeout() or is there a difference?

@markerikson
Copy link
Owner

@janne-nylund : possibly? :) Haven't thought about that in a while. I would think that you could, but I couldn't tell you off the top of my head what the exact sequence should be to make that happen.

Copy link

janne-nylund commented Jun 24, 2023

Thanks for the quick response!

I spun up a simple RTK demo and await listenerApi.delay() worked like a charm:

// throttle
listenerMiddleware.startListening({
  actionCreator: addIncrement,
  effect: async (action, listenerApi) => {
    listenerApi.dispatch(increment())
    listenerApi.unsubscribe()
    await listenerApi.delay(500);
    listenerApi.subscribe()
  },
});

// debounce
listenerMiddleware.startListening({
  actionCreator: addDecrement,
  effect: async (action, listenerApi) => {
    listenerApi.cancelActiveListeners()
    await listenerApi.delay(500);
    listenerApi.dispatch(decrement())
  },
});

@markerikson
Copy link
Owner

@janne-nylund awesome!

I'll certainly say that writing that isn't as obviously intuitive as a purposely-named throttle or debounce effect :) But we did want to keep the listener middleware API pretty simple on purpose, and it's great to see that the async primitives we provided were enough to build those. And honestly you could probably pull those out into a couple little helpers, like:

async function throttle(listenerApi, timeout) {
 listenerApi.unsubscribe()
 await listenerApi.delay(timeout);
 listenerApi.subscribe()
}

async function debounce(listenerApi, timeout) {
  listenerApi.cancelActiveListeners()
  await listenerApi.delay(timeout);
}

In fact, now that I say that...

maybe what we ought to do is take these and the other relevant examples from that effects test file, and paste them into a docs section showing how to do these?

Copy link

janne-nylund commented Jun 24, 2023

Good suggestion! This absolutely looks much cleaner:

const throttle = async (listenerApi, timeout, work) => {
  listenerApi.dispatch(work)
  listenerApi.unsubscribe()
  await listenerApi.delay(timeout);
  listenerApi.subscribe()
 }
 
 const debounce = async (listenerApi, timeout, work) => {
   listenerApi.cancelActiveListeners()
   await listenerApi.delay(timeout);
   listenerApi.dispatch(work)
 }

listenerMiddleware.startListening({
  actionCreator: addIncrement,
  effect: async (action, listenerApi) => {
    await throttle(listenerApi, 1000, increment())
  },
});

listenerMiddleware.startListening({
  actionCreator: addDecrement,
  effect: async (action, listenerApi) => {
    await debounce(listenerApi, 1000, decrement())  
  },
});

Copy link

Hi again! I got some time to play with my listener demo again and started to type the throttle & debounce functions. This is what I have so far. Could the typing be improved?

import { AnyAction, ListenerEffectAPI } from '@reduxjs/toolkit';
import { AppDispatch, RootState } from './store'

type AppListenerApi = ListenerEffectAPI<RootState, AppDispatch>;

interface IListenerHelper {
  ( 
    listenerApi: AppListenerApi, 
    timeout: number, 
    work: AnyAction
  ) : Promise<void>
}

export const throttle: IListenerHelper = async ( listenerApi, timeout, work ) => {
  listenerApi.dispatch(work);
  listenerApi.unsubscribe();
  await listenerApi.delay(timeout);
  listenerApi.subscribe();
}

 
export const debounce: IListenerHelper = async ( listenerApi, timeout, work ) => {
  listenerApi.cancelActiveListeners()
  await listenerApi.delay(timeout);
  listenerApi.dispatch(work)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants