Kaiju
kaiju is a view layer used to build an efficient tree of stateless/stateful components and help you manage that tree data.
- Data management (local/global/inter-component/intra-component) is unified via stores (FSM)
- Fast, thanks to snabbdom, aggressive component rendering isolation (a key stroke in one input component should not re-evaluate the whole app) and async RAF rendering
- Changes can easily be animated (also thanks to snabbdom)
- Global and local state can optionally use Observables for greater composition
- No JS
class
/this
nonsense - Tiny size in KB
- Comes with useful logs
- First class support for typescript with a particular attention paid to type safety
Content
- Concepts
- API
- Creating a VNode with h
- Creating a component
- Altering the DOM from a component/VNode tree
- Message: Intra and inter component communication
- Create a Message
- Send a message to a Store
- Send a message to the current component
- Send a message to the nearest parent component
- Create an Observable for all messages of a given type
- Listen to all messages bubbling to a particular DOM node
- Partially apply a Message's payload
- Dealing with unhandled messages
- Logging data changes and render timing
- Full TS Example
Components: step by step guide
kaiju
adds the concept of encapsulated components to snabbdom
's pure functional virtual dom.
Standard Virtual nodes and components are composed to build a VNode
tree that can scale in size and complexity.
A VNode
is what you get when calling snabbdom
's h
function for instance.
A component is simply a function that takes an option object as an argument and returns a VNode
ready to be used inside its parent children, i.e, this is a valid array of VNodes
:
Note: typescript will be used in the examples, however the library also works just fine with javascript.
- We start with a stateless "component"
- For comparison sake, here is the simplest stateful component definition one can write:
Now, that isn't terribly useful because we really want our component to be stateful, else we would just use a regular VNode
object.
- Let's add some state, and make it change over time:
Now we created a Message
named click that is locally sent to our component whenever the user clicks on the button.
We handle that message in connect
and return the new state of our component. The component will then redraw with that new state.
Using explicit Messages instead of callbacks to update our state brings consistency with other kinds of (external) state management and makes state debugging easier since messages can be traced and logged (see logging).
In the above code, on(click)
is in fact a shortcut for on(msg.listen(click))
.
Here's the longer form:
What msg.listen(click)
returns is an Observable that emits new values (the payload of each message)
every time the message is sent.
This is very useful because observables can easily be composed:
Now, the state is only updated if we stopped clicking for 1 second.
We could also decide to just perform a side effect, instead of updating the component's state. When performing side effects (void/undefined is returned) the component is not redrawn:
Our component now has an internal state and we know how to update it. But it's also completely opaque from the outside!
In a tree of VNodes
, parents must often be able to influence the rendering of their children.
- For that purpose, we introduce props:
Now our parent can render the component with more control: It can set the default text that should be displayed initially, but also
directly sets the paragraph text of the p
tag.
When composing components, you must choose which component should own which piece of state. Disregarding global state for now, local state can reside in a component or any of its parent hierarchy.
- Let's see how we can move the previous button
text
state one level up, so that the component parent can directly change that state:
We now delegate and send a message to our direct parent component so that it can, in turn, listen to that message from its connect
function and update its own state.
Note: The child component could send the same Message to its parent (delegation) but we choose to go with an explicit onClick
property to increase semantics, cohesion and typesafety.
At this point, the component is no longer stateful and providing it didn't have any other state, should probably be refactored back to a simple function returning a VNode
:
Finally, if we wanted a generic component we could declare it like so:
Observables
kaiju
comes with an implementation of observables (also known as streams) so that components can more easily declare
how their state should change based on user input and any other observable changes in the application.
Observables are completely optional: If you are more confident with just sending messages around every time the state should update, you can do that too.
The characteristics of this observable implementation are:
- Tiny abstraction, fast
- OO style chaining
- Multicast: All observables are aware that multiple subscribers may be present
- The last value of an observable can be read by invoking the observable as a function
- Synchronous: Easier to reason about and friendlier stack traces
- No error handling/swallowing: No need for it since this observable implementation is synchronous
- No notion of an observable's end/completion for simplicity sake and since we really have two kinds of observables: never ending ones (global state), and the ones that are tied to a particular component's lifecycle
- Lazy resource management: An observable only activates if there is at least one subscriber
- If the observable already holds a value, any subscribe function will be called immediately upon registration
To see observables in action, check the example's ajax abstraction and its usage
The Observable OO API:
The Observable static API:
Component lifecycle
Creation
- The component is now included in the application VNode tree for the first time
initState()
is called with the initial propsconnect()
is called. Observables are plugged into the component, if they already hold state synchronously, the component's initial state is updated immediately.render()
is called for the first time
Both initState
and connect
are called only once when the component first appears.
Update
- At any point in time, the component will re-render if either is true:
- The parent rerenders the component with changed props (shallow comparison)
- An Observable registered in
connect
is updated, and an updated state (shallow comparison) is returned in its handler.
Synchronously sending a message to the component in its render
method is forbidden to avoid loops.
Destruction
When some parent is removed from the tree or when the component's direct parent stops including the component in its render output, the component gets destroyed. render
will never be called again and all the Observables
are unregistered from.
Additionally, for any of these phases, the snabbdom hooks can be used on any VNode returned in render
If I want to
Initiate the component state from the initial props
Return the init state in initState
:
Continuously compute a part of the component state from its props (e.g perf optimization)
Derive some state from the props Observable in connect
:
There is no need to also derive the state in initState
, since props
is
an observable that always have an initial value (the handler will be called synchronously in connect
).
Recompute the component state if its props changed in a specific way
This is a specialization of the above that avoid doing unnecessary work. We just have to remember the last props and compare it with the new ones:
Note however that for inexpensive computations, it is generally advised to simply do it in render
as it's then easier to guarantee props, state and view are in sync.
Perform a side effect when the component is added or removed
Use a snabbdom hook.
create
is called before the element is added to the DOM,
insert
is called after the element is added the DOM,
remove
is called when the node's direct parent removes this node,
destroy
is called when this node is directly or indirectly being removed from the vnode tree.
Alter the DOM when the component was rendered
Use the postpatch
snabbdom hook.
Clean up a setInterval or remove a DOM event listener when the component is removed Good news everyone! You don't need to, if you use observables. Observables are automatically cleaned up when the component is removed.
You could also reproduce the same behavior imperatively:
Instantiate/destroy a vanillaJS widget when the component is added/removed
We can send a message from a DOM hook.
Assuming we found some vanillaJS widget named widget.Map
that has a create
and destroy
method:
Note that this kind of Message sent from a DOM lifecycle hook should always perform side effects only. If the state is updated, a warning will be logged and the component will ignore that change.
Local state vs Global state
Choosing whether a particular state is local or global, whether it's very local (component leaf) or not so local (owned by a component somewhere else in the tree) is an important decision, although it can easily be refactored based on new needs.
You typically want to keep very transient state as local as possible so that it remains encapsulated in a component and do not leak up. Less stateful components are more flexible because their parents can do what they want with that component, but stateful components are more productive and less error prone, as you can skip having boilerplate to wire the same parent state => component props everywhere it's used.
Example of typically local state
- Whether a select dropdown is opened
- Whether the component is focused
- Which grid row is highlighted
- Basically any state that resets if the user navigates away then comes back
Additionally, keeping state that is only useful to one screen should be kept inside the top-most component of that screen and no higher. Else, you would have to manually clean up that state when exiting the component.
That just leaves global state, which can be updated from anywhere and is accessed from multiple screens.
Example of typically global state
- The current url route
- User preferences
- Any cached, raw domain data that will be mapped/filtered/transformed in the different screens
Stores
A construct is provided to easily build push-based observables in a type-safe manner. This is entirely optional.
If you need a piece of state to live outside a component (it's not tied to a particular component's lifecycle), or you want your components to only care about presentational logic, you can either use Observables or Stores.
The difference is that a Store's state can be updated from the outside via Messages
and is guaranteed to have an initial value whereas an Observable can only be transformed via operators.
Example:
// This exports a store containing an observable ready to be used in a component's connect function // ...// Subscribe to it in a component's connect // Provide an initial value // ...// Then anywhere else, import the store and the messageuserStore.sendsetUserName'Monique'
connectToStore
Similarly to redux, a function is provided to create a new Component/function from an existing Component/function and a selector:
It can only connect to a single store as you usually have a global store, or a very local one that can plug into other stores' data.
API
Creating a VNode with h
Creates a VNode
This is proxied to snabbdom's h so we can add our type definitions transparently.
h'div', 'hello'
On top of the snabbdom
modules you may feed to startApp
, an extra module is always installed by kaiju
: events
.
events
is like snabbdom
's own on
module except it works with Messages
instead of just any event handler.
// Send a message to the enclosing component on click and on mousedownh'div', // Or prepare the message to be sent with an argument.// This is more efficient than creating a closure on every render. h'div',
Creating a component
The Component
factory function takes an object with the following properties:
name
Mandatory String
This is the standard Virtual DOM key
used in the diffing algorithm to uniquely identify this VNode
.
It is also used for logging purposes, so it is usually just the name of the component.
By default, components have a key
set to their name
to differentiate them from other components.
However, you can also set an external key
by defining a key property inside the Component's props. The overall key will then be name + _ + your key.
This can be useful when switching between two instances of the same component but without reusing any of its state.
sel
Optional String
An alternative hyperscript selector to use instead of component
.
Component
props
Optional Object
An object representing all the properties passed by our parent.
Typically props either represents state that is maintained outside the component or properties used to tweak the component's behavior.
The render
function will be called if the props object changed shallowly (any of its property references changed), hence it's a good practice to try and use a flat object.
Note 1: props and state are separated exactly like in React
as it works great. The same design best practices apply.
Note 2: If you wish to compute some state or generally perform a side effect based on whether some part of the props changed (similar to using componentWillReceiveProps
in react) you can use the sliding2 combinator to compare the previous props with the new ones:
onprops.sliding2,...
initState
Mandatory Object
A function taking the initial props as an argument and returning the starting state.
Note: Any synchronous observables further modifying the state in connect
will effectively change the state used for the first render.
connect
Mandatory function({ on, msg, props, state }: ConnectParams<Props, State>): void
Connects the component to the app and computes the local state of the component.
connect
is called only once when the component is mounted.
connect
is called with four arguments, encapsulated in a ConnectParams
object:
-
on
registers aMessage
orObservable
that modifies the component local state. The Observable will be automatically unsubscribed from when the component is unmounted.
Returning the current state orundefined
in anon
handler will skip rendering and can be used to do side effects. -
msg
is the interface used to send and listen to messages.
Full interface:
/** * Listens for a message sent from local VNodes or component children */listenmessage: Message<P>: Observable<P> /** * Listens to all messages bubbling up to a particular DOM node * * Example: * const overlayMessages = msg.listenAt('#popupLayer .overlay') * * Note: The DOM Element must be available at the time the function is called. */listenAttarget: string | Element: Observable<MessagePayload<>> /** * Sends a message to self. * * Example: * msg.send(AjaxSuccess([1, 2])) */sendpayload: MessagePayload<P>: void /** * Sends a message to this component's nearest parent. * * Example: * msg.sendToParent(ItemSelected(item)) */sendToParentpayload: MessagePayload<P>: void
props
An Observable with a new value every time the props passed by our parent changed. It is often enough to simply let therender
function take care of these new props but advanced users may sometimes want to derive some state from props.
Just like with props, a redraw will only get scheduled if the state object changed shallowly.
state
The Observable current state.
render
Mandatory function({ props, state, msg }: RenderParams<Props, State>): VNode | Node[]
Returns the current VNode tree of the component based on its props and state.
You can also return an Array of Node
s, where a Node
is either a VNode
, a string
, null
or undefined
.
Example:
startApp
Installs and performs the initial render of the app synchronously.
startApp
For more information about snabbdom modules see the official documentation
Render.into
This function is made available after the app was created with startApp
.
This can be used to create some advanced components with their own internal rendering needs (e.g: Efficient popups, alerts, etc).
It renders either synchronously if called from an ongoing rendering phase, or asynchronously.
// Creates a new div as a child of bodyRender.intodocument.body, firstVDom // Patch that div so that it becomes a span
Render.scheduleDOMWrite
The virtual DOM abstraction pretty much guarantees an optimal way of creating and updating the DOM in a single pass, without any layout trashing.
Sometimes, however, you may want to further alter the DOM in an imperative way when it's not possible to have a straightforward state->view binding.
Kaiju provides two functions to do that without causing layout trashing:
Render.scheduleDOMRead
and Render.scheduleDOMWrite
.
Both are called at the end of a render cycle (so still inside a requestAnimationFrame context)
The reads and writes are batched, reads are called first.
// Called within an insert hook
Message
Stores and Component can both send and listen to message. Indeed, each component has a private Store to manage its state. Messages help debugging and communicate intent better than generic model-altering callbacks. Here's what you can do with messages:
Store.
Creating a custom application message used to either communicate between components or send to a // Message taking no arguments // Message taking one argument // Message taking a tuple// Then pre-bind it so it can be used directly in the DOM:incrementBy2.with33 // Message<Event>
To store references of messages with a specific number of payloads, use:
Sending a message to a Store instance (usually to update application/domain state)
See store.send
- Sending a message to the current Component
Sending a message to the nearest parent Component
Create an Observable for all messages of a given type
msg.listen
creates an Observable publishing every Message
of that type.
This can be useful to transform the observable before handling the Message or creating reusable abstractions.
Listen to all Messages bubbling up a particular DOM Element
This should rarely be useful. It can be used when a Component (e.g a popup) renders its content in another part of the DOM tree and Messages should be listened from there instead of locally.
Partially apply a Message's payload
Use cases
- Reuse a Message inside a Component's VDOM but with a different payload
- Set part of the payload of a child's callback Message with information only useful to the parent (e.g, which child was this?)
Note 1: Partially applying a Message has a little performance cost, roughly equal to a lambda creation. However, unlike in some other VDOM frameworks, the component will not re-render if the payload wasn't actually changed.
Note2: A partially applied Message is only to be used for sending, not receiving. Always listen to the original Message.
Catching unhandled messages
Messages sent by the events
snabbdom module or when using Messages.sendToParent
will bubble up the DOM till it finds the nearest parent component.
Sometimes, this is not wanted as the nearest component could be a generic component that shouldn't listen to your business messages, only to its own messages.
For instance, inside a utility component, we could forward any messages we're not interested in to our parent (e.g explicit bubbling):
Logging data changes and render timing
kaiju
has useful logging to help you debug or visualize the data flows.
By default, nothing is logged, but that can be changed:
log.render = truelog.message = true
Additionally, you can specify which component gets logged using the component's name
:
log.render = 'select'log.message = 'popup'
You will want to change the log values as early as possible in your program so that no logs are missed.
Note: The render durations are more interesting as a relative measurement to spot bottlenecks and focus any optimization effort.
The absolute durations may be heavily influenced by the console
itself sometimes being very slow.
Full TS Example
A full application example using TypeScript