Remove Derivation.changesWithoutValues()
And replace it with `Derivation.onStale()`
This commit is contained in:
parent
9094e3041e
commit
5c1aa1cd50
13 changed files with 19 additions and 916 deletions
|
@ -1,725 +1,3 @@
|
||||||
# Into the dataverse - Get started with `@theatre/dataverse`
|
# Into the dataverse - Get started with `@theatre/dataverse`
|
||||||
|
|
||||||
This guide will help you to get started with `dataverse`, the reactive dataflow
|
> TODO: Dataverse has changed quite a bit since this guide was written, so let's write a new one.
|
||||||
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 (`variableD`), 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 derivation / tappable that we can
|
|
||||||
tap into (observe).
|
|
||||||
- The `tap()` method returns the `untap()` function which unsubscribes the observer function
|
|
||||||
- As long as `variableB` is tapped (observed) `variableB.derivation.isHot` will
|
|
||||||
bet set to `true` automatically
|
|
||||||
|
|
||||||
## Hotness
|
|
||||||
As we saw above, derivations may or may not be "hot"
|
|
||||||
(the same concept as "hot" Observables<sup>[ref](https://medium.com/codingthesmartway-com-blog/getting-started-with-rxjs-part-3-hot-and-cold-observables-4713757c9a88)</sup>). A derivation
|
|
||||||
is hot if and only if it is being tapped.
|
|
||||||
|
|
||||||
If you want to keep a derivation hot manually even if there's no tappable
|
|
||||||
attached to it anymore, you can use the `keepHot()` method. Why would you
|
|
||||||
want to keep a derivation hot? Check out this example:
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
without `keepHot()` 🥶
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
with `keepHot()` 🥵
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const variableD = prism(() => {
|
|
||||||
return performance.now()
|
|
||||||
})
|
|
||||||
console.log(variableD.getValue()) // e.g. 113.5
|
|
||||||
console.log(variableD.getValue()) // e.g. 114
|
|
||||||
// Notice they give different values
|
|
||||||
```
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const variableD = prism(() => {
|
|
||||||
return performance.now()
|
|
||||||
})
|
|
||||||
variableD.keepHot()
|
|
||||||
console.log(variableD.getValue()) // e.g. 113
|
|
||||||
console.log(variableD.getValue()) // e.g. 113
|
|
||||||
// Notice they give the same value!
|
|
||||||
```
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
To see a full example of `keepHot`, check out this modified version
|
|
||||||
of the example from the section above:
|
|
||||||
|
|
||||||
```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)
|
|
||||||
```
|
|
||||||
|
|
||||||
How does `keepHot` work? It's super simple, it just adds a tap to the derivation ([source](https://github.com/theatre-js/theatre/blob/aec6b2a25135e6264e7529e7d3800c4bc3badee6/packages/dataverse/src/derivations/AbstractDerivation.ts#L103-L105)).
|
|
||||||
|
|
||||||
### `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('pos', {x: 0, y: 0})
|
|
||||||
|
|
||||||
// Create a side effect that attaches the `mousemove` event listeners
|
|
||||||
// to the `document`
|
|
||||||
prism.effect(
|
|
||||||
'setupListeners',
|
|
||||||
() => {
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
setPos({x: e.screenX, y: 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 (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
height: 25,
|
|
||||||
width: 200,
|
|
||||||
}}
|
|
||||||
onClick={() => changePanelWidth()}
|
|
||||||
>
|
|
||||||
Change the width
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'block'
|
|
||||||
width: dims.width,
|
|
||||||
height: dims.height,
|
|
||||||
backgroundColor: '#bd6888',
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}, [panelB]) // Note that `panelB` is in the dependency array
|
|
||||||
|
|
||||||
return render
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<div>
|
|
||||||
<Comp />
|
|
||||||
</div>,
|
|
||||||
document.querySelector('body'),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### The dependency array
|
|
||||||
|
|
||||||
If you remove `panelB` from the dependency array in the previous example you
|
|
||||||
might see that there's no change in the functionality of the `Change the width`
|
|
||||||
button. It surprisingly still works:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ...
|
|
||||||
const Comp = () => {
|
|
||||||
const render = usePrism(() => {
|
|
||||||
// ...
|
|
||||||
}, []) // Here we removed `panelB` from the dependency array
|
|
||||||
|
|
||||||
return render
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
The reason behind this behavior is that even though the value of `panelB` - the
|
|
||||||
`Box` instance - is cached, the cached `Box` instance's value is still tracked
|
|
||||||
inside the callback function (which uses `prism()` under the hood, and handles
|
|
||||||
every derivation inside as its dependency). However, if you change the value of
|
|
||||||
the `panelB` variable to another `Box` instance, then that change won't be
|
|
||||||
recognized inside the callback function if `panelB` is not included in the
|
|
||||||
dependency array of `usePrism()`. Let's look at another example to make things a
|
|
||||||
bit more clear:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// Set the original width and height
|
|
||||||
const panelB = new Box({
|
|
||||||
dims: {width: 200, height: 100},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create two new `Box` instances
|
|
||||||
const theme1B = new Box({backgroundColor: '#bd6888', opacity: 1})
|
|
||||||
const theme2B = new Box({backgroundColor: '#5ac777', opacity: 1})
|
|
||||||
|
|
||||||
function changePanelWidthAndThemeOpacity() {
|
|
||||||
const oldValue = panelB.get()
|
|
||||||
// Change `width` to a random number between 0 and 200
|
|
||||||
const width = Math.round(Math.random() * 200)
|
|
||||||
panelB.set({dims: {...oldValue.dims, width}})
|
|
||||||
// Change opacity in the themes:
|
|
||||||
const opacity = width > 100 ? width / 200 : width / 100
|
|
||||||
theme1B.set({...theme1B.get(), opacity})
|
|
||||||
theme2B.set({...theme2B.get(), opacity})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPENDENCY ARRAYS DEMO
|
|
||||||
const Comp = () => {
|
|
||||||
// Get the width of the panel
|
|
||||||
const {width} = panelB.derivation.getValue().dims
|
|
||||||
// If the width of the panel is greater than 100, then
|
|
||||||
// set the value of the `theme` variable to `theme1B`,
|
|
||||||
// otherwise use `theme2B`
|
|
||||||
const theme = width > 100 ? theme1B : theme2B
|
|
||||||
|
|
||||||
const render = usePrism(() => {
|
|
||||||
const {dims} = panelB.derivation.getValue()
|
|
||||||
const {backgroundColor, opacity} = theme.get()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
height: 25,
|
|
||||||
width: 200,
|
|
||||||
}}
|
|
||||||
onClick={() => changePanelWidthAndThemeOpacity()}
|
|
||||||
>
|
|
||||||
Change the width
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
width: dims.width,
|
|
||||||
height: dims.height,
|
|
||||||
opacity,
|
|
||||||
backgroundColor,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
// Note that if the `theme` variable weren't included in the
|
|
||||||
// dependency array, then the background color of the div
|
|
||||||
// wouldn't be updated (the opacity still would).
|
|
||||||
// (Feel free to try it out.)
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
return render
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
If you omit the `theme` variable from the previous example, then the background
|
|
||||||
color of the `div` element will not be updated when the value of the `theme`
|
|
||||||
variable does, while the opacity would track the changes of the width. This
|
|
||||||
happens, because in that case the callback function in `usePrism()` caches the
|
|
||||||
value of `theme`, which is `theme1B` when `usePrims()` is called for the first
|
|
||||||
time, and updates whenever `theme1B` changes. If you pass down `theme` as a
|
|
||||||
dependency to `usePrism()`, then the callback function will always use new new
|
|
||||||
value of `theme` (which is set to `theme2B` if the `div`'s width is smaller than
|
|
||||||
or equal to `100`), whenever it changes.
|
|
||||||
|
|
||||||
### `Atom`
|
|
||||||
|
|
||||||
Remember how we compared `Box`-es to cells in the spreadsheet-analogy? `Atom`-s
|
|
||||||
are also like cells in the sense that they also hold a value (although they only
|
|
||||||
work with objects), but there's a huge difference in how their value gets
|
|
||||||
updated.
|
|
||||||
|
|
||||||
#### `Atom` vs `Box`
|
|
||||||
|
|
||||||
`Box` uses strict equality for comparing new and old values, while `Atom` tracks
|
|
||||||
the individual properties and nested properties of an object. The following
|
|
||||||
example illustrates this difference between the two pretty well:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {Atom, Box, val, valueDerivation} from '@theatre/dataverse'
|
|
||||||
|
|
||||||
const originalValue = {width: 200, height: 100}
|
|
||||||
|
|
||||||
// Create a `Box` that holds an object
|
|
||||||
const panelB = new Box(originalValue)
|
|
||||||
|
|
||||||
console.log('old value (Box): ', panelB.derivation.getValue())
|
|
||||||
// Print the new value of `panelB` to the console
|
|
||||||
// every time it changes
|
|
||||||
panelB.derivation
|
|
||||||
.changesWithoutValues()
|
|
||||||
.tap(() => console.log('new value: (Box) ', panelB.derivation.getValue()))
|
|
||||||
|
|
||||||
// Set the value of the `panelB` to a new object that has
|
|
||||||
// the same properties with the same values as `panelB`.
|
|
||||||
// Note that this will get recognized as a change, since
|
|
||||||
// the two objects are not strictly equal.
|
|
||||||
panelB.set({...panelB.get()})
|
|
||||||
|
|
||||||
// Create an `Atom` that holds an object
|
|
||||||
const panelA = new Atom({width: 200, height: 100})
|
|
||||||
|
|
||||||
console.log('old value (Atom):', val(panelA.pointer))
|
|
||||||
|
|
||||||
// Create a derivation to track the value of `panelA`
|
|
||||||
// There are a lot of new information here, we'll come back
|
|
||||||
// to this line later.
|
|
||||||
const panelFromAtomD = valueDerivation(panelA.pointer)
|
|
||||||
|
|
||||||
// Print the new value of `panelA` to the console
|
|
||||||
// every time it changes
|
|
||||||
panelFromAtomD
|
|
||||||
.changesWithoutValues()
|
|
||||||
.tap(() => console.log('new value (Atom):', val(panelA.pointer)))
|
|
||||||
|
|
||||||
// Since the next line sets changes the value of `panelA` to what it
|
|
||||||
// already holds, it does not get recognized as a change.
|
|
||||||
// The `.setIn()` method is also new, we'll cover it later.
|
|
||||||
panelA.setIn(['width'], 200)
|
|
||||||
|
|
||||||
// The next line will trigger a change as expected
|
|
||||||
panelA.setIn(['width'], 400)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Pointers
|
|
||||||
|
|
||||||
You might have wondered what `val(panelA.pointer)` meant when you read this
|
|
||||||
line:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
console.log('old value (Atom):', val(panelA.pointer))
|
|
||||||
```
|
|
||||||
|
|
||||||
`dataverse` uses pointers that point to the properties and nested properties of
|
|
||||||
the object that the `Atom` instance holds as its value.
|
|
||||||
|
|
||||||
You can use the pointers to get the value of the property they point to, or to
|
|
||||||
convert them to a derivation using the `val()` and `useDerivation()` functions:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const panelA = new Atom({width: 200, height: 100})
|
|
||||||
|
|
||||||
// Create a derivation
|
|
||||||
const panelFromAtomD = valueDerivation(panelA.pointer)
|
|
||||||
|
|
||||||
// Print the value of the property that belongs to the pointer
|
|
||||||
// Note that `panelA.pointer` and `panelA.pointer.width` are both
|
|
||||||
// pointers.
|
|
||||||
console.log(val(panelA.pointer)) // prints `{width: 200, height: 100}`
|
|
||||||
console.log(val(panelA.pointer.width)) // prints `100`
|
|
||||||
console.log(val(panelA.pointer.height)) // prints `200`
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Update the value of an `Atom`
|
|
||||||
|
|
||||||
If you want to update the value of an `Atom`, you have first choose the
|
|
||||||
property/nested property that you want to update. Then you can use the names of
|
|
||||||
its ancestor properties in an array to define the path to the property for the
|
|
||||||
`setIn()` method:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const panelA = new Atom({dims: {width: 200, height: 100}})
|
|
||||||
|
|
||||||
// Sets the value of panelA to `{dims: {width: 400, height: 100}}`
|
|
||||||
panelA.setIn(['dims', 'width'], 400)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `Ticker` and `studioTicker`
|
|
||||||
|
|
||||||
The `Ticker` class helps us to schedule callbacks using a strategy. One such
|
|
||||||
strategy could by synchronizing the execution of certain actions with the
|
|
||||||
browser's repaint schedule to avoid changes that are invisible for the user and
|
|
||||||
would only worsen the performance. This could be implemented using the
|
|
||||||
`studioTicker` from the `@theatre/studio` package.
|
|
||||||
|
|
||||||
Here's an example: we want to increase the value of a counter variable by 1 in
|
|
||||||
every 10 ms and print the current value on every repaint for 1 s:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {Box} from '@theatre/dataverse'
|
|
||||||
import studioTicker from '@theatre/studio/studioTicker'
|
|
||||||
|
|
||||||
// Clear the console to make tracking the relevant logs easier
|
|
||||||
console.clear()
|
|
||||||
|
|
||||||
// Create a counter variable
|
|
||||||
const counterB = new Box(0)
|
|
||||||
// Create a variable to track the number of repaints
|
|
||||||
let numberOfRepaints = 0
|
|
||||||
|
|
||||||
// Increase the value of the counter variable by 1
|
|
||||||
// in every 10 ms
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
counterB.set(counterB.get() + 1)
|
|
||||||
console.log(counterB.get())
|
|
||||||
}, 10)
|
|
||||||
|
|
||||||
// Increase the number of repaints by one every time
|
|
||||||
// a repaint happens
|
|
||||||
const untap = counterB.derivation
|
|
||||||
.changes(studioTicker)
|
|
||||||
.tap((newCounterValue) => {
|
|
||||||
++numberOfRepaints
|
|
||||||
console.log(`VALUE ON REPAINT: ${newCounterValue}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clean up everything after 1 s
|
|
||||||
setTimeout(() => {
|
|
||||||
clearInterval(interval)
|
|
||||||
untap()
|
|
||||||
console.log('interval is cleared.')
|
|
||||||
console.log(`Number of times when the counter got updated: ${counterB.get()}`)
|
|
||||||
console.log(`Number of repaints: ${numberOfRepaints}`)
|
|
||||||
}, 1000)
|
|
||||||
```
|
|
||||||
|
|
||||||
When I run the example above using a 60 Hz refresh rate monitor (or when the
|
|
||||||
browser itself limits the repaints to 60 times per second), I see something like
|
|
||||||
this in the console:
|
|
||||||
|
|
||||||
```
|
|
||||||
Number of times when the counter got updated: 98
|
|
||||||
Number of repaints: 60
|
|
||||||
```
|
|
||||||
|
|
||||||
What happens is that the counter gets updated about `1000ms / 10ms = 100` times,
|
|
||||||
but only 60 of these changes can be shown on screen due to the refresh rate of
|
|
||||||
my monitor. The values of the counter when the repaints happen are also printed
|
|
||||||
to the console:
|
|
||||||
|
|
||||||
```
|
|
||||||
...
|
|
||||||
94
|
|
||||||
VALUE ON REPAINT: 94
|
|
||||||
95
|
|
||||||
96
|
|
||||||
VALUE ON REPAINT: 96
|
|
||||||
97
|
|
||||||
98
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
We only covered the basics, there are much more to `Box`-es, `Atom`-s and
|
|
||||||
everything else in `dataverse`. You can always check the source code for more
|
|
||||||
information.
|
|
|
@ -23,13 +23,7 @@ export interface IDerivation<V> {
|
||||||
*/
|
*/
|
||||||
changes(ticker: Ticker): Tappable<V>
|
changes(ticker: Ticker): Tappable<V>
|
||||||
|
|
||||||
/**
|
onStale(cb: () => void): VoidFn
|
||||||
* Like {@link changes} but with a different performance model. `changesWithoutValues` returns a {@link Tappable} that
|
|
||||||
* updates every time the derivation is updated, even if the value didn't change, and the callback is called without
|
|
||||||
* the value. The advantage of this is that you have control over when the derivation is freshened, it won't
|
|
||||||
* automatically be kept fresh.
|
|
||||||
*/
|
|
||||||
changesWithoutValues(): Tappable<void>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep the derivation hot, even if there are no tappers (subscribers).
|
* Keep the derivation hot, even if there are no tappers (subscribers).
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
import type {$FixMe} from '../types'
|
|
||||||
import AbstractDerivation from './AbstractDerivation'
|
|
||||||
import type {IDerivation} from './IDerivation'
|
|
||||||
import {isDerivation} from './IDerivation'
|
|
||||||
|
|
||||||
enum UPDATE_NEEDED_FROM {
|
|
||||||
none = 0,
|
|
||||||
dep = 1,
|
|
||||||
inner = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeFlatMapDerivationClass = () => {
|
|
||||||
// TODO once prism and AbstractDerivation are merged into one, we should delete this file
|
|
||||||
class FlatMapDerivation<V, DepType> extends AbstractDerivation<V> {
|
|
||||||
private _innerDerivation: undefined | null | IDerivation<V>
|
|
||||||
private _staleDependency: UPDATE_NEEDED_FROM
|
|
||||||
|
|
||||||
static displayName = 'flatMap'
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly _depDerivation: IDerivation<DepType>,
|
|
||||||
readonly _fn: (v: DepType) => IDerivation<V> | V,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this._innerDerivation = undefined
|
|
||||||
this._staleDependency = UPDATE_NEEDED_FROM.dep
|
|
||||||
|
|
||||||
this._addDependency(_depDerivation)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
_recalculateHot() {
|
|
||||||
const updateNeededFrom = this._staleDependency
|
|
||||||
this._staleDependency = UPDATE_NEEDED_FROM.none
|
|
||||||
|
|
||||||
if (updateNeededFrom === UPDATE_NEEDED_FROM.inner) {
|
|
||||||
// @ts-ignore
|
|
||||||
return this._innerDerivation.getValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
const possibleInnerDerivation = this._fn(this._depDerivation.getValue())
|
|
||||||
|
|
||||||
if (isDerivation(possibleInnerDerivation)) {
|
|
||||||
this._innerDerivation = possibleInnerDerivation
|
|
||||||
this._addDependency(possibleInnerDerivation)
|
|
||||||
return possibleInnerDerivation.getValue()
|
|
||||||
} else {
|
|
||||||
return possibleInnerDerivation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _recalculateCold() {
|
|
||||||
const possibleInnerDerivation = this._fn(this._depDerivation.getValue())
|
|
||||||
|
|
||||||
if (isDerivation(possibleInnerDerivation)) {
|
|
||||||
return possibleInnerDerivation.getValue()
|
|
||||||
} else {
|
|
||||||
return possibleInnerDerivation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _recalculate() {
|
|
||||||
return this.isHot ? this._recalculateHot() : this._recalculateCold()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _reactToDependencyBecomingStale(
|
|
||||||
msgComingFrom: IDerivation<unknown>,
|
|
||||||
) {
|
|
||||||
const updateNeededFrom =
|
|
||||||
msgComingFrom === this._depDerivation
|
|
||||||
? UPDATE_NEEDED_FROM.dep
|
|
||||||
: UPDATE_NEEDED_FROM.inner
|
|
||||||
|
|
||||||
if (
|
|
||||||
updateNeededFrom === UPDATE_NEEDED_FROM.inner &&
|
|
||||||
msgComingFrom !== this._innerDerivation
|
|
||||||
) {
|
|
||||||
throw Error(
|
|
||||||
`got a _reactToDependencyBecomingStale() from neither the dep nor the inner derivation`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._staleDependency === UPDATE_NEEDED_FROM.none) {
|
|
||||||
this._staleDependency = updateNeededFrom
|
|
||||||
|
|
||||||
if (updateNeededFrom === UPDATE_NEEDED_FROM.dep) {
|
|
||||||
this._removeInnerDerivation()
|
|
||||||
}
|
|
||||||
} else if (this._staleDependency === UPDATE_NEEDED_FROM.dep) {
|
|
||||||
} else {
|
|
||||||
if (updateNeededFrom === UPDATE_NEEDED_FROM.dep) {
|
|
||||||
this._staleDependency = UPDATE_NEEDED_FROM.dep
|
|
||||||
this._removeInnerDerivation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeInnerDerivation() {
|
|
||||||
if (this._innerDerivation) {
|
|
||||||
this._removeDependency(this._innerDerivation)
|
|
||||||
this._innerDerivation = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _keepHot() {
|
|
||||||
this._staleDependency = UPDATE_NEEDED_FROM.dep
|
|
||||||
this.getValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _becomeCold() {
|
|
||||||
this._staleDependency = UPDATE_NEEDED_FROM.dep
|
|
||||||
this._removeInnerDerivation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FlatMapDerivation
|
|
||||||
}
|
|
||||||
|
|
||||||
let cls: ReturnType<typeof makeFlatMapDerivationClass> | undefined = undefined
|
|
||||||
|
|
||||||
export default function flatMap<V, R>(
|
|
||||||
dep: IDerivation<V>,
|
|
||||||
fn: (v: V) => R,
|
|
||||||
): IDerivation<R extends IDerivation<infer T> ? T : R> {
|
|
||||||
if (!cls) {
|
|
||||||
cls = makeFlatMapDerivationClass()
|
|
||||||
}
|
|
||||||
return new cls(dep, fn) as $FixMe
|
|
||||||
}
|
|
|
@ -17,7 +17,7 @@ export default function* iterateAndCountTicks<V>(
|
||||||
}
|
}
|
||||||
|
|
||||||
let ticksCountedSinceLastYield = 0
|
let ticksCountedSinceLastYield = 0
|
||||||
const untap = d.changesWithoutValues().tap(() => {
|
const untap = d.onStale(() => {
|
||||||
ticksCountedSinceLastYield++
|
ticksCountedSinceLastYield++
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import AbstractDerivation from './AbstractDerivation'
|
|
||||||
import type {IDerivation} from './IDerivation'
|
|
||||||
|
|
||||||
// Exporting from a function because of the circular dependency with AbstractDerivation
|
|
||||||
const makeMapDerivationClass = () =>
|
|
||||||
// TODO once prism and AbstractDerivation are merged into one, we should delete this file
|
|
||||||
class MapDerivation<T, V> extends AbstractDerivation<V> {
|
|
||||||
constructor(
|
|
||||||
private readonly _dep: IDerivation<T>,
|
|
||||||
private readonly _fn: (t: T) => V,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this._addDependency(_dep)
|
|
||||||
}
|
|
||||||
|
|
||||||
_recalculate() {
|
|
||||||
return this._fn(this._dep.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
_reactToDependencyBecomingStale() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cls: ReturnType<typeof makeMapDerivationClass> | undefined = undefined
|
|
||||||
|
|
||||||
export default function map<V, R>(
|
|
||||||
dep: IDerivation<V>,
|
|
||||||
fn: (v: V) => R,
|
|
||||||
): IDerivation<R> {
|
|
||||||
if (!cls) {
|
|
||||||
cls = makeMapDerivationClass()
|
|
||||||
}
|
|
||||||
return new cls(dep, fn)
|
|
||||||
}
|
|
|
@ -231,25 +231,18 @@ class PrismDerivation<V> implements IDerivation<V> {
|
||||||
return new DerivationEmitter(this, ticker).tappable()
|
return new DerivationEmitter(this, ticker).tappable()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is renamed to {@link PrismDerivation.onStale}.
|
|
||||||
*/
|
|
||||||
changesWithoutValues(): Tappable<void> {
|
|
||||||
return this.onStale()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a tappable that fires every time the prism's state goes from `fresh-\>stale.`
|
* Returns a tappable that fires every time the prism's state goes from `fresh-\>stale.`
|
||||||
*/
|
*/
|
||||||
onStale(): Tappable<void> {
|
onStale(callback: () => void): VoidFn {
|
||||||
return new DerivationValuelessEmitter(this).tappable()
|
return new DerivationValuelessEmitter(this).tappable().tap(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep the derivation hot, even if there are no tappers (subscribers).
|
* Keep the derivation hot, even if there are no tappers (subscribers).
|
||||||
*/
|
*/
|
||||||
keepHot() {
|
keepHot() {
|
||||||
return this.onStale().tap(() => {})
|
return this.onStale(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -132,9 +132,9 @@ type QueueItem<T = unknown> = {
|
||||||
*/
|
*/
|
||||||
| `queueUpdate()`
|
| `queueUpdate()`
|
||||||
/**
|
/**
|
||||||
* `cb` in `item.der.changesWithoutValues(cb)` was called
|
* `cb` in `item.der.onStale(cb)` was called
|
||||||
*/
|
*/
|
||||||
| `changesWithoutValues(cb)`
|
| `onStale(cb)`
|
||||||
/**
|
/**
|
||||||
* Item was rendered
|
* Item was rendered
|
||||||
*/
|
*/
|
||||||
|
@ -158,7 +158,7 @@ type QueueItem<T = unknown> = {
|
||||||
*/
|
*/
|
||||||
queueUpdate: () => void
|
queueUpdate: () => void
|
||||||
/**
|
/**
|
||||||
* Untaps from `this.der.changesWithoutValues()`
|
* Untaps from `this.der.unStale()`
|
||||||
*/
|
*/
|
||||||
untap: () => void
|
untap: () => void
|
||||||
}
|
}
|
||||||
|
@ -327,9 +327,9 @@ export function useDerivation<T>(der: IDerivation<T>, debugLabel?: string): T {
|
||||||
}
|
}
|
||||||
pushToQueue(ref.current)
|
pushToQueue(ref.current)
|
||||||
},
|
},
|
||||||
untap: der.changesWithoutValues().tap(() => {
|
untap: der.onStale(() => {
|
||||||
if (TRACE) {
|
if (TRACE) {
|
||||||
ref.current.debug!.history.push(`changesWithoutValues(cb)`)
|
ref.current.debug!.history.push(`onStale(cb)`)
|
||||||
}
|
}
|
||||||
ref.current!.queueUpdate()
|
ref.current!.queueUpdate()
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default class AudioPlaybackController implements IPlaybackController {
|
||||||
|
|
||||||
// We're keeping the rangeD hot, so we can read from it on every tick without
|
// We're keeping the rangeD hot, so we can read from it on every tick without
|
||||||
// causing unnecessary recalculations
|
// causing unnecessary recalculations
|
||||||
const untapFromRangeD = rangeD.changesWithoutValues().tap(play)
|
const untapFromRangeD = rangeD.onStale(play)
|
||||||
play()
|
play()
|
||||||
|
|
||||||
this._stopPlayCallback = () => {
|
this._stopPlayCallback = () => {
|
||||||
|
|
|
@ -283,9 +283,9 @@ export default class SheetObject implements IdentityDerivationProvider {
|
||||||
interpolate(left, right, triple.progression),
|
interpolate(left, right, triple.progression),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const untap = derivation
|
const untap = derivation.onStale(
|
||||||
.changesWithoutValues()
|
updateSequenceValueFromItsDerivation,
|
||||||
.tap(updateSequenceValueFromItsDerivation)
|
)
|
||||||
|
|
||||||
updateSequenceValueFromItsDerivation()
|
updateSequenceValueFromItsDerivation()
|
||||||
untaps.push(untap)
|
untaps.push(untap)
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class PaneManager {
|
||||||
|
|
||||||
private _instantiatePanesAsTheyComeIn() {
|
private _instantiatePanesAsTheyComeIn() {
|
||||||
const allPanesD = this._getAllPanes()
|
const allPanesD = this._getAllPanes()
|
||||||
allPanesD.changesWithoutValues().tap(() => {
|
allPanesD.onStale(() => {
|
||||||
allPanesD.getValue()
|
allPanesD.getValue()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ export class Studio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
projectsD.changesWithoutValues().tap(() => {
|
projectsD.onStale(() => {
|
||||||
attachToProjects(projectsD.getValue())
|
attachToProjects(projectsD.getValue())
|
||||||
})
|
})
|
||||||
attachToProjects(projectsD.getValue())
|
attachToProjects(projectsD.getValue())
|
||||||
|
|
|
@ -311,7 +311,7 @@ function useUpdateScrollFromClippedSpaceRange(
|
||||||
const rangeStartInScaledSpace = d.getValue()
|
const rangeStartInScaledSpace = d.getValue()
|
||||||
node.scrollLeft = rangeStartInScaledSpace
|
node.scrollLeft = rangeStartInScaledSpace
|
||||||
}
|
}
|
||||||
const untap = d.changesWithoutValues().tap(update)
|
const untap = d.onStale(update)
|
||||||
|
|
||||||
update()
|
update()
|
||||||
const timeout = setTimeout(update, 100)
|
const timeout = setTimeout(update, 100)
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const useTooltipOpenState = (): [
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return cur.changesWithoutValues().tap(() => {
|
return cur.onStale(() => {
|
||||||
const flag = cur.getValue() === id
|
const flag = cur.getValue() === id
|
||||||
|
|
||||||
if (isOpenRef.current !== flag) isOpenRef.current = flag
|
if (isOpenRef.current !== flag) isOpenRef.current = flag
|
||||||
|
|
Loading…
Reference in a new issue