Sharing logic with actions
There is a common pattern of using actions to share logic across multiple parts of a reducer. This is inefficient. Sending actions is not as lightweight as calling a method on a class.The problem
Suppose you want to run shared logic after three different actions:Problems with this approach
Performance
Two actions are processed for every user interaction
Inflexibility
Shared logic must always run after, never before
Test bloat
Tests must assert on internal shared actions
The solution
Share logic using methods instead of actions:1
Define a method on your reducer
The method can take
inout State if it needs to mutate state, and return Effect<Action> if it needs to run effects.2
Call the method directly
Call it from any action handler without sending additional actions.
3
Enjoy the flexibility
You can now run shared logic before, after, or between other logic:
Testing improvements
Tests become more streamlined:CPU-intensive calculations
Reducers run on the main thread and are not appropriate for intense CPU work.The problem
The solution
Return an effect to perform work in the cooperative thread pool:High-frequency actions
Sending actions comes with a cost. Avoid high-frequency actions unless your application truly needs them.Example: Reporting progress
Instead of reporting progress for every step:Example: Sliders
Deriving a binding directly from the store sends an action for every pixel:@State and send one action when done:
Store scoping
The most common form of scoping—scoping directly to child features—is the most performant and is the intended use.Good scoping (performant)
Scoping directly to child state and actions:Problematic scoping
Scoping on computed properties can cause performance issues:The solution
Usescope only along stored properties of child features:
1
Use stored properties
2
Move computation to child
Push computed logic into the child view or reducer, towards the leaf nodes of your application.
Performance checklist
Use this checklist to identify and fix performance issues:Are you sharing logic with actions?
Are you sharing logic with actions?
❌ Don’t send actions for shared logic✅ Use methods on your reducer instead
Is CPU work in your reducer?
Is CPU work in your reducer?
❌ Don’t perform intense calculations in reducers✅ Return effects that do work in the thread pool with periodic yields
Are you sending high-frequency actions?
Are you sending high-frequency actions?
❌ Don’t send dozens/hundreds of actions per second✅ Throttle or debounce actions, or use local
@StateAre you scoping on computed properties?
Are you scoping on computed properties?
❌ Don’t use computed properties in
scope✅ Use stored properties and move computation to child featuresProfiling tips
1
Use Instruments
Profile your app with Xcode’s Instruments to identify slow reducers and view bodies.
2
Add print statements
Put prints in computed properties to see how often they’re invoked:
3
Measure action processing time
Use the library’s built-in instrumentation to see how long actions take:
Summary
Share logic with methods
Not actions
CPU work in effects
Not reducers
Throttle high-frequency actions
Or use local state
Scope stored properties
Not computed properties