markerikson / marks-dev-blog-comments

Comments for my blog
4 stars 0 forks source link

Idiomatic Redux: Designing the Redux Toolkit Listener Middleware #51

Open utterances-bot opened 2 years ago

utterances-bot commented 2 years ago

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

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

josh-degraw commented 2 years ago

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

jakiestfu commented 2 years ago

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!

Nubuck commented 2 years ago

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

markerikson commented 2 years ago

@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: https://github.com/jeffbski/redux-logic/issues/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.

janne-nylund commented 1 year ago

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 commented 1 year ago

@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.

janne-nylund commented 1 year ago

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 commented 1 year ago

@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?

janne-nylund commented 1 year ago

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())  
  },
});
janne-nylund commented 1 year ago

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