-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Redux ‐ How to Implement
So you want to add a Redux action and are unsure how to go aboot it. Well, you've come to the right place, if the right place is filled with terrible jokes. For a more comprehensive discussion of Redux in our application, feast your eyes on this bad boy of a writeup. This page will only be concerned with the nitty gritty details of how to become the greatest Redux-master the world has ever known... for your particular feature.
To add a new Redux component, you'll need to do the following things:
- Create an action
- Create a state
- Create a middleware (this is optional, depending on what you're implementing)
- Add the required connections to turn your component into a fully operational battleship
Below, you'll find templates for the work that needs doing.
Generally, you can think of features as things that come on screen at some point that the user sees and interacts with. But, lore-wise, a Redux component doesn't need to be a thing that's visually presented to a user. Redux is kinda like the Force, invisible, but surrounding and binding everything together. You can see the Force when Jedi does a Force Jump, for example; but you might not see it when they're Force meditating. Features are kind of like Force powers. Some of them visible, some of them operating behind the scenes. But, at some point, you need to register your feature with the Redux system for it to work. For the rest of these examples, we're gonna assume that your feature is a screen that is presented to the user to make things easier to understand.
The action is basically the thing that you will dispatch to tell whomever's listening for that action that it's been... well, ya know, actioned. Actions can have properties if you need to pass things along. This one will be a simple action, with an associated action type. We create separate action classes and action types simply to keep things neatly organized. They could be part of a single giant class and enum, but that would be unwieldy, like some of the ridiculously oversized swords you find in video-games.
final class ExampleAction: Action {
override init(windowUUID: WindowUUID, actionType: any ActionType) {
super.init(windowUUID: windowUUID, actionType: actionType)
}
}
enum ExampleActionType: ActionType {
case anExampleActionForExample
}
Please note that in this simple example, we only have one type of action and action type. Depending on your feature and its requirements, it may make sense to break down all the actions needed into more specific categories for thinking through the Redux flow, and have a user action, a middleware action, and their associated action types.
You'll need to create a state which is meant to be updatable as things are happening in your app. To do this, you'll have to conform to the ScreenState
protocol and implement the correct initializers, and the corresponding reducer for your state. The reducer is basically the thing that's like, "Hay girl hay!!! I hear what you're saying, and I know exactly what to do." And then it does that thing and returns the new state.
By conforming to ScreenState
, the defaultState(from state:)
method must be implemented. This method provides a copy of the current state, resetting any transient data to their default values. Transient data includes any state properties that have default values in the initializer and should be restored when a default action is performed.
Ensure this method is used in any case where a specific action is not present, such as in else
cases or default
cases in switch statements.
import Redux
struct ExampleState: ScreenState, Equatable {
var windowUUID: WindowUUID
var exampleProperty: Bool
var exampleTransientProperty: Bool
init(appState: AppState, uuid: WindowUUID) {
guard let exampleState = store.state.screenState(
ExampleState.self,
for: .exampleFeature,
window: uuid
) else {
self.init(windowUUID: uuid)
return
}
self.init(
windowUUID: exampleState.windowUUID,
exampleProperty: exampleState.exampleProperty
)
}
init(windowUUID: WindowUUID,
exampleProperty: Bool,
exampleTransientProperty: Bool = false) {
self.windowUUID = windowUUID
self.exampleProperty = exampleProperty
self.exampleTransientProperty = exampleTransientProperty
}
static let reducer: Reducer<Self> = { state, action in
guard action.windowUUID == .unavailable || action.windowUUID == state.windowUUID else { return defaultState(from: state) }
switch action.actionType {
case ExampleActionType.anExmapleActionForExample:
return ExampleState(
windowUUID: state.windowUUID,
exampleProperty: true,
exampleTransientProperty: true
)
default:
return defaultState(from: state)
}
}
static func defaultState(from state: ExampleState) -> ExampleState {
// exampleTransientProperty is not passed here since it will be restored to default value
return ExampleState(
windowUUID: state.windowUUID,
exampleProperty: state.exampleProperty
)
}
}
So you've create a state. Heck yes! But the app doesn't know what to do with it. So, like a cute little mushroom in the forest, you must reach out with your code tendrils and make the connections that will make everything work.
As mentioned before, your feature need not be a screen. But you have to register to redux somewhere. As a screen, you'll conform to StoreSubscriber
import Redux
class ExampleViewController: UIViewController, StoreSubscriber {
typealias SubscriberStateType = ExampleState
private var exampleState: ExampleState
init() {
subscribeToRedux()
}
deinit {
unsubscribeFromRedux()
}
// MARK: - Redux
func subscribeToRedux() {
store.dispatch(
ScreenAction(
windowUUID: windowUUID,
actionType: ScreenActionType.showScreen,
screen: .exampleFeature
)
)
let uuid = windowUUID
store.subscribe(self, transform: {
return $0.select({ appState in
return ExampleState(appState: appState, uuid: uuid)
})
})
}
func unsubscribeFromRedux() {
store.dispatch(
ScreenAction(
windowUUID: windowUUID,
actionType: ScreenActionType.closeScreen,
screen: .exampleFeature
)
)
}
func newState(state: ExampleState) {
exampleState = state
// do stuff with your new state here
}
private func exampleFunctionThatWillDispatchTheExampleActionForThisExample() {
store.dispatch(
ExampleAction(
windowUUID: windowUUID,
actionType: ExampleActionType.anExampleActionForExample
)
)
}
}
Add your feature to the AppScreen
enum so that Redux can identify your feature. Basically, this is your license plate, and you don't wanna drive without one or... ok, so this metaphor broke, because your car will totally work without a licence plate. But Redux won't. So just add your feature here.
enum AppScreen {
....
case exampleFeature
....
}
You'll have to add your feature to the AppScreenState
enums.
enum AppScreenState: Equatable {
...
case exampleFeature(ExampleState)
...
static let reducer: Reducer<Self> = { state, action in
switch state {
...
case .exampleFeature(let state):
return .exampleFeature(ExampleState.reducer(state, action))
...
}
}
var associatedAppScreen: AppScreen {
switch self {
...
case .exampleFeature: return .exampleFeature
...
}
}
var windowUUID: WindowUUID? {
switch self {
...
case .exampleFeature(let state): return state.windowUUID
...
}
}
}
You'll also have to add your feature to ActiveScreenStates
s updateActiveScreens
function so it knows what to return.
private static func updateActiveScreens(action: Action, screens: [AppScreenState]) -> [AppScreenState] {
switch action.actionType {
...
case ScreenActionType.showScreen:
let uuid = action.windowUUID
switch action.screen {
...
case .exampleFeature:
screens.append(.exampleFeature(ExampleState(windowUUID: uuid)))
...
default:
return screens
}
return screens
}
}
You'll also need to add your feature in the screenState
's return in the AppState
file.
func screenState<S: ScreenState>(_ s: S.Type,
for screen: AppScreen,
window: WindowUUID?) -> S? {
return activeScreens.screens
.compactMap {
switch ($0, screen) {
...
case (.exampleFeature(let state), .exampleFeature): return state as? S
...
}
Ah the middleware. Not like Middle Earth, but, equally likeable because it can do fancy things like interacting with the outside world to have side-effects. To implement a middleware, you'll declare it in the following way.
You'll also want to give the middleware a provider. This is basically like a reducer. But the middleware thinks it's fancy, so it calls it another name. Programming!
import Redux
final class ExampleMiddleware {
private let logger: Logger
private let telemetry = ExampleTelemetry() // you might think of passing this in as a dependency for more programming brownie points
init(logger: Logger = DefaultLogger.shared) {
self.logger = logger
}
lazy var exampleProvider: Middleware<AppState> = { state, action in
switch action.actionType {
case ExampleActionType.anExampleActionForExample:
self.telemetry.exampleActionActioned()
default:
break
}
}
}
Once more in the AppState
file, you'll need to come down and add your middleware to the middlewares portion of the store.
let store = Store(state: AppState(),
reducer: AppState.reducer,
middlewares: [
...,
ExampleMiddleware().exampleProvider,
...
]
And now, your journey is complete. If you enjoyed the ride, please tap the like button with grace and humility and only consider the subscribe button if you're willing to continue your journey of self reflection and growth as a developer... and maybe, even, as a human being.