Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Repo info
Activity
  • Oct 02 16:41
    greenkeeper[bot] labeled #670
  • Oct 02 16:41
    greenkeeper[bot] opened #670
  • Oct 02 16:41

    greenkeeper[bot] on cross-env-6.0.3

    chore(package): update cross-en… (compare)

  • Oct 01 17:57
    jayphelps closed #669
  • Oct 01 17:57
    jayphelps closed #668
  • Oct 01 17:57
    greenkeeper[bot] labeled #669
  • Oct 01 17:57
    greenkeeper[bot] opened #669
  • Oct 01 17:56

    greenkeeper[bot] on @types

    chore(package): update @types/s… (compare)

  • Oct 01 17:46
    greenkeeper[bot] labeled #668
  • Oct 01 17:46
    greenkeeper[bot] opened #668
  • Oct 01 17:45

    greenkeeper[bot] on cross-env-6.0.2

    chore(package): update cross-en… (compare)

  • Oct 01 17:43
    jayphelps closed #666
  • Oct 01 17:43
    jayphelps closed #665
  • Oct 01 17:43
    jayphelps closed #667
  • Oct 01 17:21
    greenkeeper[bot] labeled #667
  • Oct 01 17:21
    greenkeeper[bot] opened #667
  • Oct 01 17:21

    greenkeeper[bot] on cross-env-6.0.1

    chore(package): update cross-en… (compare)

  • Oct 01 07:41
    char0n commented #402
  • Oct 01 06:58
    cjol commented #402
  • Oct 01 06:56
    cjol commented #402
liamzdenek
@liamzdenek
action$.ofType(A,B,C).pipe(debounce(selectorFn)) ?
(of course you have to provide a selector function to debounce), but that should be the general approach.
Pingshun Huang (Alex)
@pingshunhuangalex
Isn't it just the same as having a debounceTime(xxx) before switchMap? This will only debounce when the epic is triggered by the same action type not multiple actions
liamzdenek
@liamzdenek
yes, it's the same, but no, it doesn't work like that. debounce/debounceTime doesn't care about the actual event data that's emitted by the observable. It's all in one big stream of events with different actions, and debounce/debounceTime operates on the stream as a whole
You can think of the color on the marble as your action type. The type/"color" has no effect on the output stream
Pingshun Huang (Alex)
@pingshunhuangalex
But I tried the debounceTime, it works when there's only one action type but doesn't work with multiple ones. So if the epic is first triggered by A and immediately triggered by B, having a debounceTime there won't stop the epic from executing twice
liamzdenek
@liamzdenek
and when you use the exact same code but just swap B for A, does it only run once?
Pingshun Huang (Alex)
@pingshunhuangalex
So if it's .ofType(A) (one action type), it works fine
liamzdenek
@liamzdenek
I'm saying, leave it as .ofType(A, B, C), but trigger it twice in a row with A. Does it only run once?
if yes, then try twice in a row with only B and make sure that runs once
Pingshun Huang (Alex)
@pingshunhuangalex
Sure let me try
liamzdenek
@liamzdenek
also the other uh, sanity check is to make sure your debounceTime is sufficiently large to catch both actions in the same window. I think the argument is in milliseconds
but if it worked fine with only A well, it's probably big enough
Pingshun Huang (Alex)
@pingshunhuangalex
Yes, I'm using 1000, should be long enough
liamzdenek
@liamzdenek
oh and the other sanity check is uh, if you were rewriting multiple epic functions into one... make sure all those old functions are commented out. It could be two epics running simultaneously. You could probably debug that more with tap() + console.log but uh, i've made this mistake a bunch
Skimming the docs, I don't think my understanding of debounceTime is wrong (although the first few things I've said test that), but uh, now I'm just trying to look for common mistakes
Pingshun Huang (Alex)
@pingshunhuangalex
Ok, something interesting here, didn't see it coming either. For every action A, the epic got triggered twice, and action B is normal. DebounceTime also seems to be working
So maybe it's not a debounceTime issue. I'll dig further. Thanks for the sanity check reminders @liamzdenek
liamzdenek
@liamzdenek
that sounds very much like you have an old function still listening to action A :p good luck, i'm off to bed
you can use tap((action) => console.log('got action', action))
and then use action A, the one that's triggered twice. It should print out that line for both triggers. If it does, it means there's no rogue old function
Pingshun Huang (Alex)
@pingshunhuangalex
Thanks again @liamzdenek night
Kevin Ghadyani
@Sawtaytoes

@RikuVan When I say read and write observables, I mean observables dedicated to reading data when an action occurs and dispatching that data out as another action.

Write observables will accept data and write to a single source whether that be localStorage, an external API, or back to Redux state. Sometimes I have these combined, but my most-recent projects separate them out since I can make more-generic epics this way.

Richard Van Camp
@RikuVan
@Sawtaytoes sounds sensible, small, single purpose but generic and therefore reusable. Its is really easy to watch epics bloat over time as various edge cases are handled and devs try to 'get fancy' with dispatching more actions from them and adding branching logic--makes maintaining tests harder too. Good to think about these things now at the outset of a big new project, a stage I happen to be in.
Kevin Ghadyani
@Sawtaytoes
I see. Yes. The hardest part about generic epics is debugging. Redux Devtools sucks in this respect. It will only show the action type, nothing else. I often pass a namespace prop or something to help figure out what's going on when actions are generic. You'll need to figure out a debugging solution yourself. In my Node.js projects, I can't use Redux Devtools, so I create my own logger which logs the action type and the namespace prop if it exists. Those little things greatly help when debugging. I also add a ofNamespace filter similar to ofType so I can mergeMap accordingly. You could also use groupBy so you can safely switchMap multiple different API calls with a single epic.
@RikuVan ^^
xavier7179
@xavier7179

I'm struggling with writing an epic that on action calls a function (which comes from a node module) that I would like to wrap using bindNodeCallback... I somehow solved how to call it but it looks like I missed something because I cannot fire actions based on the mapping of error and results... My actual code looks like this:

const fnAsObservable = bindNodeCallback(fn);
...
export function myEpic(action$) {
    return (
        action$.pipe(
            ofType(MY_ACTION_TYPE),
            mergeMap((action) =>
                fnAsObservable().pipe(
                    map(results => actionDone(results)),
                    catchError(err => actionFail(err))
                )
            )
        )
    );
}

The fnAsObservable is correctly called but the MY_ACTION_TYPE is "fired" in endless loop... I also tried to .map directly (instead of pipe() the fnAsObservable, but then I got an error saying the .map was not a function)
Thank you in advance for the help!

Kevin Ghadyani
@Sawtaytoes
@xavier7179 catchError needs an observable returned. Return of(actionFail(err)).
Kevin Ghadyani
@Sawtaytoes
@xavier7179 for the loop, check if the action done is the correct type.
Kevin Ghadyani
@Sawtaytoes

@xavier7179 There are a few reasons there would be an infinite loop. You need to look at the output of this. Add a tap after the catchError and log the output. That way, we can track down the issue. The code you wrote looks good otherwise and shouldn't cause infinite loops. You'll want to take a look at fnAsObservable() to see what it's doing, check to make sure actionDone doesn't have the type MY_ACTION_TYPE, and also that after ACTION_DONE is called, something else isn't dispatching MY_ACTION_TYPE.

This issue could be anywhere in your flow. Without any debugging information, it will be impossible to fix.

Anthony Lukach
@alukach

Any tips on using an async function in a mergeMap operator? Here's what I'm trying to do:


const fetchClientsEpic: Epic<any, any, AppState> = (action$, state$, context) =>
  action$.pipe(
    filter(isActionOf(fetchClients.request)),
    mergeMap(async () => { // This doesn't work!
      const token = await context.client!.getTokenSilently();
      return ajax
        .getJSON<fetchClientsResponse>(
          apiEndpoints.clients,
          tokenToHeader(token)
        )
        .pipe(
          flatMap((data) => [
            // ...
          ]),
          catchError((e) => of(fetchClients.failure(e.xhr.response)))
        );
    })
  );

I get the following error: Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.

liamzdenek
@liamzdenek
You should remove the async keyword from the mergeMap function, and instead convert the promise returned by context.client!.getTokenSilently() to an Observable by using from(context.client!.getTokenSilently()) and then use it as an Observable stream that either emits (one event and closes) or emits an error
and then you can just chain it normally into the ajax call
(Ideally, this token would be stored in state$ though)
Anthony Lukach
@alukach

@liamzdenek thanks, I do think I see what you're getting at. This did the trick:

const fetchClientsEpic: Epic<any, any, AppState> = (action$, state$, context) =>
  action$.pipe(
    filter(isActionOf(fetchClients.request)),
    mergeMap(() =>
      from(context.client!.getTokenSilently() as Promise<string>).pipe(
        mergeMap((token: string) =>
          ajax
            .getJSON<fetchClientsResponse>(
              apiEndpoints.clients,
              tokenToHeader(token)
            )
            .pipe(
              flatMap((data) => [
                //...
              ]),
              catchError((e) => of(fetchClients.failure(e.xhr.response)))
            )
        )
      )
    )
  );

Is the nested mergeMap necessary or is there is a cleaner way?

(for the record, the reason that I don't store the token in state$ is because context.client.getTokenSilently() has some helper tooling to retrieve the token if the cached token is valid or fetch a new token if the cached token is expired. I'm trying out a new Auth0 library, still not sure if it's better than my previous technique)
liamzdenek
@liamzdenek

You can write rxjs code a few different ways generally, and it's mostly a matter of taste/readability as far as how you'd like to write it.

You could do something like this:

action$.pipe(
  filter(isActionOf(fetchClients.request))
  withLatestFrom(
    from(context.client!.getTokenSilently() as Promise<string>)
  ),
  mergeMap([action, token] => {
    return ajax.getJSON(...)
    .pipe(
      mergeMap(data => ...),
      catchError(e => ...)
    );
  }),
);
Anthony Lukach
@alukach
@liamzdenek that's very clean, thanks
liamzdenek
@liamzdenek
np
Kevin Ghadyani
@Sawtaytoes
@liamzdenek So withLatestFrom is the new combineLatest operator? Are there other new operators for the observable counterparts where the same-name operator was removed?
liamzdenek
@liamzdenek
withLatestFrom only emits when the source observable emits, not when the child observables emits
combineLatest emits when any observable emits
so using Anthony's example, if the getTokenSilently was replaced with a BehaviorSubject, we don't want to re-run everything below withLatestFrom with the most recent action when the BehaviorSubject happens to change
liamzdenek
@liamzdenek
withLatestFrom is semantically similar to this, but without the typing
.pipe(
  combineLatest(second$),
  distinctUntilChanged(([prevSource, prevSecond], [currSource, currSecond]) => prevSource !== currSource)
)
xavier7179
@xavier7179
@Sawtaytoes thank you, I will start checking things... before posting here, I already check whether the actions called within map and catchError were ok, and they seems right: they return a type that is correctly different than MY_ACTION_TYPE (and some log placed in action functions seems to confirm that the original action return MY_ACTION_TYPE is called instead of the two supposed to be called when the fnAsObservable reach the callback...
Kevin Ghadyani
@Sawtaytoes
@xavier7179 You'll wanna put some tap(console.log) statements in there to be sure. I bet you something else is calling MY_ACTION_TYPE that's not even related to this epic.
Shobhit Gupta
@shobhitg
Hi, I wish to get access to the action$ observable outside of epics. Is it possible?
Richard Van Camp
@RikuVan
withLatestFrom is nicer I think when you want to use state$ when responding to an action instead of state$.value. Cosmetic I guess. I wonder if there are other common use cases in epics?
Kevin Ghadyani
@Sawtaytoes
@shobhitg Yes and no. What reason do you want it?
@RikuVan Not just cosmetic, that's actually a great use case. Shoot, I just fixed up some code last night which would've benefited.