mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 10:50:00 +00:00
Docs: Reactivity (#7266)
This commit is contained in:
parent
2629ea0d76
commit
b5ecd82ab3
4 changed files with 197 additions and 37 deletions
2
.github/workflows/autofix.yaml
vendored
2
.github/workflows/autofix.yaml
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue