[Sitka] An Overview of Sitka Module Manager #typescript #redux
Sitka is a module manager for the state-related parts of your application, and used with Typescript allows you to create strongly-typed APIs for specific parts of your application's Redux state. Each module has the ability to communicate with other modules, enabling greater functionality through coordinated inter-module interaction. Using Redux-Saga forks, Sitka is also able to schedule long-running daemon processes very easily.
Sitka makes use of both Redux and Redux-Saga.
Using Redux, Sitka maintains application state in a Redux store. Each individual module is able to manage the mutations of its specific part of Redux state.
Using Redux-Saga is an excellent way to coordinate both asynchronous and synchronous operations. It's very easy to map the methods of a module, within its Typescript class, to a collection of operations expressed as a Redux-Saga function.
Let's look at a simple counter application, in which there is one piece of state to manage a counter application:
// the shape of the managed part of state
interface Counter {
value: number
}
// default/initial state of the managed part of Redux state
const defaultCounterState: Counter = {
value: 0,
}
Our workflow before creating Sitka consisted of these steps:
*function
*function
, which handles the action's payload as well as any other application side-effects, and setting a new value in Redux state, using a second Redux action creator// 1. interface for action creator to handle increment
interface HandleIncrementAction {
type: "HANDLE_INCREMENT",
}
// 2. action creator to handle increment
const handleIncrement = (): HandleIncrementAction => ({
type: "HANDLE_INCREMENT"
})
// 3. a listener in the root saga
default function* root(): {} {
yield [
takeEvery("HANDLE_INCREMENT", handleIncrement)
]
}
// 4. a selector function to get a specific part of Redux state
function selectCounter(state: AppState): Counter {
return state.counter
}
// 5. a saga function to handle side effects and/or payload
function* handleIncrement(action: HandleIncrementAction): {} {
// uses the selector defined in step 4
const counter = yield select(selectCounter)
const newValue = counter.value + 1
// uses the action/interface defined in steps 6 and 7
yield put(actions.setCounter(newCounter))
}
// 6. interface for action creator to set new value in state
interface SetCounter {
type: "SET_COUNTER"
value: number
}
// 7. action creator to set new value in state
const setCounter = (value: number) => ({
type: "SET_COUNTER",
value,
})
// 8. a reducer listening for the action called in step 5
function counter(
state: Counter = defaultCounterState,
action: SetCounterAction,
): number {
switch (action.type) {
case "SET_COUNTER":
return { ...state, value: action.value }
default:
return state
}
}
// 9. reducer is registered with the root reducer
const rootReducer = redux.combineReducers({
counter,
})
Typescript, Redux and Redux-Saga are great but typical usage requires a lot of boilerplate, as the example above shows.
This is everything that had to be written, in addition to all the import
statements that are needed when each of these pieces are in separate files!
Sitka dramatically cuts down the amount of boilerplate. All the code that is needed to accomplish the same ends as the counter application above can be written using Sitka like this:
interface CounterState {
readonly value: number
}
class CounterModule extends SitkaModule<CounterState, AppModules> {
public moduleName: string = "counter"
public defaultState: CounterState = {
value: 0,
}
public *handleIncrement(): {} {
const counter: CounterState = yield select(this.getCounter)
const newValue = counter.value + 1
yield put(this.setState({ value: newValue }))
}
private getCounter(state: AppState): CounterState {
return state.counter
}
}
...and that's it. A full counter application can be found here on Github.
A moduleName
and defaultState
are set within the class, and a single generator function *handleCounter
is defined, which simply increments the counter by 1.
This is SO much less code than the above example. It is much more maintainable, much easier to reason about, and overall leaves you with a much cleaner codebase.
In your presentational component, this is how you might call *handleIncrement
:
const state = store.getState()
const modules = state.__sitka__.getModules()
const { counter } = modules
// this fires a Redux action that
// Sitka uses to increment counter
counter.handleIncrement()
The several advantages of using Sitka over our previous workflow have proven to be significant:
There is far less boilerplate to write and maintain. This alone leaves your application feeling much lighter and easier to work with. It is also much easier to introduce a new feature into your application whether that means adding a new module, adding or changing functionality in an existing module, or even moving functionality from one module to another.
There is far less code in a Sitka-powered application overall. This means there is far less room for errors and accidental omissions while developing, which means no more losing time because you forgot to register your new reducer with the root reducer, amongst all other boilerplate.
There is a very easy-to-learn pattern of doing things with Sitka. For example, the majority of a module's methods will look and feel fairly similar to *handleCounter
above:
Sitka is not exclusive of your existing workflow in your current Redux application. See an example of adding Sitka into a project. You can see an example of usage using React-Redux's connect
function to connect your component to a Sitka-powered Redux store
.
We are using Sitka in live projects, and it is delightful to work with. You get to take advantage of all the benefits of a strongly-typed codebase, while also enjoying writing a small fraction of the code you needed before using Sitka. We here at Olio Apps hope you try out Sitka to manage your Redux state in your next project, or even to manage a new feature and piece of state in your existing project.