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

Discussion: declarative side effects and data fetching approaches #349

Closed
markerikson opened this issue Feb 10, 2020 · 47 comments
Closed

Discussion: declarative side effects and data fetching approaches #349

markerikson opened this issue Feb 10, 2020 · 47 comments
Labels
discussion enhancement New feature or request

Comments

@markerikson
Copy link
Collaborator

Currently, Redux Toolkit adds thunks as the default supported approach for any kind of async / side effect behavior, largely because it's the simplest possible approach that works, and because it's also the most widely used Redux async middleware.

That's a large part of why I'm considering adding some kind of a createAsyncThunk API to abstract common data fetching and action dispatching logic. It matches what we already have built in to RTK, and what many Redux users are already doing.

@davidkpiano has strongly argued that side effects should be declarative, instead, possibly along the lines of the redux-loop store enhancer.

I'm open to discussing possible APIs and approaches here. My initial point of concern is that this is a very different kind of API than most Redux users are used to, and it would add an extra level of teaching and documentation to explain.

Discuss!

@davidkpiano
Copy link

davidkpiano commented Feb 10, 2020

My initial point of concern is that this is a very different kind of API than most Redux users are used to

If you had asked developers what they wanted, they would have said they want to easily mutate state and call side-effects whenever and wherever they want.

“If I had asked people what they wanted, they would have said faster horses.”

EDIT: I'm looking over this and will provide my thoughts soon. Here's another source of inspiration: https://redux-saga.js.org/docs/basics/DeclarativeEffects.html

@oleksii-demedetskyi
Copy link

I have tried Declarative Side-Effects many times and always fail to scale them. Whole system is become too fragile to accept changes.

I have found that Data-Driven Side-Effects works instead. For the short:

  1. Reducers update the state based on actions.
  2. Side effect interpreter perform side effects based on the state.
  3. That's it.

In some sense UI is just another side effect.

@markerikson
Copy link
Collaborator Author

@AlexeyDemedetskiy : I think that's literally what David means by "declarative" here:

// reducer:
case 'FETCH':
  return {
    ...state,

    // finite state
    status: 'loading',

    // actions (effects) to execute
    actions: [
      { type: 'fetchUser', id: 42 }
    ]
  }

// UI:
const { actions } = state;

useEffect(() => {
  actions.forEach(action => {
    if (action.type === 'fetchUser') {
      fetch(`/api/user/${action.id}`)
        .then(res => res.json())
        .then(data => {
           dispatch({ type: 'RESOLVE', user: data });
        })
    }
    // ... etc. for other action implementations
  });
}, [actions]);

@davidkpiano
Copy link

@AlexeyDemedetskiy This is insufficient. There are use-cases when a side-effect needs to be executed without the state changing.

@oleksii-demedetskyi
Copy link

@davidkpiano I never met these use-cases, so your experiences can be different.
@markerikson I have misread then. My bad, sorry. However, usually I just have separated reducers for storing required actions per domain. Usually these domains are - db access, network, gps, routing.

@phryneas
Copy link
Member

@davidkpiano I'm a bit tired so I've just skimmed this (will give it a proper reading tomorrow), but I'm already seeing some problems with the approach you are suggesting, maybe you can already correct me on them if I read something wrong? :)

  • This seems like it only works with a local reducer state. If this is one global state and multiple instances of the same component are listening to it, all those components will start the fetch. If it were only one central component that handled all async stuff I wouldn't really see the difference to one central middleware.
  • I don't see how this approach allows for cancelling, which you are (rightfully) critiqueing with thunks
  • Adding additional actions will re-trigger the previous ones if they haven't been removed yet. Only always returning one action might lead to missed actions, if multiple dispatches are batched
  • If the component that would execute the fetch would be dismounted before the fetch is executed, you would end up with half-baked asynchronous state somehow.

I am sure that some of these things can be addressed by additional safeguards in an actual implementation, but that would get quite complicated and potentially very long.
A lot more complicated than for example writing a saga, which many people already consider overkill for many use cases.

I think the idea of declarative side effects is quite appealing, but from the examples in the blog posts, I see more problems than solutions right now. Especially binding the execution of these async actions into component lifecycle seems a bit weird to me. And I wouldn't really know how we could teach this without completely confusing people.

Could you provide one example implementation that does not have the problems I described so that we can see if this is feasible?

@neurosnap
Copy link

neurosnap commented Feb 27, 2020

I've spent a lot of time thinking about and writing code using concepts like side-effects as data, so I'm very excited to see redux-toolkit contributors are discussing it!

If you want a battle-tested example of writing declarative side-effects in the redux ecosystem, redux-saga should be the primary source of inspiration. re-frame is also another great resource that emphasizes side-effects as data.

@markerikson and I have already had plenty of discussions about redux-saga and why it's hard to nominate it for official support. Personally, generators are so incredibly useful for async flow control that it's worth the struggle to onboard developers to the concept. There are definitely downsides but I've built many large scale web applications using redux-saga and I would need a very compelling reason to use something else for managing side-effects, even for moderately-sized web apps.

Having said that, the arguments against it are still valid and I have spent some time trying to figure out how to solve those shortcomings.

Here are some libraries that I've built to try to make adoption easier. I lend these to you all as possible inspiration.

cofx is detached from redux but shares a similar API to redux-saga. Building on top of that, redux-cofx is a hybrid between redux-thunk and redux-saga. The benefit is there are no watchers, they are used like redux-thunk but instead of the middleware calling a simple function, it will also properly handle executing generators. Because cofx is separated from redux it's also easy to create a hook using a very similar API, which is not straightforward with redux-saga.

Please forgive me for citing my own projects, but I do feel like they are relevant and possible sources of inspiration to discuss.

@phryneas
Copy link
Member

This unfortunately doesn't solve the main gripe I have with saga (which I - in theory - love).
It's not typesafe. You cannot have TS infer the return type of yield based on what you yielded.
And until TS gets better support (and I have no idea how that would look) for generator function typings, that won't change.

It's unfortunate, but as it currently stands, I think the best way to go is something promise/async-based.

@neurosnap
Copy link

Totally agree, typescript is the worst part about using generators for async flow control, no argument there. I find the compromise acceptable, but others might not.

@bdwain
Copy link

bdwain commented May 14, 2020

@phryneas @neurosnap redux-loop provides declarative side effects (without generators) and we've been improving the type safety a lot lately. We have an upcoming major release that should fix the remaining issues with it. While I am a biased maintainer, I have used it for a large application for 4 years now and find it works very well.

@martpie
Copy link

martpie commented Jun 9, 2020

Maybe I can share a little bit a real-world example I am trying to tackle right now for some inspiration (you are all thinking this problem much better than me).

  • I have a list of products in a cart
  • I have a list of discounts, depending on multiple factors, let's say:
    • the number of products
    • the combination of some products
    • the birth date of the user
    • the zip code of the user

Any time one of this parameter changes, I need to re-fetch the discounts (from a specific API endpoint)

Solution 1: call refreshDiscounts from components (componentDidUpdate, events handlers...)

  • dev error prone
  • not centralized
  • some edgecases may be missed
  • false positives

Solution 2: call refreshDiscounts from thunks

  • it works, and is (kind of) centralized
  • some advanced use-cases may be hard to tackle (data aggregation from multiple slices)

Solution 3: subscribe to store changes via redux-on-state-change

this is my current solution, but it is hard to test, and a bit dirty:

  • you can do advanced comparisons/aggregate data from multiple stores to see if you need to refresh the discounts
  • performance impact

Others

  • redux-saga (heavy, steep learning curve)
  • redux-loop (a good in between, but same issues as thunks)
  • etc

I think this is a particularly common scenario, does anyone have suggestions/best practices/implementations to share?

@phryneas
Copy link
Member

phryneas commented Jun 9, 2020

@martpie I'd dispatch a thunk from a component - the thunk itself would decide if there is actually a need of refreshing some things, depending on when the last refresh was etc. (probably overridable from a thunk argument, so that lifecycle refresh could be handled differently from "the user pressed a refresh button" refresh)

I don't really see why your "aggregation of data" there would be more complicated than in any other solution (given that thunks have access to the full state), but I believe that those values might actually be derived values - and those should never be persisted in the store, but rather be calculated in a (possibly memoized) selector.
The store should probably contain discount percentages, but not the finalized prices.

@thirafidide
Copy link

@martpie In case of redux-loop, you can make an action of REFRESH_DISCOUNT which only run API and doesn't change state

case 'REFRESH_DISCOUNT':
  return loop(state, Cmd.run(refreshDiscount, {
    args: [getArgs(state, action)]
  });

then whichever action that would trigger the refresh should call the action via Cmd.action

case 'ADD_PRODUCT':
  const newState = addProduct(state, action.product);
  return loop(newState, Cmd.action({ type: 'REFRESH_DISCOUNT'; additionalParams: ... }))

So you can refresh the discount from any slices. If you put the handler of REFRESH_DISCOUNT on reducer high enough so it has access to any necessary state, I think there is no problem with data aggregation

@bdwain
Copy link

bdwain commented Jun 9, 2020

You can also pass getState (and dispatch) to the refreshDiscount method if your reducer does not have access to the data that the function needs on its own.

case 'REFRESH_DISCOUNT':
  return loop(state, Cmd.run(refreshDiscount, {
    args: [whatever, whatever2, Cmd.getState, Cmd.dispatch]
  });

//refresh-discount.js
function refreshDiscount(whatever, whatever2, getState, dispatch){
  const myPieceOfState = getState().my.piece.of.state;
  dispatch({type: 'hello'});
}

@nathggns
Copy link

How sure are we that "declarative effects" are what we want? I thought it was, but the more I think about it, the more I realise I want first class support for data driven effects — React isn't just declarative, it's data driven — to update the view you update the data.

I'm not sure that "effects are data" is the solution. In this case, it would still be possible to end up in an illegal state, where you've updated the state to a position that requires a side effect, but you don't also manually add the side effects the state.

I think the quite widespread misuse of React's useEffect to create data driven side effects (i.e, dispatch this action when this state changes and this condition is true) shows that there is appetite for this.

I would love to be able to set my state anywhere in my reducer, and the related side effects to just work.

I like the idea that React UI is just another data driven side effect. However, React redux currently implements this via a store subscriber, which has a shortcoming, which is that it's very difficult to implement side effects that depend on order of execution. I.e, I need to perform a side effect before react redux renders with this new state, or I need to perform one after. Scheduling of side effects vs react redux definitely needs to be part of the discussion. Currently redux just runs all store subscribers in order of attachment to the store- I don't think this is sufficient.

@nathggns
Copy link

Just to add to my above comment, the reason why I consider the use of useEffect for one time side effects abuse is because it's very difficult with the api of useEffect to predict how frequently an effect will run, especially if you need to use multiple distinct pieces of data in your effect but then only want the effect to fire when some of those change, not all.

Because of this it's generally best practice I believe to make your React effects resilient to overfiring- I consider it most useful for syncing data between React's declarative model and other imperative APIs you may need to use. I don't think it's actually a good api for firing one time data driven side effects, and I don't think the React team do either,

@nordfjord
Copy link

nordfjord commented Sep 3, 2020

I don't feel particularly qualified, but I'd like to drop my 2 cents.

I don't think redux-loop fits well with the current status of RTK. Does returning a loop(state, cmd) even work with immer?

So what if we had an RTK native way to "queue effects" what might that took like

Here's a half thought out idea: an effects object baked into createSlice.

const slice = createSlice({
  name: 'user',
  initialState: {status: 'idle'},
  reducers: {
    gotUser(state, action) {
     // ...
    },
    getUser(state, action) {
      // ...
      
      slice.effects.fetchUser()
    },
    getUserFromInput(state, action) {
      // ...
     
      slice.effects.handleInput(action.payload)
    }
  },
  effects: {
    async fetchUser() {
      const user = await fetchUser()
      return slice.actions.gotUser(user)
    },
    async handleInput(input, canceled) {
      await wait(300)
      if (canceled()) return
      const user = await fetchUser(input)
      return slice.actions.gotUser(user)
    }
  }
})

The effects object is a Record<string, (...args: any[])=> Action | Promise<Action>>

calling slice.effects[x] does not immediately call the function defined below, it instead puts the effect on an effect queue which is processed after the reducer is done.

If the same effect function is called while still being processed it's automatically canceled.

POC Here

I think this works only because it's baked into createSlice, it wouldn't work as a traditional middleware.

I think the benefits of this approach are:

  1. It feels pretty natural, "oh I need an effect, I'll just call it right here."
  2. Gives some benefits over thunks (like cancellation) without needing too much knowledge
  3. Colocates state, actions, and effects

And to be clear I'm not suggesting this as an API.

I'm just trying to broaden the discussion beyond "What middleware is best," because with toolkit there's an opportunity to use createReducer/createSlice to bake effects into the mix.

@RobIsHere
Copy link

By using callbacks(hell) and promise-chains the javascript runtime is now comparable to windows 3.x that did cooperative multitasking (the program is giving back control to the os after a chunk of work like the promise function would give back control to the js runtime). At this operating system level it's absolutely necessary to handle parallelism and concurrency. Even single-threaded, it's just easier because your functions are atomic operations.

By using redux thunk you are basically just sending a prayer to heaven: "don't let any race condition happen!". Let me explain it by a very very common requirement, that should be ridicolously simple: I'm pretty sure that it's very very hard to write a correct login/logout flow without race conditions with thunks.

E.g. consider login attempts that are failing after the user has logged out because the sendLogout-call is hanging until network timeout for 30 or 60 seconds.
You have to consider 2 thunks (sendLogin, sendLogout) with their success and some different error states that can each be fired again before the last one completed. And the two calls must be controlled and synchronized.

With Thunks alone to code a somehow working login/logout flow you have to ignore race conditions. You have to ignore that the mobile network could be very slow. You have to ignore that results of login/logout/retry login because of lagging networks could come back in different order. If you do not ignore these things, the example gets very complicated because of the necessary thunks micromanagement.

So to sum it up: redux-thunk is hardly solving any real-world problem. It's just for hobby projects or people who don't recognize their programming errors. Redux-saga is solving a big pain point (handling parallelism and concurrency without getting insane), that has always been a pain in computer science. Therefore it should be in IMHO.

@markerikson
Copy link
Collaborator Author

@RobIsHere FWIW, I agree that thunks have limits in how they can interact with data flow. However, I do not intend to make sagas a default in RTK for all the reasons listed here:

https://blog.isquaredsoftware.com/2020/02/blogged-answers-why-redux-toolkit-uses-thunks-for-async-logic/

@allforabit
Copy link

How about augmenting a thunk with some basic cancellation similar to how useEffect does it? Maybe dispatch could return a cancellation fn and if it's called it won't fire the thunk callback. I'm not sure how feasible returning a fn from dispatch would be though!

@nordfjord
Copy link

nordfjord commented Nov 6, 2020 via email

@markerikson
Copy link
Collaborator Author

Yep, createAsyncThunk already has built-in support for cancellation via an AbortController. You can then do:

function MyComponent(props: { userId: string }) {
  const dispatch = useAppDispatch()
  React.useEffect(() => {
    // Dispatching the thunk returns a promise
    const promise = dispatch(fetchUserById(props.userId))
    return () => {
      // `createAsyncThunk` attaches an `abort()` method to the promise
      promise.abort()
    }
  }, [props.userId])
}

That's certainly not as flexible as sagas or observables, and I'm not at all arguing it's a "best" or "final" approach, but it is more than you would typically get with a hand-written thunk.

@megagon
Copy link

megagon commented Feb 26, 2021

Yep, createAsyncThunk already has built-in support for cancellation via an AbortController. You can then do:

function MyComponent(props: { userId: string }) {
  const dispatch = useAppDispatch()
  React.useEffect(() => {
    // Dispatching the thunk returns a promise
    const promise = dispatch(fetchUserById(props.userId))
    return () => {
      // `createAsyncThunk` attaches an `abort()` method to the promise
      promise.abort()
    }
  }, [props.userId])
}

That's certainly not as flexible as sagas or observables, and I'm not at all arguing it's a "best" or "final" approach, but it is more than you would typically get with a hand-written thunk.

based on your answer, can I propose some help from my side? I'm ready do this by myself, because that's the only one thing that I miss in RTK right now.
What I want to see in RTK: I want to have something that will allow me to cancel request from any other parts of my application, it seems like in current implementation we can do it only from the element that initiated that request (at least without dirty hacks as storing promises in redux)
Here is my proposal:
dispatch(fetchUserById(props.userId))
will store promise inside it self in a stack.
fetchUserById (any createAsyncThunk) will be extended to have some operations to work with requests, eg

fetchUserById.getAllPendingRequests() => [{id: promise}]
fetchUserById.cancelAllRequests()
fetchUserById.cancelPrevious()
fetchUserById.cancelById()
... ?

@markerikson can you please let me know you think about this approach?

@markerikson
Copy link
Collaborator Author

@megagon That doesn't seem like something that would go inside of the thunks themselves, but rather in a middleware of some kind. I'd suggest playing around with writing a custom middleware first that does this sort of thing and see where that goes.

@megagon
Copy link

megagon commented Feb 26, 2021

@megagon That doesn't seem like something that would go inside of the thunks themselves, but rather in a middleware of some kind. I'd suggest playing around with writing a custom middleware first that does this sort of thing and see where that goes.

Do you suggest just to write custom middleware internally for us? Or do you mean to make a middleware that will be delivered with all others redux's thunks?
I wanted to have something better inside RTK, because current approach with aborting promises is hard to use.

@markerikson
Copy link
Collaborator Author

I'm saying that A) I think the logic of "tracking what promises exist" seems more suited for a custom middleware, and B) I'd like to see a working example in action to motivate further discussion.

@fma1
Copy link

fma1 commented Mar 15, 2021

@RobIsHere
It's true that redux-saga and redux-observable solve login/logout better.
But I'm looking at Mark's post from February about RTK Toolkit defaulting to Thunks, and I would say that Thunks are more well-known and used more. And as Eric says, you can add any middleware you want with configureStore().

I'm thinking about the list of requirements for redux-observable, and I'd say the list is somewhat like this:

  • Understand Observables and rxjs operators like merge, concat, etc.
  • Make sure you call epicMiddleware.run(rootEpic)
  • Write extra action types just to kick off the actual epic logic

@megagon
You might want to look at redux-saga or redux-observable. In redux-observable, cancellation usually consists of dispatching a Redux action which is given to the takeUntil() operator:
https://redux-observable.js.org/docs/recipes/Cancellation.html

@RobIsHere
Copy link

I think a solution would be to hide saga behind some Interface. Then make ready-made building blocks like LEGO.

There could be a

  • Loginflow
  • Jobsflow ( Thunksflow )
  • ApiJobsFlow
  • ParallelJobsFlow
  • SerialJobsFlow
  • CancellableJobsFlow
  • ...and so on

All composable and backed by sagas that execute unknown to the beginner-user. A pro-user could reuse the building blocks in his sagas. A beginner-user just composes some function calls and runs them and waits for a promise resolution/cancellation while he is totally unaware of the saga(s) working in background.

That is my way of doing it. But I don't need this baked into redux-toolkit. Although I'm astonished a little bit because writing a new middleware is also like writing a saga but a lot harder. So where should this lead if everyone writes a new middleware for every problem he has?

(reactive things like rxjs is something I do not use because of testability, code quality and maintainability)

@markerikson
Copy link
Collaborator Author

So at this point it feels like the "declarative data fetching" thing is going to be covered by RTK Query:

https://rtk-query-docs.netlify.app

Obviously when it does come out no one will have adopted it yet, and like any other new API, it will take a very long time to get picked up. But, in a sense we could say that it does "solve" the data fetching use case, and that's certainly the majority of the reason for doing async work with Redux.

I'm still open to further discussions on this topic, but at that point it feels like there's less need for us to build in anything effects-wise.

My gut says redux-loop is an interesting idea conceptually, but I'm concerned by the lack of overall adoption in the ecosystem historically, as well as the lack of info on using it with TS. We're not building in sagas for the reasons I linked to above (plus TS concerns), and observables also feel to heavyweight at this point.

Maybe I'm being too dismissive of the options, but there's also a lot of inertia in the ecosystem at this point and I have to take that into account.

@KurtGokhan
Copy link

I will throw in some thoughts, but warn me if this is not the correct place. I am a pretty noob Redux user.

This discussion has gone mostly about the reducer side of things but I find the "consumer" side to be a harder concept. For the reducer side, I had written something very much like RTK Query and it is working for me so far. But there are some cases which it does not handle well. Suppose the following use case:

// section of Login page component
const { data, error, isLoading } = useSelectLoginResult();

useEffect(() => {
  if(error) showSnackbarForFiveSeconds(error);
}, [error]);

const onFormSubmit = () => dispatch(doLoginRequest(username, password));

This will briefly show a snackbar when there is an error in the login request. (I abstracted showSnackbarForFiveSeconds but suppose there is some declarative logic there, not just imperative)

The first problem with this is, when this component is unloaded and loaded again, the snackbar will show again. Because error will still be in state. To overcome this, I reset the error or the whole result when the component is unloaded. But even that is not good because multiple components can be using this state.

The second problem is, when login request is done again, and the error is the same, the snackbar will not appear. Because useEffect will run only when error changes. I could reset the error before doing each request. But it feels like a hack.

Maybe because I am coming from an OOP and event-driven background but an event feels more right in this particular case. Something like this should be easy to write:

// Reducer side
const loginRequestThunk = () => async (dispatch, getState, dispatchEffect) => {
  try { 
    await fetch(/* ... */);
    // dispatch etc.
  } catch(error) { 
    dispatchEffect('loginError', error); 
  }
}

// Component side
useDispatchedEffect('loginError', (error) => showSnackbarForFiveSeconds(error));

But do you think would go well with the principles of Redux?

@markerikson
Copy link
Collaborator Author

Yeah, that sort of "one-off trigger" behavior has always been tricky with React and Redux in general, specifically because it doesn't entirely mesh well with the notion of "UI = f(state)".

Hypothetically, one option would be to not actually trigger the toast itself based on the existence of error, but rather to have another reducer listen for the "request failed" action and add an object describing the toast to an array. Then, have a <ToastMessages> component that reads that array of toast descriptions, renders them, and dispatches an action after N seconds to remove each one. (The basic technique here is what I described in my post Practical Redux, Part 10: Managing Modals and Dialogs.)

I'm not saying this is the best way to handle this or the only way to handle this - it's just the first thing that pops into my head reading your comment.

I'm actually kind of curious how XState handles this sort of scenario. Don't want to totally derail the thread, but if @davidkpiano would like to drop a comment describing that approach I'd be interested.

@davidkpiano
Copy link

davidkpiano commented Apr 14, 2021

I'm actually kind of curious how XState handles this sort of scenario. Don't want to totally derail the thread, but if @davidkpiano would like to drop a comment describing that approach I'd be interested.

This is one of the biggest pitfalls of effectful logic in React in general - it's based on what changed, instead of based on events and declarative effects.

State machines handle when effects happen explicitly, and they always happen due to an event (on transitions). To make organization easier, statecharts define the 3 areas where effects can be defined:

  • On entry to a state (all transitions that enter a state)
  • On exit from a state (all transitions that exit a state)
  • On transition

Considering the above problem, we can quickly sketch a (partial) state machine that looks like this:

Screen Shot 2021-04-14 at 4 29 13 PM

XState takes the state-machine approach of (state, event) => (nextState, effects?), so it's not just returning the next state, but also a declarative description of "effects" (XState calls these actions, didn't want to confuse) that should be executed. These effects are fire-and-forget, so they're discarded when calculating the next state each time (this is the same idea as a finite-state transducer, if curious).

So, the resulting event-state pairs might look like this:

  1. init -> ('loading', [])
  2. FAILURE -> ('error', ['showAlert'])
  3. ANYTHING_ELSE -> ('error', []) (remember: effects are discarded)

So, because 'showAlert' is an effect that is only executed on the transition between loading and error, it will only be executed once, which is what you would expect. No hacks!

To emulate this, you need to somehow represent this "transition", so the useEffect code would need to be refactored to look something like this:

// pseudocode!!
useEffect(() => {
  if(error && previousStateWas(loading)) {
    showSnackbarForFiveSeconds(error);
  }
}, [error]);

In Redux (with reducers), you can represent these declarative effects with a tuple:

// Notice how effects from previous state are discarded each time
const reducer = ([ justTheState, ____ ], action) => {
  // ...
  return [nextState, effects];
}

In XState, the above pattern just has a bit of structure around it:

// ...
on: {
  FAILURE: {
    target: 'error',
    actions: (_, event) => showSnackbarForFiveSeconds(event.error)
  }
}

@markerikson
Copy link
Collaborator Author

Yeah, I thought it was likely something along those lines. Thanks for the clarification!

@asherccohen
Copy link

Why not repurpose this thread to "finite declarative transitions and effect in redux"?

No pun here, and no competition with #xstate or the amazing work @davidkpiano is doing with it, but there's a big potential here.

Given the popularity of redux (especially in enterprise and large teams) and how projects based on it can benefit from refactoring to rtk-toolkit, it wouldn't be a far fetch to envision an API to write finite-state-machines and declarative effects with it.

What is currently missing and what are the limitations?

@markerikson
Copy link
Collaborator Author

A user did open up #1065 a few days ago to discuss specifically adding some kind of state-machine-related util to our API, so there's overlap with this discussion.

@asherccohen
Copy link

Awesome!will move to that ticket then, thanks @markerikson!

@RobIsHere
Copy link

Please make this stuff a peer-project that can be installed only if desired. So devs doing things like these in saga can continue to use redux toolkit without forking only to get the superflous stuff out.

Things like #1065 are a piece of cake in saga, less than an hour of work, and everyone using saga already has an implementation like this.

@davidkpiano
Copy link

Things like #1065 are a piece of cake in saga, less than an hour of work, and everyone using saga already has an implementation like this.

@RobIsHere Can you please show a code example (gist, etc.) of such an implementation?

@phryneas
Copy link
Member

@RobIsHere something like that would probably be tree-shakeable or if it became too big (which sounds unlikely), maybe it's own entry point.

But if we decided that we add it to RTK, it would not be it's own package.

I assume that before people start forking things wildly to shave off that last kilobyte, they probably just turn on tree shaking in their bundler, with much higher gains over all their dependencies.

@markerikson markerikson added discussion enhancement New feature or request labels Jun 7, 2021
@janoist1
Copy link

I love using sagas. I also love redux-toolkit. So I've created a package that glues these 2 awesome things together: saga-toolkit.
The main idea is to resolve async thunk actions by sagas (takeEveryAsync, takeLatestAsync, takeAggregateAsync).
You can also dispatch async actions in sagas and wait for them (putAsync).
I encourage everyone to try it out and send me feedback. 👍

@markerikson
Copy link
Collaborator Author

Reviving this thread a bit:

I've been working on fleshing out a new "action listener middleware" that would allow running callbacks in response to specific actions (or potentially state changes).

At the moment, I think we can actually replicate a decent subset of what you can do with sagas, like takeEvery and takeLeading. But, anything that involves cancelation (takeLatest, fork, hierarchical tasks) is much harder, because that really requires generator functions rather than async/await.

I just did some brainstorming on the difficulties of cancelation and some potentially interesting looking bits to investigate at #1648 (comment) .

I'm very curious how much overlap there is between the current concept for that listener middleware, and this discussion - particularly things like longer-running workflows, error handling, and cancelation.

On the one hand, we don't actually want to turn this into a full-blown attempt to rebuild sagas/observables with a homegrown equivalent. On the other hand, if we solve some of the same use cases by adding a few specific primitives and getting some decent bang for the buck in terms of API complexity and bundle size, that could be useful.

Anyone in this thread have thoughts on potentially useful capabilities, overlap with things like state machines, desired side effects approaches, etc?

Like, if someone were to pop up and say "here's how to do exactly the same things this prototype middleware does with XState or a similar FSM lib plugged into Redux", that would be a valuable piece of information ( paging @davidkpiano ), as would "this middleware is going in the wrong direction and here's a better alternative approach".

@markerikson
Copy link
Collaborator Author

This thread has been sitting around for a while. Lemme recap where things stand today:

@zoubingwu
Copy link

@markerikson So the createListenerMiddleware now can actually completely replace createAsyncThunk, right? Will you guys considering deprecate this api someday?

@phryneas
Copy link
Member

@zoubingwu those two apis complement each other, they don't replace each other. They have completely different mindsets and make sense in different situations for different use cases.

None of them will be deprecated for the other.

@zoubingwu
Copy link

zoubingwu commented Jun 11, 2022

@phryneas OK, I just played with createListenerMiddleware a little bit and couldn't find when I should use createAsyncThunk over createListenerMiddleware 😂 the latter seems much more powerful. And I like writing all side effects using just one same pattern to make code easy to read and understand.

@phryneas
Copy link
Member

@zoubingwu createAsyncThunk is for "doing a thing, now, explicitly, and also dispatch some actions" and the listener is for "if that action is dispatched, do those side effects".

A thunk is much more "direct and compact" - you dispatch the order to do something, and actions will be dispatched as a side effect (at least if using createAsyncThunk, using only thunks there might even not be actions at all).

The listener middleware is "hey, if this (related or completely unrelated) things happens, also do this.

@markerikson
Copy link
Collaborator Author

I don't think we've got a lot more to discuss here, so I'm going to go ahead and close this issue.

Happy to discuss more if anyone does have further ideas for improvements!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion enhancement New feature or request
Projects
None yet
Development

No branches or pull requests