Docs: Reactivity (#7266)

This commit is contained in:
Nigel Breslaw 2025-01-06 17:22:30 +01:00 committed by GitHub
parent 2629ea0d76
commit b5ecd82ab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 197 additions and 37 deletions

View file

@ -49,7 +49,7 @@ jobs:
- name: Format, Lint on npm projects
run: |
pnpm install --frozen-lockfile
pnpm format
pnpm format:fix
pnpm lint
- name: Check license headers
run: cargo xtask check_license_headers --fix-it

View file

@ -71,6 +71,10 @@ export default defineConfig({
label: "Reactivity",
slug: "guide/language/concepts/reactivity",
},
{
label: "Reactivity vs React.js",
slug: "guide/language/concepts/reactivity-vs-react",
},
],
},
{

View file

@ -0,0 +1,66 @@
---
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
title: Reactivity vs React.js
description: Reactivity vs React.js
---
## Comparison to React.js
The following sections are for those coming from or are familiar with the [React.js](https://react.dev/) web framework.
We're going to compare patterns from React.js based app development with Slint.
:::note[Note]
There is no web browser as part of Slint to render the UI. There is no DOM or shadow DOM.
:::
### Component Life Cycle Management
React.js has a model where on a state change a component is destroyed and recreated. By default this will
also include the destruction of all child components of an element and these then all need to be recreated.
To manage performance, careful use of `useMemo()` and `useCallback()` are needed to avoid unnecessary re-renders. Even
though the need for this has been reduced via the React Compiler it's still necessary to understand this model
to understand how a React app behaves.
Slint is much simpler and uses fine-grained reactivity: Components update, but they aren't destroyed and recreated. There is no equivalent of
`useMemo()` and `useCallback()` as they are unnecessary.
### State
React.js refers to properties that update and re-render the component as state. They are opt-in and by
default are not tracked.
```jsx
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => {
setCount((currentCount) => currentCount + 1)
}}>{count}</button>;
}
```
```slint playground
import { Button } from "std-widgets.slint";
export component Counter {
property <int> count: 0;
Button {
text: count;
clicked => {
count += 1;
}
}
}
```
The classic counter example also shows key differences. First the `count` property is declared and
a `count` value and `setCount()` function are deconstructed from the useState hook. Note that 'count' cannot
be directly accessed and must be updated via `setCount()`.
The counter button is then used to update the count property and to ensure it's correctly updated must rely
on the `currentCount` value returned by `setCount()` is used to update the values. As using `setCount(count + 1)`
can cause issues in more complex scenarios where the state is updated later.
While the Slint example may not look much simpler, it does the same job and has less gotchas. As everything
in Slint is reactive by default, the property is declared in one single way. The language has strong types and for
numbers has both `float`s and `int`s. The property can also be safely modified directly which also in this example
allows the use of the `+=` operator.

View file

@ -4,17 +4,134 @@ title: Reactivity
description: Reactivity
---
## Reactivity
Reactivity is core concept within Slint. It allows the creation of complex dynamic user interfaces with a fraction of the code.
The following examples will help you understand the basics of reactivity.
```slint playground
export component MyComponent {
width: 400px; height: 400px;
Rectangle {
background: #151515;
}
ta := TouchArea {}
myRect := Rectangle {
x: ta.mouse-x;
y: ta.mouse-y;
width: 40px;
height: 40px;
background: ta.pressed ? orange : white;
}
Text {
x: 5px; y: 5px;
text: "x: " + myRect.x / 1px;
color: white;
}
Text {
x: 5px; y: 15px;
text: "y: " + myRect.y / 1px;
color: white;
}
}
```
As the name suggests, Reactivity is all about parts of the user interface automatically updating or 'reacting'
to changes. The above example looks simple, but when run it does several things:
- The `Rectangle` will follow the mouse around as you move it.
- If you `click` anywhere the `Rectangle` will change color.
- The `Text` elements will update their text to show the current position of the `Rectangle`.
The 'magic' here is built into the Slint language directly. There is no need to opt into this or define
specific stateful items. The `Rectangle` will automatically updates because it's `x` and `y` properties
are bound to the `mouse-x` and `mouse-y` properties of the `TouchArea` element. This was done by giving
the `TouchArea` a name to identify it `ta` and then using the name in what Slint calls an `expression`
to track values. Its as simples as `x: ta.mouse-x;` and `y: ta.mouse-y;`. The mouse-x and mouse-y properties
are built into the `TouchArea` and automatically update as the cursor moves over them.
The `TouchArea` also has a `pressed` property that is only `true` when the cursor is pressed or clicked down.
So the ternary expression `background: ta.pressed ? orange : white;` will change the background color of the `Rectangle`
to orange when `ta.pressed` is true, or white when it isn't.
Similarly the 2 text items are updating by tracking the rectangle's `x` and `y` position.
## Performance
From a performance perspective, Slint works out what properties are changed. It then finds all the expressions
that depend on that value. These dependencies are then re-evaluated based on the new values and the UI will update.
The re-evaluation happens lazily when the property is queried.
Internally, a dependency is registered for any property accessed while evaluating a binding.
When a property changes, the dependencies are notified and all dependent bindings
are marked as dirty.
## Property Expressions
Expressions can vary in complexity:
```slint no-test
// Tracks the `x` value of an element called foo
x: foo.x;
// Tracks the value, but sets it to 0px or 400px based on if
// foo.x is greater than 400px
x: foo.x > 100px ? 0px : 400px;
// Tracks the value, but clamps it between 0px and 400px
x: clamp(foo.x, 0px, 400px);
```
As the last example shows functions can be used as part of a property expression. This can be
useful for when an expression is too complex to be readable or maintained as a single line.
```slint playground
export component MyComponent {
width: 400px; height: 400px;
pure function lengthToInt(n: length) -> int {
return (n / 1px);
}
Rectangle {
background: #151515;
}
ta := TouchArea {}
myRect := Rectangle {
x: ta.mouse-x;
y: ta.mouse-y;
width: 40px;
height: 40px;
background: ta.pressed ? orange : white;
}
Text {
x: 5px; y: 5px;
text: "x: " + lengthToInt(myRect.x);
}
Text {
x: 5px; y: 15px;
text: "y: " + lengthToInt(myRect.y);
}
}
```
Here the earlier example was updated to use a function to convert the length to an integer.
This also truncates the x and y values to be more readable i.e. '4' instead of '4.124488'.
## Purity
Slint's property evaluation is lazy and "reactive". Property
bindings are evaluated when reading the property value. Dependencies between properties are
automatically discovered during property evaluation. The property stores the
result of the evaluation. When a property changes, all dependent properties get
notified, so that the next time their value is read, their binding is re-evaluated.
For any reactive system to work well, evaluating a property shouldn't change any
observable state but the property itself. If this is the case, then the expression
is "pure", otherwise it's said to have side-effects. Side-effects are problematic
@ -23,7 +140,7 @@ their order or affect whether they happen at all. In addition, changes to
properties during their binding evaluation due to a side-effect may result in
unexpected behavior.
For this reason, bindings in Slint _must_ be pure. The Slint compiler enforces
For this reason, bindings in Slint **must** be pure. The Slint compiler enforces
code in pure contexts to be free of side effects. Pure contexts include binding
expressions, bodies of pure functions, and bodies of pure callback handlers.
In such a context, it's not allowed to change a property, or call a non-pure
@ -43,34 +160,7 @@ export component Example {
}
```
## Bindings
The binding expression is automatically re-evaluated when properties accessed in the expression change.
In the following example, the text of the button automatically changes when
the user presses the button. Incrementing the `counter` property automatically
invalidates the expression bound to `text` and triggers a re-evaluation.
```slint
import { Button } from "std-widgets.slint";
export component Example inherits Window {
preferred-width: 50px;
preferred-height: 50px;
Button {
property <int> counter: 3;
clicked => { self.counter += 3 }
text: self.counter * 2;
}
}
```
The re-evaluation happens lazily when the property is queried.
Internally, a dependency is registered for any property accessed while evaluating a binding.
When a property changes, the dependencies are notified and all dependent bindings
are marked as dirty.
Callbacks in native code by default don't depend on any properties unless they query a property in the native code.
## Two-Way Bindings
@ -93,4 +183,4 @@ export component Example {
background: blue;
}
}
```
```