A minimalist predictable state management library for Swift apps, modeled after ReduxJS. It is deliberately very similar to ReduxJS so much so that it mirrors much of ReduxJS APIs, like the store object methods: getState()
, dispatch()
, subscribe()
and combineReducers()
. It also adopts the middleware system for asynchronous data flow management that ReduxJS uses, complete with similar API. If you're familiar with using ReduxJS, you should be easily familiar with Turnstate; in fact, thanks to the Swift language and its strong typing system, Turnstate offers a smoother and more fluent ergonomics than ReduxJS! I promise ya :-)
Unlike other similar Redux-like or unidirectional data-flow state management frameworks and libraries created for Swift platforms, this library is completely independent of any reactive libraries (or any other non-standard Swift library dependencies), like RxSwift or Combine. Which means it can be used for any form of Swift app development, including for UIKit, AppKit, SwiftUI, Catalyst, Linux, etc. It also means this library backward compatible with OS versions up to iOS 8.
The name "Turnstate" is an amalgalm of the words "state" and "turnstile"--which is defined by Wikipedia "as a form of gate which allows one person to pass at a time. It can also be made so as to enforce one-way human traffic, and in addition, it can restrict passage only to people who insert a coin, a ticket, a pass, or similar."
At the core of the library is the Store
class. This brings together the state, actions, middleware and reducers that participate in managing data and its flow to and fro the components of your application.
The store has several responsibilities:
- Holds the current application state (which is prescribed to be in the form of a struct with a tree of read-only properties);
- Allows access to this state via the
getState()
instance method; - Registers state "reducer" functions, which are the only way to update the state.
- Allows state to be updated via the
dispatch(action)
instance method, which invokes registered middleware plugins and, subsequently, reducers, passing along the action dispatched and the current state; - Registers listener callbacks via
subscribe(listener)
instant method; - Handles unregistering of listeners via the unsubscribe function returned by
subscribe(listener)
instance method.
It's important to note that you must only have a single instance of this class in your application. When you want to split your data handling logic, you'll use reducer composition and create multiple reducer functions that can be combined together, instead of creating separate stores.
Whereas, in ReduxJS, a store is created with a call to the function createStore
, in Turnstate an instance of a store is created via its init
ializer, like so:
let store: Store<AppState> = Store(
initialState: AppState(...),
rootStateReducer: combineReducers(reducer1, reducer2, ...),
middleware: [middleware1, middleware2, ...]
)
Object representing the current state tree of your application. The Turnstate library requires that a root state be created and associated, via a generic parameter, with an instance of a Store. This root state must conform to the RootStateProtocol.
The RootStateProtocol defines a type for the tree of data that constitutes the state of applications that use this library. Apps should declare conforming types as struct
s and the type's properties should be made immutable by declaring them with private(set) var
while setting initial/default values. The only way to change the values of the properties will be through reducer
functions registered with the instance of the Store that the conforming state is associated with.
This protocol also provides a helper copy
function that makes it easier for reducer
functions to clone existing state and modify relevant properties in a fluid syntax.
An action represents an object describing a change, alongside any associated piece of data (usually referred to as its payload), that makes sense for your application. Actions are the only way to get data into the store, so any data, whether from UI events, network callbacks, or other sources such as WebSockets needs to eventually be dispatched as actions--i.e. invoking the dispatch(action)
method of the store instance and passing in the action object.
Turnstate defines the StoreActionProtocol as a base type for actions that can be dispatched in an instance of the Store
(store) class. Apps must create an enum
that extends this protocol and then define cases inside of the enum that represents the actions that can be dispatched to the store. For example:
enum Action: StoreActionProtocol {
case UserCreateRequested(User)
}
where UserCreateRequested
is the type of the action to be dispatched for creating a User
object that is passed as a payload.
Very similar to the Redux JS Middleware System, this provides a mechanism for attaching functionality to an app in a composable manner. It is also the only way to handle asynchronous operations in order to process action
s dispatched to the store.
Middleware plugins sit between the store and state reducers defined for the store. The plugins first receive actions dispatched to the store, which they then handle (if they are interested in the action) and then passed along in the chain of plugins registered in the store, at the end of which the actions are then passed to the registered state reducers.
Middleware plugins must be defined as (only) classes that conform to the StoreMiddlewareProtocol. Dependencies required for the plugins' operations should be injected via the init
ializer for the plugin classes (and stored in private properties to be reused, throughout the lifecycle of the plugins' instances, for every action the plugins are interested in).
The StoreMiddlewareProtocol defines a run
function which the Store instance, which the middleware plugin is registered with, invokes to execute the plugin. This run
function has the following signature:
func run(
store: StoreAPI,
next: @escaping (_ a: StoreActionProtocol) -> Void,
action: StoreActionProtocol
)
where
-
the
store
parameter is of typeStoreAPI
, a typealias for a Swift tuple containing two functions: thedispatch
andgetState
functions, which are passed in by the Store instance that this middleware plugin is registered with. These are the samedispatch
andgetState
functions that are actually part of the store. Important to note: ThestoreAPI.dispatch
should be used to send a new action to thestore
, whilenext
function (below) is used to continue the passed-inaction
along the chain of middleware plugins.The
storeAPI.getState
function returns the current state of the application held in the Store. -
the
next
parameter defines a function to invoke in order to pass-on the currently dispatchedaction
to the next plugin in the chain of middleware plugins registered with the Store. This must be called just once at the end of this function to signal that the plugin is done with its operations. Thenext
function signature defines aStoreActionProtocol
parameter with which it expects to receive the currently dispatchedaction
. -
Lastly, the
action
parameter defines the action currently dispatched to the store.
It's important to note that you must only have a single instance of the Store class in your application. When you want to split your data handling logic, you'll use reducer composition and create multiple reducer
functions that can be combined together, instead of creating separate stores.
A reducer
function is really a pure function that is used to compute a new state
given the currently existing state and an action
dispatched to the store.
The Turnstate library expects reducer
functions to have a signature like so:
(RootStateProtocol, StoreActionProtocol) -> RootStateProtocol
Essentially, the reducer
function will expect the current state
object, which conforms to the RootStateProtocol, and an action
object, which conforms to the StoreActionProtocol, dispatched to the store
, and then return a new (or, if the reducer isn't interested in this action, returns the same) state
object.
The Turnstate library provides a helper function that turns a list of different reducing functions into a single reducing function you can pass to the store init
ializer. The resulting reducer calls every child reducer, and gathers their results to update the root state object. This function helps you organize your reducers to manage their own slices of state.