Whatsup alternatives and similar libraries
Based on the "derby" category.
Alternatively, view core alternatives based on common mentions on social networks and blogs.
-
Alpine.js
A rugged, minimal framework for composing JavaScript behavior in your markup. -
GrapesJS
Free and Open source Web Builder Framework. Next generation tool for building templates without coding -
Adonis
π The Node.js Framework highly focused on developer ergonomics, stability and confidence -
Rete.js
JavaScript framework for visual programming and creating node editor #StandWithUkraine -
Million
ππΌ6οΈβ£ An extremely fast virtual DOM that makes React up to 70% faster! -
litegraph.js
A graph node engine and editor written in Javascript similar to PD or UDK Blueprints, comes with its own editor in HTML5 Canvas2D. The engine can run client side or server side using Node. It allows to export graphs as JSONs to be included in applications independently. -
FoalTS
Full-featured Node.js framework, with no complexity. π Simple and easy to use, TypeScript-based and well-documented. -
Keo
Plain functions for a more functional Deku approach to creating stateless React components, with functional goodies such as compose, memoize, etc... for free. -
finity
A finite state machine library for Node.js and the browser with a friendly configuration DSL. -
makefun
Use promise values as they are satisfied otherwise wait for the promise
Appwrite - The Open Source Firebase alternative introduces iOS support
* Code Quality Rankings and insights are calculated and provided by Lumnify.
They vary from L1 to L5 with "L5" being the highest.
Do you think we are missing an alternative of Whatsup or a related project?
Popular Comparisons
README
What is it?
Whats Up is a reactive framework based on the ideas of streams and fractals. It is very easy to learn and powerful to work with. It has only a few components, but enough to build complex applications.
Install
Use the WhatsUp CLI to generate whatsup projects quickly. See below for the necessary steps:
- Install the WhatsUp CLI globally:
npm i -g @whatsup/cli
# or
yarn global add @whatsup/cli
- Create a project:
whatsup project
- Run the application:
npm start
# or
yarn start
Streams
Conse
it's like a observable
import { conse } from 'whatsup'
const name = conse('Natali')
Cause
it's like a computed
import { cause } from 'whatsup'
const user = cause(function* () {
while (true) {
yield {
name: yield* name,
}
}
})
Fractal
This is the cherry on the cake. We'll tell you about it below :)
Simple rules
yield
- send to stream
yield*
- read from stream
Watching
import { whatsUp } from 'whatsup'
const onData = (data) => console.log(data)
const onError = (err) => console.error(err)
const dispose = whatsUp(user, onData, onError)
//> {name: 'Natali'}
name.set('Aria')
//> {name: 'Aria'}
dispose() // to stop whatsUping
Extended example
You can extend base classes, but you need to implement the whatsUp
method
import { conse, cause, whatsUp } from 'whatsup'
interface 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('Natali')
whatsUp(user, (data) => console.log(data))
//> {name: 'Natali'}
user.name.set('Aria')
//> {name: 'Aria'}
Single argument of whatsUp
method - context: Context
The context has several useful methods for controlling flow, such as update
import { cause, whatsUp } from 'whatsup'
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
let i = 0
while (true) {
setTimeout(() => ctx.update(), this.delay)
// kickstart ^^^^^^^^^^^^ updating loop
yield i++
}
}
}
const timer = new Timer(1000)
whatsUp(timer, (data) => console.log(data))
//> 0
//> 1
//> 2 ...
Local-scoped variables & auto-dispose unnecessary dependencies
You can store ancillary data available from calculation to calculation directly in the generator body and you can react to disposing with the native language capabilities
import { conse, cause, whatsUp } from 'whatsup'
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
let i = 0
let timeoutId: number
try {
while (true) {
timeoutId = setTimeout(() => ctx.update(), this.delay)
yield i++
}
} finally {
// This block will always be executed when unsubscribing from a stream.
clearTimeout(timeoutId)
console.log('Timer destroed')
}
}
}
class App extends Cause<number> {
readonly showTimer = conse(true);
*whatsUp() {
// local scoped Timer instance, she is alive while the stream App is alive
const timer = new Timer()
while (true) {
if (yield* this.showTimer) {
const time = yield* timer
yield time
} else {
yield 'App timer is hidden'
}
}
}
}
const app = new App()
whatsUp(app, (data) => console.log(data))
//> 0
//> 1
//> 2
app.showTimer.set(false)
//> Timer destroed
//> App timer is hidden
app.showTimer.set(true) // the timer starts from the beginning
//> 0
//> 1
//> ...
Mutators
Allows you to create new data based on previous. You need just to implement the mutate method.
import { cause, Mutator } from 'whatsup'
class Increment extends Mutator<number> {
mutate(prev: number | undefined = 0) {
return prev + 1
}
}
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
while (true) {
setTimeout(() => ctx.update(), this.delay)
yield new Increment()
// we no longer need to store a local counter "i"
}
}
}
Mutator shorthand
You can create a mutator using shorthand
import { cause, mutator } from 'whatsup'
const increment = mutator<number>((n = 0) => n + 1)
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
while (true) {
setTimeout(() => ctx.update(), this.delay)
yield increment
}
}
}
Mutators & filters
Mutators can be used to write filters.
import { whatsUp, cause, Mutator } from 'whatsup'
class EvenOnly extends Mutator<number> {
readonly next: number
constructor(next: number) {
super()
this.next = next
}
mutate(prev = 0) {
return this.next % 2 === 0 ? this.next : prev
// We allow the new value only if it is even,
// otherwise we return the old value
// if the new value is strictly equal (===) to the old value,
// the App will stop updates propagation
}
}
class App extends Cause<number> {
*whatsUp() {
const timer = new Timer()
while (true) {
yield new EvenOnly(yield* timer)
}
}
}
const app = new App()
whatsUp(app, (data) => console.log(data))
//> 0
//> 2
//> 4
//> ...
You can create custom equal filters
import { whatsUp, cause, Mutator } from 'whatsup'
class EqualArr<T> extends Mutator<T[]> {
readonly arr: T[]
constructor(arr: T[]) {
super()
this.arr = arr
}
mutate(prev) {
const { arr } = this
if (Array.isArray(prev) && prev.lenght === arr.length && prev.every((item, i) => item === arr[i])) {
return prev
}
return arr
}
}
/*
then in whatsUp generator - yield new EqualArr([...])
*/
Mutators & JSX
Fractal has its own plugin that converts jsx-tags into mutators calls. You can read the installation details here whatsup/babel-plugin-transform-jsx and whatsup/jsx
import { conse, Cause } from 'whatsup'
import { render } from '@whatsup-js/jsx'
class User extends Cause<JSX.Element> {
readonly name = conse('John')
readonly age = conse(33);
*whatsUp() {
while (true) {
yield (
<Container>
<Name>{yield* this.name}</Name>
<Age>{yield* this.age}</Age>
</Container>
)
}
}
}
const user = new User()
render(user)
// Yes, we can render without a container, directly to the body
The mutator gets the old DOMNode and mutates it into a new DOMNode the shortest way.
Fractal
It looks like a cause, but for each consumer, the fractal creates a new iterator and context. Contexts bind to the consumer context like a parent-child relation and form a context tree. This allows you to organize the transfer of factors down the tree, as well as the bubbling of events up. A cause, unlike a fractal, creates one iterator for all consumers, and one context without a reference to the parent (root context).
import { Fractal, Event, Context, factor } from 'whatsup'
import { render } from '@whatsup-js/jsx'
const Theme = factor<'light' | 'dark'>('light')
// factor determining color scheme
// Ρustom event for remove Todo
class RemoveEvent extends Event {
constructor(readonly todo: Todo) {
super()
}
}
class Todo extends Fractal<JSX.Element> {
readonly name: Conse<string>
constructor(name: string) {
this.name = conse(name)
}
*whatsUp(ctx: Context) {
const theme = ctx.find(Theme)
// get value of ^^^ Theme factor
const onClick = () => ctx.dispatch(new RemoveEvent(this))
// start event bubbling ^^^^^^^^
while (true) {
yield (
<Container theme={theme}>
<Name>{yield* this.name}</Name>
<RemoveButton onClick={onClick} />
</Container>
)
}
}
}
class Todos extends Fractal<JSX.Element> {
readonly list: List<Todo> = list()
create(name: string) {
const todo = new Todo(name)
this.list.insert(todo)
}
remove(todo: Todo) {
this.list.delete(todo)
}
*whatsUp(ctx: Context) {
ctx.share(Theme, 'dark')
// ^^^ define value of Theme factor for children contexts
ctx.on(RemoveEvent, (e) => this.remove(e.todo))
// ^^ start event listening
while (true) {
const acc = []
for (const todo of yield* this.list) {
acc.push(yield* todo)
}
yield <Container>{acc}</Container>
}
}
}
const todos = new Todos()
render(todos)
Sharing
This mechanism allows you to make any object publicly available to all children.
import { Fractal, whatsUp } from 'whatsup'
class Session {
constructor(readonly token: string) {}
}
class App extends Fractal<string> {
*whatsUp(ctx: Context) {
const session = new Session('Secret token')
const page = new Page()
ctx.share(this.session) // share Session instance
while (true) {
yield `App ${yield* page}`
}
}
}
class Page extends Fractal<string> {
*whatsUp(ctx: Context) {
const session = ctx.get(Session) // get Session instance
while (true) {
yield `Page ${session.token}`
}
}
}
whatsUp(new App(), (d) => console.log(d))
//> App Page Secret token
There is also a find
method. It looks for objects with instanceof
.
Sharing with factors
The share
method can take a factor as a share-key.
import { Fractal, whatsUp, factor } from 'whatsup'
const Theme = factor<string>('light')
// default value ^^^^^^^
class App extends Fractal<string> {
*whatsUp(ctx: Context) {
const page = new Page()
ctx.share(Theme, 'dark') // share theme value for children contexts
while (true) {
yield `App ${yield* page}`
}
}
}
class Page extends Fractal<string> {
*whatsUp(ctx: Context) {
const theme = ctx.get(Theme) // get Theme shared value
// when no factor is found (not shared in parents)
// the default value is returned - 'light'
while (true) {
yield `Page. Theme is ${theme}.`
}
}
}
whatsUp(new App(), (d) => console.log(d))
//> Page. Theme is dark.
Asynchrony (deferred job)
The context has a defer
method. This method allows you to start the execution of asynchronous code, after the execution of which ctx.update()
will be automatically called. Defer returns an object like {done: boolean, value: T}
.
import { Cause, whatsUp } from 'whatsup'
// welcome.ts
export class Welcome extends Cause<string> {
*whatsUp() {
while (true) {
yield 'Hello world'
}
}
}
// app.ts
class App extends Cause<string> {
*whatsUp(ctx) {
const deferred = ctx.defer(async function () {
const { Welcome } = await import('./welcome')
return new Welcome()
})
// deferred is {done: false}
yield 'Loading...'
// deferred is {done: true, value: Welcome}
const welcome = deferred.value
while (true) {
yield `App: ${yield* welcome}`
}
}
}
whatsUp(new App(), (d) => console.log(d))
//> Loading...
//> App: Hello world
Delegation
A useful mechanism by which a stream can delegate its work to another stream.
import { fractal, conse, whatsUp, delegate } from 'whatsup'
const Name = conse('John')
const User = fractal(function* () {
while (true) {
yield `User ${yield* Name}`
}
})
const Guest = fractal(function* () {
while (true) {
yield delegate(User) // delegate work to User
}
})
const guest = new Guest()
whatsUp(guest, (data) => console.log(data))
//> User John
In the following example, you can see what happens if a delegation is passed to the conse.
import { cause, conse, whatsUp, delegate } from 'whatsup'
const BarryName = cause(function* () {
while (true) yield 'Barry'
})
const Name = conse('John')
whatsUp(Name, (data) => console.log(data))
//> John
Name.set(delegate(BarryName))
//> Barry
Incremental & glitch-free computing
All dependencies are updated synchronously in a topological sequence without unnecessary calculations.
import { conse, cause, whatsUp } from 'whatsup'
const num = conse(1)
const evenOrOdd = cause(function* () {
while (true) {
yield (yield* num) % 2 === 0 ? 'even' : 'odd'
}
})
const isEven = cause(function* () {
while (true) {
yield `${yield* num} is ${yield* evenOrOdd}`
}
})
const isZero = cause(function* () {
while (true) {
yield (yield* num) === 0 ? 'zero' : 'not zero'
}
})
whatsUp(isEven, (data) => console.log(data))
whatsUp(isZero, (data) => console.log(data))
//> 1 is odd
//> not zero
num.set(2)
//> 2 is even
num.set(3)
//> 3 is odd
num.set(0)
//> 0 is even
//> zero
Lifecycle
Inside the generator, the keyword yield
push data to stream, and yield*
pull data from stream.
The life cycle consists of three steps:
- create a new iterator using a generator
- the iterator starts executing and stops after
yield
orreturn
, during this operation, all calls toyield*
automatically establish the observed dependencies; as soon as a new data is generated, the stream reports this to the parent and goes into standby mode for updates - having received the update message, the stream clears the list of dependencies and, if in the previous step the data was obtained using
yield
, the stream continues its work from the second step; if there was areturn
, the work continues from the first step
The return
statement does the same as the yield
statement, but it does the iterator reset, and the stream starts its life anew.
Examples
- Sierpinski - perfomance test like React sierpinski triangle. [source]
- Todos - WhatsUp realization of TodoMVC. [source]
- Loadable - an example showing how you can organize the display of loaders during background loading. I specifically added small delays there in order to slow down the processes. [source]
- Factors - work in different conditions. One and the same fractal, depending on the factor set in the context, gives three different results, and also maintains their relevance. Try editing the name and age. [source]
- Antistress - just a toy, click the balls, paint them in different colors and get cool pictures. In fact, this is a fractal that shows a circle inside itself, or three of the same fractals inscribed in the perimeter of the circle. Click - paint, long click - crush, long click in the center of the crushed circle - return to its original state. If you crush the circles to a sufficiently deep level, you can see the Sierpinski triangle. [source]