Redux-toolkit: Discussion: declarative side effects and data fetching approaches

Created on 10 Feb 2020  ·  39Comments  ·  Source: reduxjs/redux-toolkit

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!

discussion enhancement

Most helpful comment

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.

All 39 comments

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

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.

@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]);

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

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

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

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.

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

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.

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.

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

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?

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

@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

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

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.

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,

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.

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.

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

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!

I think you should look at createAsyncThunk. I believe that has cancellation

On Fri, Nov 6, 2020 at 5:37 PM allforabit notifications@github.com wrote:

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!


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/reduxjs/redux-toolkit/issues/349#issuecomment-723331047,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AA4MSMPJBHQCHPEHR4V4ANDSOR3C7ANCNFSM4KSQCWIA
.

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.

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?

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

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.

@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

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)

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.

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?

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.

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

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

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?

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.

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

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.

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?

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Izhaki picture Izhaki  ·  3Comments

ouweiya picture ouweiya  ·  3Comments

denu5 picture denu5  ·  4Comments

cole-robertson picture cole-robertson  ·  3Comments

SoYoung210 picture SoYoung210  ·  4Comments