I have been following the widely given advice of learning React development by first mastering component props
, encapsulating UI state in component level this
My answer is mostly speaking from my own experience learning redux and using it professionally. The team I was on went down the same path of setter-like actions, and then shifted away to action names that were more event-based and describe what had happened rather than what should happen.
Question: Is it best practice to concentrate business logic in a web of fairly intricate action creators?
This depends on how your actions are named. In your case, your actions are very glorified setters, so all of your business logic is going to live inside of Action Creators. If you name your actions to be more event-like (descriptive about what happened) rather than setters, you're going to have some of the business logic shift into the reducers, and complexity removed from action creators, because event actions naturally feel more re-usable across different reducers. When you do setter actions, the tendency is to have setter-actions that interact with only 1 reducer, and create more setter-actions when you want other reducers to be involved.
If you have an app for a school, and a student is expelled, you'll likely dispatch a REMOVE_STUDENT
and then aDELETE_GRADES_FOR_STUDENT
action. If your action has an event-like name, you may be more inclined to just have a STUDENT_EXPELLED
action that the grades reducer and student roster reducer both act upon it.
There is nothing technically stopping you from having setter-like names, and acting on them in multiple reducers, though. It's just not the tendency that my team fell into when working in Redux and using setter-like names. We didn't want to muddy up the expectations and purity that came from having concise action names, where the impact on state was very clear. REMOVE_STUDENT_GRADES
and DELETE_STUDENT_FROM_ROSTER
felt good.
Question: Is it normal, and acceptable practice to have boilerplate reducers that function merely as glorified setters to the redux store state?
It is normal, but not necessarily correct. This is how our codebase grew initially - we even had standards to name our actions as RESET_...
, SET_...
, REMOVE_...
, ADD_...
, UPDATE...
etc. This seemed to work for a while, until we bumped into cases where we needed multiple reducers to update according to a single action.
You actions will evolve in one of these 2 ways (or both)
Dispatch multiple actions in a row (you must use a library like redux-batch-actions if you want to dispatch multiple actions in a row). We chose not to use this, because it's cumbersome and didn't feel like it scaled very well as our codebase grew in size.
Rename your actions to be more generic and re-usable across different reducers. This is what we ended up doing. Having actions as setters and getters was cumbersome. Dan Abramov and others has expressed their opinion that Redux Actions should be events
(a description of a thing that has happened), rather than instructions
(a description of a thing that should happen). The team I work on agreed with this, and we've moved away from the setters-style of actions. There was much debate about this earlier on when Redux was new.
In scenario 1, you might do something like this:
// student has been expelled from school, remove all of their data
store.dispatch(batchActions(
removeStudentFromClass(student),
removeStudentsGrades(student)
));
// student roster reducer
case REMOVE_STUDENT_FROM_CALLS:
/* ... */
// student grades reducer
case REMOVE_STUDENT_GRADES:
/* ... */
If you go down this path without using Batch Actions, it's an absolute nightmare. Each dispatched event will recompute state, and re-render your app. This falls apart very quickly.
// student has been expelled from school, remove all of their data
store.dispatch(removeStudentFromClass(student));
// app re-rendered, students grades exist, but no student!
store.dispatch(removeStudentsGrades(student));
In the above code, you dispatch an action to remove the student from class, and then the app re-renders. If you have a grades page open, and you can see the students grades, but the student is removed, you're very likely going to reference state in the student roster reducer to grab the student info and that can/will throw a JS error. Bad news. You have the grades for a student of undefined
?! I ran into issues like this myself, and it was part of our motivation for moving to option 2 below. You'll hear about these kinds of states called "intermediate states" and they're problematic.
In scenario 2 your code might look more like this:
store.dispatch(expelStudent(student));
// student roster reducer
case EXPEL_STUDENT:
/* ... */
// student grades reducer
case EXPEL_STUDENT:
/* ... */
With the code above, the student is expelled via the action, and their data is removed from all reducers in 1 step. This scales nicely and your codebase reflects the business terms related to your app that you would talk about day-to-day. You can also perform the same state updates for multiple events if it makes sense from a business logic perspective:
case EXPEL_STUDENT:
case STUDENT_DROPPED_OUT:
case STUDENT_TRANSFERRED:
/* remove student info, all actions must have action.payload.student */
Question: Is it normal for a redux application to rely extensively on thunk'ed action creators, and rarely to fire a standard object action directly?
Yes definitely. As soon as you need to grab a little piece of data from the store in an action creator, it has to become a thunk. Thunks are very common, and should have been part of the redux library.
As our thunks grew in complexity, they got confusing and difficult to easily understand. We started to abuse promises and return values and it was taxing. Testing them was also a nightmare. You have to mock out everything, it's painful.
To solve this problem, we pulled in redux-saga. Redux-saga is easily testable with libraries like redux-saga-test-plan or redux-saga-test-engine (we use test-engine and have contributed to it as needed).
We aren't 100% sagas, and don't aim to be. We still use thunks as needed. If you need to upgrade your action to be a little smarter, and the code is very simple, there's no reason why you shouldn't upgrade that action to a thunk.
As soon as an action creator gets complex enough to warrant some unit testing, redux-saga might come in handy.
Redux-saga does have a rough learning curve to it, and feels quite bizarre at first. Testing sagas manually is miserable. Great learning experience, but I would not ever do it again.
Question: Is redux the correct tool to use for a global state store? Are there alternatives out there that behave more akin to react's built-in this.state, allowing a global application state to be propagated through stateless react components, and updated from throughout the application via a centralized 'switchboard', without the seemingly endless web of boilerplate, constants and switch statements that come with adopting redux?
MobX - I've heard good things about it from people who have your same complaints about Redux (too much boilerplate, too many files, everything is disconnected) I don't use it myself and have not used it, though. There's a good chance that you'll enjoy it more than Redux. It solves the same problem, so if you actually enjoy it more then it may be worth the switch. Developer experience is very important if you're going to work on code for a long time.
I'm okay with the Redux boilerplate and whatnot. The team I worked on has made macros to scaffold out the boilerplate of creating new actions, and we have lots of tests in place so our Redux code is pretty solid. Once you work with it for a while, you internalize the boilerplate and it's not as exhausting.
If you do stick with Redux long term, and are savvy enough to adopt flow on top of redux it's a huge win for long-term maintainability. Fully-typed redux code is amazing to work in, especially for refactoring. It's so easy to refactor a reducer/actionCreators, but forget to update unit test code. If your unit tests are covered by flow, it's going to complain that you're calling a function incorrectly immediately. It's wonderful.
Introducing Flow is a complex hurdle to get over, but well worth it. I wasn't involved in the initial set up, and I assume it's gotten easier to introduce to a codebase, but I'd imagine that it will take some learning and hours. Worth it though. Definitely 100% worth it.
Question: Could one legitimately (in all seriousness, not just blatantly demonstrating a point) use redux with precisely one reducer?
You definitely could, it could work for a small app. It wouldn't scale well for a larger team, and refactoring seems like it would become a nightmare. Splitting the store up into separate reducers lets you isolate responsibilities and concerns.