Reactive around reducer
Published: 2023-10-24 21:29:33

Recently, I've been experimenting with an implementation of my library that provides reactivity to ReScript, called [ReX](https://github.com/MikeSkoe/ReX). The goal of this project is to implement a low-level reactive library for ReScript and explore a combination of reducers and observables to achieve scalability. Additionally, I want to experiment with OCaml's React-like API and have some fun along the way. You can check an example usage of the library in a React application [here](https://github.com/MikeSkoe/paper-rock-scissors)

## A taste of ReX
Before diving into my thoughts, let me quickly show you an example of how to use the ReX library

```rescript
let intString = ReX.make(Belt.Int.toString);
let floatString = ReX.make(Belt.Float.toString);

let numbers = ReX.both(intString, floatString, ("0", "0.0"))
    ->ReX.map(((left, right)) => `${left}:${right}`)
    ->ReX.debounce(100);

let unsub = numbers->ReX.sub(Js.log);

intString->ReX.call(3);
floatString->ReX.call(2.2);
```

## React Architecture
Sometimes I meditate about Elm-ish (like Redux) vs reactive (like MobX, Jotai, etc). On the one hand, the Elm-ish way provides robust global state management that is easy to test and debug. On the other hand, reactive options let you write a boilerplate code with declarative time-spaced operators. Ultimately I concluded that the best option is a **proper combination**.

## How I see it
```
[Domain] <- [Reducer] <- [Observable] <- [UI]
```
When it comes to designing an architecture, I imagine it as a system with multiple layers, each with its own specific purpose.

At the very core, we have the **domain** of our application, which contains the business logic with no dependencies. Surrounding the domain is a **reducer**, which is a thin layer that exposes the domain in the form of `(action, prevState) => newState`.

> Languages that support Algebraic Data Types (ADT) are particularly useful here because they eliminate the need to write any boilerplate code to make actions.

The reducer is then covered with **observables**, which provide a reactivity part to the architecture. Wrapping our reducers with observables gives us a lot of opportunities to declaratively handle effects like throttling actions, implementing selectors, combining the observables, and more.

As an example, let's imagine a todo application.

### Domain
A type of the **domain**-level modules could look like this:

```rescript
module Tasks: {
    type task = Todo(string) | Done(string, float)
    type t = list<task>
    
    let empty: t
    let addTask: (t, string) => t
    let doneTask: (t, task, float) => t
    let filterTasks: (t, task => bool) => t
    let isDone: task => bool
    let isTodo: task => bool
}

module Filter: {
    type t = All | TodoOnly | DoneOnly

    let filterTasks: (Tasks.t, t) => Tasks.t
}
```

### Reducer (State)
Next we represents the functionality in the form of **actions** and a **state**

```rescript
module State = {
    type action =
        | AddTask(string)
        | DoneTask(Tasks.task, float)

    type state = Tasks.t;

    let initialState: state = Tasks.empty;

    let reducer = (state: state, action: action): state =>
        switch action {
            | AddTask(content) => state->Tasks.addTask(content)
            | DoneTask(task, timestamp) => state->Tasks.doneTask(task, timestamp)
        }
}
```

### Observable (Store)
And now we can use the state in our observables

```rescript
module Store = {
    let { make, id, thunk, either, reduce, sub, both, map, delay } = module(ReX);

    // inputs
    let addTask = make(str => State.AddTask(str));
    let doneTask = make(task => State.DoneTask(task, Js.Date.now()));
    let fetchNewTask = make(id)
        ->thunk((source, dispatch) => {
            fetchFrom(source)
                ->Js.Promise2.then(async response => {
                    dispatch(State.AddTask(response))
                })
                ->ignore;
        });
    let taskFilter = make(id);

    // global state
    let tasks =
        addTask
        ->either(doneTask->delay(500))
        ->either(fetchNewTask)
        ->reduce(State.initialState, State.reducer)

    // effects (middleware)
    let unsubLogFilter = taskFilter->sub(log)

    // outputs
    let filteredTasks =
        tasks
        ->both(taskFilter, (Tasks.empty, Filter.All))
        ->map(((tasks, filter)) => Filter.filterTasks(tasks, filter))
}
```

In terms of Redux, we have:
- `Store.addTask`, `Store.fetchNewTask`, etc. as actions. So adding a new task from a React component is as simple as `Store.addTask(contentString)` instead of `dispatch(createAddTaskAction(contentString))`
- `Store.tasks` as a reducer that contains the most critical logic
- `Store.unsubLogFilter` as a middleware
- `Store.filteredTasks` as a selector. So that we can expose only the selected slices of states, instead of only the main state, delegating the selection to components

### Presentation (View)
In order to be able to use observables in React components, we need to sync them with Component's lifecycle. Here is a usefull hook that takes an observable and a selector and provides a local slice of obsevable's stream

```rescript
let useSync: (ReX.t<BattleState.action, BattleState.state>, 'a, BattleState.state => 'a) => 'a
    =
    (t, initial, selector) => {
        let snapshot = React.useRef(initial);

        React.useSyncExternalStore(
            ~subscribe = sub => {
                let unsub = t->ReX.sub(value => {
                    snapshot.current = selector(value);
                    sub();
                });
                (.) => unsub();
            },
            ~getSnapshot = () => snapshot.current,
        );
    }
```

So now we can use observables like:
```rescript
let choice = app->useSync(None, getChoice); // app is an observable; getChoice is a pure state's selector
```

The full example of useing the observables together with React can be found [here](https://github.com/MikeSkoe/paper-rock-scissors/tree/main)


## In conclusion
My thoughts about the layered architecture for reactive programming combined with an immutable global state led me to create the ReX library, which provides reactivity to ReScript. However, if you need to follow a similar approach in production (specifically with TypeScript), I highly recommend trying to connect RxJS to Redux manually and with tools like redux-observable