I'm building an app with React/Redux that is in some ways similar to a text editor. It's not a text editor exactly, but it's the same general paradigm. There is a cursor for placing new items. Items can be added, selected, deleted, etc.
I'm struggling with the best way to structure my reducers in a way that is in keeping with the spirit of redux. I have separate state slices representing the selection state, the text itself, the cursor state, and other settings. I think the "redux" approach would be to have reducers for each of these state slices, independently mutating state in response to an action.
However, in a text editor these state slices are a lot more coupled than at first glance. When you press a key, sometimes a letter will be added at the location of the cursor, and the cursor will move forward. If text is selected, however, the selected text will be deleted first. If you are in "insert" mode, text to the right will be "consumed". Or maybe a modifier key was down, and text isn't added at all.
In other words, the different state slices are very coupled and what happens in one depends on the current state of the others.
I've read the "Beyond Combine Reducers" section in the Redux manual and know how to share state between slice reducers, but this seems inelegant if the end result is passing the entire state to every slice reducer. The other thing I don't like about his approach is that each reducer would have to look at the overall state and independently come to the same conclusion about what its correct response to a particular action should be. Is that what I should be doing or I should be structuring my state differently somehow?
The alternative of one centralized reducer telling the cursor, the selection state, the content, etc. how to mutate is easier conceptually but doesn't seem to scale well.
I've also read that many times coupled state is a sign that your state isn't minimal and that you should be restructuring and using memoized selectors. However that doesn't seem to be the case here. The position of the cursor is not derivable from the text, nor is the selection state.
Lastly, I've also considered Thunk middleware as this is something that I've seen suggested for handling multiple/more complex actions. I'm hesitant because it seems like it is more meant for asynchronous dispatch and this is not that.
I'd like to understand the correct approach for designing this type of app that is most in keeping with the "redux" design pattern and understand any tradeoffs there might be if there are multiple ways forward.
I wrote that "Structuring Reducers" doc section, so nice to see that people are at least reading it and finding it useful :)
You're right that the idiomatic intended approach for Redux reducer logic is small reducer functions, organized by slice of state, independently responding to the same actions. However, that's not a fixed requirement, and there are definitely times when it makes more sense to consolidate some of the logic into one place.
It's a bit hard to give absolute specific advice without seeing what your state structure is and exactly what problems you're trying to solve, but overall, you should feel free to structure your state and your reducer logic in whatever way makes the most sense for your application. If it works better to have some of that logic in a more centralized reducer function that updates several nested pieces of state at once, go for it!
As a couple other observations:
Per the Redux FAQ question on "sharing state across reducers", one approach is to put more information into the dispatched action. For example, rather than dispatching {type : "KEYSTROKE", key : "A"}
, you could dispatch {type : "KEYSTROKE", key : "A", cursorPos : 12345, ctrl : true, alt : false}
.
Also, while thunks are a good place for basic async logic, they are also useful for complex synchronous logic as well, including inspecting the current app state. I have a gist that demonstrates some common thunk use cases.
Lemme toss out a couple more resources that may be of general assistance to you:
(As a side note, I'm also currently working on a blog post that will discuss what actual technical limitations Redux requires and why, vs how you are "intended" to use Redux, vs how it's possible to use Redux. Will take me a while to finish it, but keep an eye on my blog if you're interested.)
Finally, if you'd like to discuss things further, the Reactiflux chat channels on Discord are a great place to hang out, ask questions, and learn. The invite link is at https://www.reactiflux.com . Please feel free to drop by there and ask questions - I'm usually on there evenings US time, but there's always a bunch of people hanging out happy to discuss things.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With