by

Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Repo info
Activity
  • Jul 01 17:00
    jayphelps closed #724
  • Jul 01 17:00
    jayphelps commented #724
  • Jun 25 21:48
    squgeim commented #493
  • Jun 25 20:11
    jayphelps commented #493
  • Jun 25 20:07
    squgeim commented #493
  • Jun 25 10:58
    evertbouw locked #413
  • Jun 25 10:58
    evertbouw commented #413
  • Jun 23 05:48
    ghasemikasra39 commented #413
  • Jun 22 17:11
    jayphelps commented #413
  • Jun 22 16:51
    ghasemikasra39 commented #413
  • Jun 20 10:14
    eightyfive commented #276
  • Jun 20 08:28
    sagarkalokhe09 commented #276
  • Jun 11 18:17
    jayphelps edited #695
  • Jun 11 18:16
    jayphelps commented #695
  • Jun 11 17:28
    vvscode commented #695
  • Jun 06 19:38
    hitmands closed #539
  • May 26 21:27
    sotojuan closed #35
  • May 25 17:15
    jayphelps commented #726
  • May 25 17:15

    jayphelps on master

    docs(AddingNewEpicsAsynchronous… (compare)

  • May 25 17:15
    jayphelps closed #726
Jonas Snellinckx
@Superjo149
@Sawtaytoes I cannot really give you a "this better than that" example. But I can show you what I have currently put together. And what I have my concerns about. This is all very much a WIP, so bear with me. As you can see in https://github.com/Superjo149/auryo/blob/63cc86f338bfae137480b4e30d61a4b9d9be4d1e/src/common/store/player/epics.ts#L71, and https://github.com/Superjo149/auryo/blob/63cc86f338bfae137480b4e30d61a4b9d9be4d1e/src/common/store/player/epics.ts#L183 , my Epics are growing quit large. I try to put as much comments as possible, but even then, its not really as clear to read as I want to. I tried here to split it up into a different sub-function, but I'm not convinced this is better. https://github.com/Superjo149/auryo/blob/63cc86f338bfae137480b4e30d61a4b9d9be4d1e/src/common/store/playlist/epics.ts#L533
Might also just be too biased in the way of writing everything since I'm rewriting this from thunk
Kevin Ghadyani
@Sawtaytoes

@Superjo149 Thanks for the code samples. Looking through them now.

Breaking epics up into subfunctions, from the few times I've tried, lead to lots of indirection, and it became difficult to know what code was doing what. Part of the benefit of Redux-Observable is the top-to-bottom pipeline of events.

Redux-Observable is definitely a different way of thinking about things from Thunk; that's for sure. You have to think concurrency rather than sequential execution.

Kevin Ghadyani
@Sawtaytoes

@Superjo149 Looking through your examples, I see what you mean. There's a lot of stuff going on here.

When you request a track to be played, you could fire off a FETCH_TRACK action. Give the action a namespace prop, and in your switchMap, instead of the concat, do action$.pipe(isActionOf(playTrack.fetchTrackSuccess)). You could add a takeUntil(action$.pipe(isActionOf(playTrack.fetchTrackFail))).

That's one way. Might not be what you need though.

Another idea is to break it down into smaller epics.

Here's my thinking, you're doing an action to trigger a track to play, but that action needs to dispatch another action to fetch a track. Depending on if that happens or not, one of 2 other actions would dispatch depending on the results because you might need to display an error to the user or do more processing.

When doing more processing, you could call the same fail action again to notify the user, or call another action to do another set of work. Eventually, something's gonna hit a reducer and play the track.

Kevin Ghadyani
@Sawtaytoes

@Superjo149 Same thing with the playlist. You're not calling a function to get a playlist, you're dispatching an action to start a chain of events. Think of it like a conveyor belt in a video game.

  1. You put a donut on a conveyor belt.
  2. It will first land in the fryer where it will cook one side.
  3. Then it goes through a flipper and the other side fries.
  4. Depending on the type of donut, it will move to the cake or glaze conveyor belt, but it could be under-cooked and move to the trash pile.
  5. If it's a cake donut, it might get chocolate on it, or it might go straight to the customer on another conveyor belt.

This is how you think of things in Redux-Observable terms. You say "I want a donut" and give it some params. The donut goes through a bunch of conveyors and eventually, it will pop out on the conveyor you're looking at (Redux state from Reducer update). It'll either be as you expected or with a note saying it was trashed.

2019-10-10.png

@Superjo149 ^^ I was working on a plugin last year to save browser tabs and made a flow chart of events. I did this using Redux-Observable, but no Redux. Any time I needed to update state, I did so by saving to Chrome's sync storage.

The point is, I called many actions to accomplish a single task. eventually, a JavaScript event was dispatched to the configuration tab.

Brendon
@brendonco
hi @jayphelps how do I do a cleaup with redux-observable
e.g. I have a redux getTodoList action
useEffect(() => {
  dispatch(getTodoList());

  return () => {}
})
Brendon
@brendonco
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Evert Bouw
@evertbouw
I don't think that effect is causing that crash. Can you post more code? A repo or codesandbox preferrably
Michał Lipiński
@falsyvalues
Hi folks, I'm wondering if anyone had similar non trivial case, where one of epics produces sequence of actions like: REQUEST and then REQUEST_SUCCESS / REQUEST_FAILURE from multiple sources. Second epic could handle incorrect results from REQUEST_SUCCESS or retry REQUEST_FAILURE.
With ajax (RxJS) its quite simple, but how to create similar logic where epic and actions are our source of truth, especially things like tracking retries, timeout of whole operation etc?
// Example with ajax
ajax().pipe(
  withLatestFrom(state$),
  tap(([response, state]) => {
    // Some state related logic
    if (!state.ok) {
      throw new Error('Invalid state');
    }
  }),
  // Retry logic
  retryWhen(
    ((delayDuration, maxRetries) => {
      let retriesLeft = maxRetries;

      return (errors) =>
        errors.pipe(
          switchMap((error) =>
            retriesLeft-- > 0
              ? of(error).pipe(delay(delayDuration))
              : throwError(new RetryError())
          )
        );
    })(500, 3)
  ),
  // Whole retry process timeout
  timeout(10000)
);
Kevin Ghadyani
@Sawtaytoes
@falsyvalues You can always listen for action$.pipe(ofType(REQUEST_SUCCESS)), but typically, you'd have another epic handling these cases separately.
Michał Lipiński
@falsyvalues

Hi @Sawtaytoes, yes I started with this kind of apprach

export const retryEpic = (action$, state$) =>
    action$.pipe(
        ofType(REQUEST),
        switchMap(sourceAction =>
            action$.pipe(
                ofType(REQUEST_FAILURE),
                // Replay source action
                map(() => request(sourceAction.payload)),
                delay(500)
            )
        )
    );

but then I realize that tracking number of retries or global timeout will not work like it was in ajax example. Any thoughts?

Kevin Ghadyani
@Sawtaytoes

You can use a scan for retry state. It's a bit more involved to get something like that working because you'll need to create it as part of a retry loop.

You can instead use Redux state to store these retries.

Also, using switchMap here will be problematic right? What happens if 2 things are sending REQUEST and have failures? Only one will fire the retry action. You need mergeMap to do what you want; and then that gets more complicated. My article called "Redux-Observable will solve your state problems" should go over the case where you have more than one area of code trying to share the same epic.

@falsyvalues I think the retry logic can be part of the ajax call. That would be the easiest route. Why were you wanting to move it out again? To record each retry? You could definitely dispatch an action in those cases.

For weird situations like this, you could also make dispatch a dependency. https://itnext.io/the-best-practice-anti-pattern-5e8bd873aadf

Kevin Ghadyani
@Sawtaytoes

@falsyvalues After thinking about it, I might I need more context.

But there are some other things I can suggest as well:
You can mimic Redux-Thunk's orchestration with Redux-Observable. If you need orchestration, just dispatch an action and at the same time, call an async function that returns an observable. Nothing's stopping you from having a single epic as an orchestrator. You don't need to necessarily dispatch an action and listen for a response if that makes sense. But if you have a separate epic executing some code, then yes, either listen for state$ to update or listen for another action$ to come in.

Michał Lipiński
@falsyvalues
@Sawtaytoes Thanks. I have similar thoughts. I've been thinking about keeping the retry in a request object or a store.
Unfortunately, the whole solution is getting more and more complicated with that as you said. The reason for this was the need to duplicate the request logic where we need to handle the retry, but in the long run, this duplication seems to be cheaper than overengineered more universal solution.
Kevin Ghadyani
@Sawtaytoes
@falsyvalues Yeah. It depends on your team and the kind of project you want. Always remember you can create custom operators to assist with these situations as well depending on the number of use cases. Other than that, you're probably on the right track :)
Matheus Castiglioni
@mahenrique94
Hi guys, some example of action with multiple actions dispatch?
I need dispatch two actions when get api response
Kevin Ghadyani
@Sawtaytoes
@mahenrique94 There are a few options. Probably the simplest is using a mergeMap and then returning an of with 2 arguments. Each argument is a different action.
Debayan Paul
@piquantradish_twitter
Hi Guys, I'm trying to use redux observable in my react native app. However the epic is not dispatching its action. I've followed the steps given in the documentation. Can u guys help me?
Kevin Ghadyani
@Sawtaytoes
@piquantradish_twitter please paste your code for the epic ;)
Debayan Paul
@piquantradish_twitter
@Sawtaytoes Day 2 and its working now. Couldn't really figure out why it wasn't working earlier. Thank you for the reply though. Can't wait to get started :)
tim
@glomotion

hello there!
so i'm just trying to wrap my head around how to wrangle many async actions out of a single action stream using redux-observable (alongside redux/toolkit)...

Forewarning, i am admittedly a tiny bit of a rxjs noobie (only been using it for a few months...).

Im trying to work out how to chain multiple actions, both before and after the ajax call below:

export const fetchCardsEpic = (action$, state$) => {
  return action$.pipe(
    ofType(FETCH_CARDS),
    mergeMap((action) => {
      return [
        of(setLoading(true)),
        delay(1000), 
        of(setLoading(false)),
        delay(1000), 
        of(setLoading(true)),
        ajax.getJSON('https://dev.godsunchained.com/proto?format=flat').pipe(
          mergeMap((response) => {
            return [
              cardsReceived(response),
              setVikingTribe(response),
              setLoading(false),
            ];
          })
        ),
      ];
    })
  );
};

(setLoading, cardsReceived and setVikingTribe are normal redux actions)

could someone help me understand how to write the above epic so that it doesn't produce the errors:

A non-serializable value was detected in an action, in the path: `<root>`.
and
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
tim
@glomotion

i was able to get this code running, however:

export const fetchCardsEpic = (action$, state$) => {
  return action$.pipe(
    ofType(FETCH_CARDS),
    mergeMap((action) => {
      return concat(
        of(setLoading(true)),
        ajax.getJSON('https://dev.godsunchained.com/proto?format=flat').pipe(
          switchMap((response) => {
            return concat([
              cardsReceived(response),
              setVikingTribe(response),
              setLoading(false),
            ]);
          })
        ),
      );
    })
  );
};

i was just hoping to figure out how to handle many different actions prior to the endpoint call.

Kevin Ghadyani
@Sawtaytoes
@glomotion of takes multiple args. So where you have of(setLoading), just do more actions in there, or add more in your concat call. If you want multiple to go off at once, use merge instead of concat.
tim
@glomotion
@Sawtaytoes , hey thanks for the reply! Hrmmm so i'm more curious about how to fire off staggered actions. How about if i simplify what i'm trying to do.... would you be able to show me how could i write the following (so that each setLoading action fires, with a short delay in between?
export const fetchCardsEpic = (action$) => {
  return action$.pipe(
    ofType(FETCH_CARDS),
    map(() => setLoading(true)),
    delay(500),
    map(() => setLoading(false)),
    delay(500),
    map(() => setLoading(true)),
  );
};
^ the code above only seems to fire the last setLoading action.
Kevin Ghadyani
@Sawtaytoes

@glomotion You're correct. The last action mapped is going to fall through the pipeline and get dispatched. Calling setLoading only sets the next state of the pipeline to a Redux action object.

To dispatch multiple actions at different times, I need to understand the problem more. Are you wanting to dispatch an action after X seconds or after 2 other actions have dispatched? To do it after X seconds, then put these two in your of: timer(500).pipe(mapTo(setLoading(false))) and timer(1000).pipe(mapTo(setLoading(true)))

@glomotion I'm sure there's something more complicated you're trying to accomplish though.

The most-important thing to understanding is pipeline splitting. You're trying to divide your output into 2 which gets merged by RxJS in the .subscribe. You're not using .subscribe in an Epic, but Redux-Observable does this under-the-hood.

So when you do of(action1, action2), it will dispatch both actions because you're passing one action down, then the other. If you want to have multiple timers set, that requires creating 2 pipelines. One pipeline for the 500 timer and another for the 1000 timer. You can then do:

merge(
  timer(500).pipe(mapTo(setLoading(false))),
  timer(1000).pipe(mapTo(setLoading(true))),
)

I'm betting though, what you really want is to dispatch an action, wait for a response, and then dispatch another action right? Please tell me if I'm wrong. If you want orchestration like that, you're "doing it wrong". If all you're doing is setting a timer like your example, then my solution will work for you.

Anton Kudris
@jodaka

Guys, I'm feeling really stupid but I can't figure out how to do what appears to be a simple thing:
upon firing an action, set loading state to true, then run promise, then set loading state to false.

I've been reading stackoverflow and this chat. Tried a bunch of code examples, but they don't seem to work for me. Most examples from @Sawtaytoes fails with 'You provided function where a stream was expected'. Obviously I'm doing it all wrong, but I can't figure where.

this one is the closest I could wrote. It sets loading state to false once the promise is fulfilled, but I couldn't make it set loading to true, before running Promise.all. Tried all sorts of 'concat', 'merge' but failed.

const getMyTemplatesEpic = (action$) =>
  action$.pipe(
    ofType(TEMPLATES_ACTIONS.GET_TEMPLATES),
    switchMap(() => Promise.all([API.getTemplates(), API.getProject(), API.getGroups(), API.getSharedTemplates()])),
    map(([my, project, group, shared]) =>
      getTemplatesSuccess({
        my,
        businessCases: project.templates.concat(group),
        shared
      })
    ),
    catchError((err) => of(getTemplatesError(err))),
    switchMap(() => of(templatesLoading(false)))
  )
Evert Bouw
@evertbouw
@glomotion your example could be written as
export const fetchCardsEpic = (action$) => {
  return action$.pipe(
    ofType(FETCH_CARDS),
    mergeMap(() => merge(
        setLoading(true).pipe(delay(500)),
        setLoading(false).pipe(delay(1000)),
        setLoading(true).pipe(delay(1500)),
    ))
  );
};
Anton Kudris
@jodaka
I figured out most of my problems comes from the fact that I imported concat and merge from rxjs/operators. Once I fixed imports, things started to work as expected.
Evert Bouw
@evertbouw
@jodaka I'd recommend keeping more operators inside the switchMap subroutine, putting catchError in the root of the epic will replace the epic itself in case it errors
2 replies
where you only want to provide a fallback for the subroutine
this should work
const getMyTemplatesEpic = (action$) =>
  action$.pipe(
    ofType(TEMPLATES_ACTIONS.GET_TEMPLATES),
    switchMap(() =>
      from(
        Promise.all([
          API.getTemplates(),
          API.getProject(),
          API.getGroups(),
          API.getSharedTemplates(),
        ])
      ).pipe(
        startWith(templatesLoading(true)), // we can fire this action when the subroutine starts
        map(([my, project, group, shared]) =>
          getTemplatesSuccess({
            my,
            businessCases: project.templates.concat(group),
            shared,
          })
        ),
        catchError((err) => of(getTemplatesError(err))),
        endWith(templatesLoading(false)),  // and end with this one
      )
    )
  );
3 replies
tim
@glomotion
@evertbouw thank you! that code really helped me get it. :) I think i need to read more / do more tutorials on rxjs.
@evertbouw your suggestion can't compile - i cant pipe simple actions, because they don't return observables.
Kevin Ghadyani
@Sawtaytoes

@glomotion startWith, in that example, happens when the switchMap triggers.

It will go startWith and then map the values from it. If you need to have it fire without going to the map, then you need to put startWith where there's endWith at the end of the pipeline.

Glad you got some stuff working by importing concat and merge as observables instead of as operators!

Davit Barbakadze
@jayarjo_twitter
why is it necessary to call run (e.g. epicMiddleware.run(rootEpic))? what happens if we do not call it? I think it should be explained in docs, since it's not obvious
Davit Barbakadze
@jayarjo_twitter
ok I see now - so that's basically what adds project specific epics to the party, but why not simply pass them in createEpicMiddleware() ?
Evert Bouw
@evertbouw
startWith/endWith is basically the same as doing concat,
concat(
    of(a),
    b,
    of(c),
)

b.pipe(
    startWith(a),
    endWith(c),
)
Kevin Ghadyani
@Sawtaytoes
@jayarjo_twitter no clue. I believe that was changed in Redux-Observable v1, but only @jayphelps would know.
Evert Bouw
@evertbouw
in order to make state$ work, the epics have to start after the store is initialized so they have a proper initial state
Evert Bouw
@evertbouw
hmm the initial state was related but not directly why there is a .run, from the change log:
BREAKING CHANGE:

You must now provide your rootEpic to epicMiddleware.run(rootEpic) instead of passing it to createEpicMiddleware. This fixes issues with redux v4 where it's no longer allowed to dispatch actions while middleware is still being setup. Redux v4 support will be coming in a follow up PR.
Kevin Ghadyani
@Sawtaytoes
@jayarjo_twitter ^^ There's your answer