From 949fe935cbecf92a2359362f69307453a373ee31 Mon Sep 17 00:00:00 2001 From: Fulop <43729152+fulopkovacs@users.noreply.github.com> Date: Wed, 6 Apr 2022 17:28:08 +0200 Subject: [PATCH] Improve the documentation of dataverse (#116) --- packages/dataverse/README.md | 5 +- packages/dataverse/docs/GET_STARTED.md | 673 ++++++++++++++++++ .../dataverse/src/derivations/prism/prism.ts | 8 + packages/react/src/index.ts | 18 +- 4 files changed, 698 insertions(+), 6 deletions(-) create mode 100644 packages/dataverse/docs/GET_STARTED.md diff --git a/packages/dataverse/README.md b/packages/dataverse/README.md index 164f4e3..0735646 100644 --- a/packages/dataverse/README.md +++ b/packages/dataverse/README.md @@ -1,11 +1,10 @@ # @theatre/dataverse -Dataverse is the reactive dataflow library [Theatre.js](https://www.theatrejs.com) is built on. It is inspired by ideas in [functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming) and it is optimised for interactivity and animation. +Dataverse is the reactive dataflow library [Theatre.js](https://www.theatrejs.com) is built on. It is inspired by ideas in [functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming) and it is optimised for interactivity and animation. Check out the [Get started guide](./docs/GET_STARTED.md) for a more practical introduction. Dataverse is currently an internal library. It is used within Theatre.js, but its API is not exposed through Theatre. This is so that we can iterate on the reactive internals while keeping the public API stable. We plan to eventually have an LTS release, but for now, expect a high release cadence and API churn in dataverse while the API in Theatre.js remains stable. ## Documentation * API Docs: [docs.theatrejs.com/api/dataverse](https://docs.theatrejs.com/api/dataverse.html) -* Guide: TODO - +* Get started guide: [GET_STARTED.md](./docs/GET_STARTED.md) diff --git a/packages/dataverse/docs/GET_STARTED.md b/packages/dataverse/docs/GET_STARTED.md new file mode 100644 index 0000000..443f777 --- /dev/null +++ b/packages/dataverse/docs/GET_STARTED.md @@ -0,0 +1,673 @@ +# Into the dataverse - Get started with `@theatre/dataverse` + +This guide will help you to get started with `dataverse`, the reactive dataflow +library that [Theatre.js](https://www.theatrejs.com) is built on. It is inspired +by ideas in +[functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming) +and it is optimised for interactivity and animation. + +## Main concepts + +A good analogy for `dataverse` would be a spreadsheet editor application. In a +spreadsheet editor you have cells that store values, cells that store functions, +that manipulate the values of other cells. The cells have identifiers (e.g. +`A1`, `B3`, etc...) that are used to reference them in the functions. These are +similar to the set of tools that `dataverse` provides for manipulating data. +Here's a quick comparison: + +| `dataverse` | Spreadsheet editor analogy | role | +| ----------------------- | ----------------------------------- | ---------------------------------------------------------------------------------- | +| sources (`Box`, `Atom`) | a cell that holds a value | `Box`: holds a simple value, `Atom`: holds an object with (sub)props to be tracked | +| derivations | functions | changes recorded on the value of an `Box` or `Atom` | +| pointers | addresses of the cells (`A1`, `B3`) | they point to a (sub)prop of an `Atom` | + +Note that some concepts in `dataverse` go beyond the spreadsheet analogy. + +## Practical Introduction + +Here we collected a few examples that introduce the main concepts/tools in +`dataverse` through practical examples. We strongly recommend running the +examples on your local machine (see the [Setup](#setup) section to see how to +configure your local environment before running the examples). + +0. [Setup your local environment for running the examples](#setup) +1. [`Box`](#box-store-simple-values) +2. [Observe values](#observe-values) +3. [`map()`](#map) +4. [`prism()`](#prism) + - [A basic example](#a-basic-example) + - [`prism.state()` and `prism.effect()`](#prismstate-and-prismeffect) + - [Other methods of `prism()`](#other-methods-of-prism) +5. [`usePrism()` (from `@theatre/react`)](#useprism) +6. [`Atom`](#atom) + - [`Atom` vs `Box`](#atom-vs-box) + - [`Pointers`](#pointers) + - [Update the value of an `Atom`](#update-the-value-of-an-atom) +7. [`Ticker`](#ticker-and-studioticker) + +### Setup + +You are encouraged to follow the examples on your machine by cloning the +[`theatre-js/theatre`](https://github.com/theatre-js/theatre/) repo and creating +a new directory and file called `dataverse/index.tsx` in +`theatre/packages/playground/src/personal/` (this directory is already added to +`.gitignore`, so you don't have to worry about that). + +### `Box`: store simple values + +Let's start with creating a variable that holds a simple value, which we can +change and observe later: + +```typescript +import {Box} from '@theatre/dataverse' + +// `theatre/packages/playground/src/personal/dataverse/index.tsx` + +const variableB = new Box('some value') +console.log(variableB.get()) // prints 'some value' in the console +``` + +> As you can see there's a naming convention here for boxes (`variableB`), +> pointers (`variableP`), derivations (`variableP`), etc... + +Now we can change the value: + +```typescript +variableB.set('some new value') +console.log(variableB.get()) // prints 'some new value' in the console +``` + +### Observe values + +Let's say you want to watch the value of `variableB` for changes and execute a +callback when it does change. + +```typescript +import {Box} from '@theatre/dataverse' + +const variableB = new Box('some value') +// Change the value of variableB to a random number in every 1000 ms +const interval = setInterval(() => { + variableB.set(Math.random().toString()) + console.log('isHot?', variableB.derivation.isHot) +}, 1000) + +// Watch `variableB` changes and print a message to the console when the value of +// `variableB` changes +const untap = variableB.derivation.changesWithoutValues().tap(() => { + console.log('value of variableB changed', variableB.derivation.getValue()) +}) + +// Stop observing `variableB` after 5000 ms +setTimeout(untap, 5000) + +// Clear the interval after 7000 ms +setTimeout(() => { + clearInterval(interval) + console.log('Interval cleared.') +}, 7000) +``` + +A few notes about the example above: + +- `variableB.derivation.changesWithoutValues()` returns a tappable that we can + tap into (observe). +- The `tap()` method returns the `untap()` function which aborts th +- As long as `variableB` is tapped (observed) `variableB.derivation.isHot` will + bet set to `true` automatically + +What if you want to keep a derivation hot manually even if there's no tappable +attached to it anymore? In this case you can use the `keepHot()` method as seen +below: out this modified version of the previous example: + +```typescript +variableB.set('some new value') +console.log(variableB.get()) // prints 'some new value' in the console + +// Change the value of variableB to a random number in every 1000 ms +const interval = setInterval(() => { + variableB.set(Math.random().toString()) + // This will print 'isHot? true' every time, since we kept + // the derivation hot by calling the 'keepHot()' method + console.log('isHot?', variableB.derivation.isHot) +}, 1000) + +// Watch `variableB` changes and print a message to the console when the value of +// `variableB` changes +const untap = variableB.derivation.changesWithoutValues().tap(() => { + console.log('value of variableB changed', variableB.derivation.getValue()) +}) + +// Stop observing `variableB` after 5000 ms +setTimeout(untap, 5000) + +// Keep the derivation hot +variableB.derivation.keepHot() + +// Clear the interval after 7000 ms +setTimeout(() => { + clearInterval(interval) + console.log('Interval cleared.') +}, 7000) +``` + +### `map()` + +It is also possible to create a derivation based on an existing derivation: + +```typescript +const niceNumberB = new Box(5) +const isNiceNumberEvenD = niceNumberB.derivation.map((v) => v % 2 === 0) + +// the following line will print '5, false' to the console +console.log(niceNumberB.get(), isNiceNumberEvenD.getValue()) +``` + +The new derivation will be always up to date with the value of the original +derivation: + +```typescript +import {Box} from '@theatre/dataverse' + +const niceNumberB = new Box(5) +const isNiceNumberEvenD = niceNumberB.derivation.map((v) => + v % 2 === 0 ? 'even' : 'odd', +) + +const untap = isNiceNumberEvenD.changesWithoutValues().tap(() => {}) + +const interval1 = setInterval(untap, 5000) +const interval2 = setInterval(() => { + niceNumberB.set(niceNumberB.get() + 1) + console.log( + `${niceNumberB.get()} is an ${isNiceNumberEvenD.getValue()} number.`, + ) +}, 1000) + +// clear the intervals +setTimeout(() => { + clearInterval(interval1) + console.log('interval1 is cleared.') +}, 7000) + +setTimeout(() => { + clearInterval(interval2) + console.log('interval2 is cleared.') +}, 7000) +``` + +### `prism()` + +At this point we can make derivations that track the value of an other +derivation with [the `.map()` method](#map), but what if we want to track the +value of multiple derivations at once for the new derivation? This is where the +`prism()` function comes into play. + +#### A basic example + +Let's say that we have two derivations and we want to create a derivation that +returns the product of their values. In the spreadsheet analogy it would be like +having two cells with two functions and third cell that contains a function that +calculates the product of the previous two cells. Whenever the first two cells +recalculate their value, the third cell will also do the same. + +Here's how we would solve this problem in `dataverse`: + +```typescript +import {Box, prism} from '@theatre/dataverse' + +const widthB = new Box(1) +const heightB = new Box(2) +const padding = 5 + +const widthWithPaddingD = widthB.derivation.map((w) => w + padding) +const heightWidthPaddingD = heightB.derivation.map((h) => h + padding) + +const areaD = prism(() => { + return widthWithPaddingD.getValue() * heightWidthPaddingD.getValue() +}) + +console.log('area: ', areaD.getValue()) +widthB.set(10) +console.log('new area: ', areaD.getValue()) +``` + +#### `prism.state()` and `prism.effect()` + +Prisms don't always follow the rules of functional programming: they can have +internal states and perform side effects using the `prism.state()` and +`prism.effect()` methods. Their concept and API is very similar to React's +`useState()` and `useEffect()` hooks. + +The important thing to know about them is that: + +- `prism.state()` returns a state variable and a function that updates it. +- `prism.effect()` receives two arguments: + 1. The first one is a key (a string), which should be unique to this effect + inside the prism + 2. The second one is a callback function as an argument that gets executed + when the derivation is created (or the dependencies in the dependency array + change). The callback function may return a clean up function that runs + when the derivation gets updated or removed. + +Let's say you want to create a derivation that tracks the position of the mouse. +This would require the derivation to do the following steps: + +1. Create an internal state where the position of the mouse is stored +2. Attach an event listener that listens to `mousemove` events to the `document` +3. Update the internal state of the position whenever the `mousemove` event is + fired +4. Remove the event listener once the derivation is gone (clean up) + +This is how this derivation would look like in code: + +```typescript +import {prism} from '@theatre/dataverse' + +const mousePositionD = prism(() => { + // Create an internal state (`pos`) where the position of the mouse + // will be stored, and a function that updates it (`setPos`) + const [pos, setPos] = prism.state<[x: number, y: number]>('pos', [0, 0]) + + // Create a side effect that attaches the `mousemove` event listeners + // to the `document` + prism.effect( + 'setupListeners', + () => { + const handleMouseMove = (e: MouseEvent) => { + setPos([e.screenX, e.screenY]) + } + document.addEventListener('mousemove', handleMouseMove) + + // Clean up after the derivation is gone (remove the event + // listener) + return () => { + document.removeEventListener('mousemove', handleMouseMove) + } + }, + [], + ) + + return pos +}) + +// Display the current position of the mouse using a `h2` element +const p = document.createElement('h2') +const [x, y] = mousePositionD.getValue() +p.textContent = `Position of the cursor: [${x}, ${y}]` +document.querySelector('body')?.append(p) + +// Update the element's content when the position of the mouse +// changes +mousePositionD.changesWithoutValues().tap(() => { + const [x, y] = mousePositionD.getValue() + p.textContent = `Position of the cursor: [${x}, ${y}]` +}) +``` + +#### Other methods of `prism` + +Prism has other methods (`prism.memo()`, `prism.scope()`, `prism.ref()`, etc) +inspired by React hooks, but they aren't used that much in `@theatre/core` and +`@theatre/studio`. You can check out the +[tests](../src/derivations/prism/prism.test.ts) or the +[source code](../src/derivations/prism/prism.ts) to get more familiar with them. + +### `usePrism()` + +You can also use derivations inside of React components with the `usePrism()` +hook from the `@theatre/react` package, which accepts a dependency array for the +second argument. If the prism uses a value that is not a derivation (such as a +simple number, or a pointer), then you need to provide that value to the +dependency array. + +#### A simple example + +Here's a simple example: we have a Box that contains the width and height of a +div (let's call it `panel`). Imagine that we want to have a button that changes +the width of the `panel` to a random number when clicked. + +```typescript +import {Box} from '@theatre/dataverse' +import {usePrism} from '@theatre/react' +import React from 'react' +import ReactDOM from 'react-dom' + +// Set the original width and height +const panelB = new Box({ + dims: {width: 200, height: 100}, +}) + +function changePanelWidth() { + const oldValue = panelB.get() + // Change `width` to a random number between 0 and 200 + panelB.set({dims: {...oldValue.dims, width: Math.round(Math.random() * 200)}}) +} + +const Comp = () => { + const render = usePrism(() => { + const {dims} = panelB.derivation.getValue() + return ( + <> + +
+ > + ) + }, [panelB]) // Note that `panelB` is in the dependency array + + return render +} + +ReactDOM.render( +