Hacker News new | ask | show | jobs
by lf-non 2151 days ago
I'd recommend caution when adopting redux-toolkit in large & rapidly-evolving projects.

I recently (2 months back) moved a medium sized project (~240 branch reducers, ~600 actions) away from redux-toolkit.

My primary complaint is that the recommended setup doesn't work well with changing requirements where we often have to move away from branch-local state handling to something that needs access to wider state.

We started out with a number of slices and our reducer logic was local to these slices and used only the state within that slice. However as our application evolved, for processing a number of these actions (which were previously slice-local) we needed access to state from other branches. So now we had a couple of options:

A. Dispatch thunks instead of actions: This gets ugly real fast because now your action handling logic is split across thunks & reducers, and is hard to follow. This also needs refactoring across every dispatch site.

B. Use something like redux sagas to intercept actions: We found this to be "too" flexible and felt that we were better-off without the entire machinery of spawning sagas on the fly. We wanted it to be easy to reason about what happens when an action is dispatched looking at the code without having to debug what all sagas could be running at that particular point of time.

C. Move the action handling higher up: requires extensively refactoring the reducers.

The solution we settled on was redux-loop [1]: A port of elm's effect system to redux. This was neat because we could easily convert the reducer to return loops instead of states, and thereby easily get access to full state and dispatch while retaining the ability to follow through the complete action handling flow from a single starting point that didn't change.

TypeScript support in redux-toolkit is also kind'a bolted on and users are recommended different approaches when they care about type-safety. It proved to be a pain to communicate junior devs multiple times that you should use leave the reducers as empty object and instead use "extraReducers" with builder API.

We found using immer[2] (for managing immutable state) and unionize[3] (for handling discriminated union of action types) directly to be a much better solution than redux-toolkit's abstractions.

[1]: https://github.com/redux-loop/redux-loop

[2]: https://github.com/immerjs/immer

[3]: https://www.npmjs.com/package/unionize

1 comments

RTK doesn't change anything about how you write reducers and actions that need to interact across slices. Our recommendations have always been the same: reorganize slice reducers so they handle more state, put more data in actions, or coordinate via side effects [0].

The only thing that changes with RTK is that we now recommend using the single-file "slice/ducks" pattern for a given feature's Redux logic, and RTK's `createSlice` API makes it easier to write code that's organized that way. If you do have cross-slice dependencies, where slice A and B both want to respond to each other's actions, that _could_ potentially lead to cyclic dependency issues. Our RTK Usage Guide page specifically addresses that question [1], and resolving it is generally a matter of defining the relevant actions in a separate file again. But, having logic in a single file by default drastically simplifies things in most cases. This issue isn't unique to RTK - it exists any time you're trying to have different slices depend on each other, regardless of how the logic in those files are implemented.

Also strongly disagree that RTK's TS support is "bolted on". RTK is written in TS, and we've spent hundreds of hours trying to ensure our APIs work well with TS [2]. We test against multiple TS versions, design our APIs to minimize the amount of types you have to declare, and try to offer the best type safety possible with our preferred API structure.

The only time you would ever define `createSlice.extraReducers` as an empty object is if you are _only_ using that slice as a data cache and not adding any additional client-side logic that would manipulate that cached data. In that case, you'd probably be better suited to use `createReducer` directly.

Having said that, we are currently working on a PR to add the ability to declare async thunks directly inside of `createSlice` [3], leveraging our new `createAsyncThunk` API [4] so that their action types are automatically generated to match the slice name and the action creator name you specify. That will eliminate the need to call `createAsyncThunk` separately and pass its actions to `createSlice.extraReducers`.

Finally, RTK has been built around Immer since day 1, and it's used in `createReducer` and `createSlice` to allow you to write simpler "mutating" immutable update logic.

If you have any additional specific concerns, please ping me on Twitter or in the Reactiflux Discord. I'm always happy to answer questions and offer suggestions, and I would really be interested in seeing some details on the app you're working on to see if there's any ideas for improving RTK's APIs for your kind of use case.

[0] https://redux.js.org/faq/reducers#how-do-i-share-state-betwe...

[1] https://redux-toolkit.js.org/usage/usage-guide#exporting-and...

[2] https://github.com/reduxjs/redux-toolkit/pull/393

[3] https://github.com/reduxjs/redux-toolkit/pull/637

[4] https://redux-toolkit.js.org/api/createAsyncThunk