frankmeza
12/5/2018 - 10:27 PM

[Sitka] An Overview of Sitka Module Manager #typescript #redux

[Sitka] An Overview of Sitka Module Manager #typescript #redux

Sitka Module Manager

Overview

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 Redux and Redux-Saga

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.

Why Sitka Was Created

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:

  1. an interface for an action creator
  2. an action creator
  3. a listener for this action in the sagas index, routing to a saga *function
  4. a selector to get that part of state
  5. the saga *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
  6. an interface for this second action creator
  7. this second action creator
  8. a reducer listening for this second action
  9. making sure that this reducer has been registered with the root reducer

A Counter App Before Sitka

// 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!

A Counter App With Sitka

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()

Advantages of Using Sitka in Your Redux Application

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:

    1. get the module-specific piece of state
    2. create an updated replacement value
    3. set that replacement value in state

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.