WhatsUp - front-end framework based on ideas of streams and fractals
Tags: javascript, typescript, webdev
Published: 2021-04-02 09:20:59

Hi guys!

My name is Dan. Today I want to share my project with you. It is a frontend framework. I collected my most exotic ideas in it.

```bash
npm i whatsup
``` 

It is built on generators, provides functionality similar to react + mobx out of the box, has good performance, and weighs less than 5kb gzip. With a reactive soul. With minimal api. With the maximum use of native language constructs.

The architectural idea is that our entire application is a tree structure, along the branches of which the flow of data is organized in the direction of the root, reflecting the internal state. During development, we describe the nodes of this structure. Each node is a simple self-similar entity, a full-fledged complete application, all work of which is to receive data from other nodes, process it and send it next.

This is the first part of my story. We'll take a look at state management here.

## Cause & Conse

Two basic streams for organizing reactive data state. For ease of understanding, they can be associated with the familiar computed and observable.

```typescript
const name = conse('John')

whatsUp(name, (v) => console.log(v))
//> "John"

name.set('Barry')
//> "Barry"
```
[Example](https://codesandbox.io/s/whatsup-conse-example-sjuep)

Nothing special, right? `conse` creates a stream with an initial value, `whatsUp` - add the observer. Through `.set(...)` we change the value - the observer reacts - a new entry appears in the console.

`Cause` is created from a generator, inside which the `yield*` expression is the "connection" of an external thread to the current one. The situation inside the generator can be viewed as if we are inside an isolated room, in which there are several `yield*` inputs and only one `return` output

```typescript
const name = conse('John')

const user = cause(function* () {
    return {
        name: yield* name,
        //    ^^^^^^ connect stream name 
    }
})

whatsUp(user, (v) => console.log(v))
//> {name: "John"}

name.set('Barry')
//> {name: "Barry"}
```
[Example](https://codesandbox.io/s/whatsup-cause-example-0lpj9)

`yield* name` sets the dependence of the `user` stream on the `name` stream, which in turn also leads to quite expected results, namely - change the name - the user changes - the observer reacts - the console shows a new record.

## What is the advantage of generators?

Let's complicate our example a little. Let's imagine that in the data of the user stream, we want to see some additional parameter `revision`, that reflects the current revision.

It's easy to do - we declare a variable `revision`, the value of which is included in the dataset of the `user` stream, and each time during the recalculation process, we increase it by one.

```typescript
const name = conse('John')

let revision = 0

const user = cause(function* () {
    return {
        name: yield* name,
        revision: revision++,
    }
})

whatsUp(user, (v) => console.log(v))
//> {name: "John", revision: 0}

name.set('Barry')
//> {name: "Barry", revision: 1}
```
[Example](https://codesandbox.io/s/whatsup-revision-example-yyo4r)

But something is wrong here - `revision` looks out of context and unprotected from outside influences. There is a solution to this - we can put the definition of this variable in the body of the generator, and to send a new value to the stream (exit the room) use `yield` instead of `return`, which will allow us not to terminate the execution of the generator, but to pause and resume from the place of the last stops on next update.

```typescript
const name = conse('John')

const user = cause(function* () {
    let revision = 0

    while (true) {
        yield {
            name: yield* name,
            revision: revision++,
        }
    }
})

whatsUp(user, (v) => console.log(v))
//> {name: "John", revision: 0}

name.set('Barry')
//> {name: "Barry", revision: 1}
```
[Example](https://codesandbox.io/s/whatsup-revision-example-2-mrntb)

Without terminating the generator, we get an additional isolated scope, which is created and destroyed along with the generator. In it, we can define the variable `revision`, available from calculation to calculation, but not accessible from outside. At the end of the generator, `revision` will go to trash, on creation - it will be created with it.

## Extended example

The functions `cause` and `conse` are shorthand for creating streams. There are base classes of the same name available for extension.

```typescript
import { Cause, Conse, whatsUp } from 'whatsup'

type UserData = { name: string }

class Name extends Conse<string> {}

class User extends Cause<UserData> {
    readonly name: Name

    constructor(name: string) {
        super()
        this.name = new Name(name)
    }

    *whatsUp() {
        while (true) {
            yield {
                name: yield* this.name,
            }
        }
    }
}

const user = new User('John')

whatsUp(user, (v) => console.log(v))
//> {name: "John"}

user.name.set('Barry')
//> {name: "Barry"}
```
[Example](https://codesandbox.io/s/whatsup-extended-example-0fxb8)

When extending, we need to implement a `whatsUp` method that returns a generator.

## Context & Disposing

The only argument accepted by the whatsUp method is the current context. It has several useful methods, one of which is `update` - allows you to force initiate the update procedure.

To avoid unnecessary and repeated computation, all dependencies between threads are dynamically tracked. When the moment comes when the stream has no observers, the generator is automatically destroyed. The occurrence of this event can be handled using the standard `try {} finally {}` language construct.

Consider an example of a timer thread that generates a new value with a 1 second delay using setTimeout, and when destroyed, calls clearTimeout to clear the timeout.

```typescript
const timer = cause(function* (ctx: Context) {
    let timeoutId: number
    let i = 0

    try {
        while (true) {
            timeoutId = setTimeout(() => ctx.update(), 1000)
            // set a timer with a delay of 1 sec

            yield i++
            // send the current value of the counter to the stream 
        }
    } finally {
        clearTimeout(timeoutId)
        // clear timeout
        console.log('Timer disposed')
    }
})

const dispose = whatsUp(timer, (v) => console.log(v))
//> 0
//> 1
//> 2
dispose()
//> 'Timer disposed'
```
[Пример на CodeSandbox](https://codesandbox.io/s/whatsup-update-and-dispose-ce4ct)

## Mutators

A simple mechanism to generate a new value based on the previous one. Consider the same example with a mutator based timer.

```typescript
const increment = mutator((i = -1) => i + 1)

const timer = cause(function* (ctx: Context) {
    // ...
    while (true) {
        // ...
        // send mutator to the stream
        yield increment
    }
    // ...
})
```
[Example](https://codesandbox.io/s/whatsup-timer-with-increment-mutator-rgr2v)

A mutator is very simple - it is a method that takes a previous value and returns a new one. To make it work, you just need to return it as a result of calculations, all the rest of the magic will happen under the hood. Since the previous value does not exist on the first run, the mutator will receive `undefined`, the `i` parameter will default to `-1`, and the result will be `0`. Next time, zero mutates to one, etc. As you can see, `increment` allowed us to avoid storing the local variable `i` in the generator body.

That's not all. In the process of distributing updates by dependencies, the values ​​are recalculated in streams, while the new and old values ​​are compared using the strict equality operator `===`. If the values ​​are equal, the recalculation stops. This means that two arrays or objects with the same data set, although equivalent, are still not equal and will provoke meaningless recalculations. In some cases this is necessary, in others it can be stopped by using the mutator as a filter.

```typescript
class EqualArr<T> extends Mutator<T[]> {
    constructor(readonly next: T[]) {}

    mutate(prev?: T[]) {
        const { next } = this

        if (
            prev && 
            prev.length === next.length && 
            prev.every((item, i) => item === next[i])
        ) {
            /*
            We return the old array, if it is equivalent 
            to the new one, the scheduler will compare 
            the values, see that they are equal and stop 
            meaningless recalculations
            */
            return prev
        }

        return next
    }
}

const some = cause(function* () {
    while (true) {
        yield new EqualArr([
            /*...*/
        ])
    }
})
```

In this way, we get the equivalent of what in other reactive libraries is set by options such as shallowEqual, at the same time we are not limited to the set of options provided by the library developer, but we ourselves can determine the work of filters and their behavior in each specific case. In the future, I plan to create a separate package with a set of basic, most popular filters.

Like `cause` and `conse`, the `mutator` function is shorthand for a short definition of a simple mutator. More complex mutators can be described by extending the base Mutator class, in which the `mutate` method must be implemented.

Look - this is how you can create a mutator for a dom element. The element will be created and inserted into the body once, everything else will boil down to updating its properties.

```typescript
class Div extends Mutator<HTMLDivElement> {
    constructor(readonly text: string) {
        super()
    }

    mutate(node = document.createElement('div')) {
        node.textContent = this.text
        return node
    }
}

const name = conse('John')

const nameElement = cause(function* () {
    while (true) {
        yield new Div(yield* name)
    }
})

whatsUp(nameElement, (div) => document.body.append(div))
/*
<body>
    <div>John</div>
</body>
*/
name.set('Barry')
/*
<body>
    <div>Barry</div>
</body>
*/
```
[Example](https://codesandbox.io/s/whatsup-dom-mutator-r4u17)

## Actions

Actions are designed to perform batch updates of data in streams.

```typescript
import { cause, conse, action, whatsUp } from "whatsup";

const name = conse("John");
const age = conse(33);

const user = cause(function* () {
  return {
    name: yield* name,
    age: yield* age
  };
});

whatsUp(user, (v) => console.log(v));
//> {name: "John", age: 33}

// without action
name.set("Barry");
age.set(20);

//> {name: "Barry", age: 33}
//> {name: "Barry", age: 20}

// with action

action(() => {
  name.set("Jessy");
  age.set(25);
});

//> {name: "Jessy", age: 25}
```
[Example](https://codesandbox.io/s/whatsup-actions-hlytk)

## Conclusion

In this article, I described the basic capabilities of WhatsUp for organizing state management. In the next article, I will tell you how WhatsUp can work with jsx, about the event system and the exchange of data through the context.

If you liked the idea of my framework - leave your feedback or a star on the github. I'll be very happy. Thanks!

{% github whatsup/whatsup%}