diff --git a/articles/flow/configuration/feature-flags.adoc b/articles/flow/configuration/feature-flags.adoc index 01dad29908..5654a8857b 100644 --- a/articles/flow/configuration/feature-flags.adoc +++ b/articles/flow/configuration/feature-flags.adoc @@ -62,18 +62,15 @@ Enables the experimental PDF Viewer component, [classname]`PdfViewer`, for showi `ssePushTransport`:: Enables <<{articles}/flow/advanced/server-push#push.configuration.transport.sse, Server-Sent Events>> as a server push transport. +`fullstackSignals`:: +Enables <<{articles}/flow/ui-state/full-stack-signals#, shared signals in React views>>. + == Legacy Feature Flags `themeComponentStyles`:: Enables loading shadow DOM component styles from the `frontend/themes//components` folder as in previous Vaadin versions. -== Hilla Feature Flags - -`fullstackSignals`:: -Enables <<{articles}/hilla/guides/full-stack-signals#, Hilla Full-stack Signals>> - - == Managing Feature Flags Feature flags can be managed in different ways: by using Vaadin Copilot; by editing the feature flags properties file; or by setting Java system properties. Each of these methods is described in the sub-sections that follow. As you read, keep in mind that system properties always have the highest priority. Since they're not written into the feature flags properties file, they're valid only for a single execution. diff --git a/articles/flow/react-views/browser-callable-services/type-conversion.adoc b/articles/flow/react-views/browser-callable-services/type-conversion.adoc index 158f86848c..ace282c674 100644 --- a/articles/flow/react-views/browser-callable-services/type-conversion.adoc +++ b/articles/flow/react-views/browser-callable-services/type-conversion.adoc @@ -87,7 +87,7 @@ The Java types map to TypeScript types as follows: Vaadin generates a TypeScript type for each bean, record, and enum that a service method uses, directly or through other types. The generated files are in [filename]`src/main/frontend/generated`, in directories named after the Java package. For example, the type of `com.example.application.Person` is in [filename]`com/example/application/Person.ts`. -A method that returns a `Flux` returns a subscription in the browser instead of a `Promise`. See <>. A method that returns a shared signal returns a signal in the browser. See <<{articles}/hilla/guides/full-stack-signals#,Full-Stack Signals>>. +A method that returns a `Flux` returns a subscription in the browser instead of a `Promise`. See <>. A method that returns a shared signal returns a signal in the browser. See <<{articles}/flow/ui-state/full-stack-signals#,Shared Signals in React Views>>. [[null-and-undefined]] diff --git a/articles/flow/ui-state/full-stack-signals.adoc b/articles/flow/ui-state/full-stack-signals.adoc new file mode 100644 index 0000000000..0674b66185 --- /dev/null +++ b/articles/flow/ui-state/full-stack-signals.adoc @@ -0,0 +1,159 @@ +--- +title: Shared Signals in React Views +page-title: Full-stack signals for React views in Vaadin applications +description: How React views use the shared signals that browser-callable services return. +meta-description: Reference for Vaadin full-stack signals - shared signals that browser-callable services return to React views, their client API, and their access control. +section-nav: badge-preview +order: 45 +--- + + += Shared Signals in React Views +:toclevels: 2 + +:preview-feature: Full-stack signals +:feature-flag: com.vaadin.experimental.fullstackSignals +:ticket-url: https://github.com/vaadin/hilla/issues/new/choose +include::{articles}/_preview-banner.adoc[opts=optional] + +A browser-callable service can return a <> to React views. The view gets a full-stack signal: a signal in the browser that holds the value of the shared signal. A change that one browser makes goes through the server to every browser that uses the signal. + +section_outline::[] + + +[[services]] +== Returning Signals from Services + +A method of a browser-callable service can return a shared signal of the following types. The generated TypeScript function returns the matching signal from `@vaadin/hilla-react-signals`. + +[cols="1,1,2"] +|=== +|Java Type |TypeScript Type |Value + +|[classname]`SharedValueSignal` +|`ValueSignal` +|A value of the type `T` + +|[classname]`SharedNumberSignal` +|`NumberSignal` +|A number + +|[classname]`SharedListSignal` +|`ListSignal` +|An array with a `ValueSignal` for each item +|=== + +The other shared signal types, such as [classname]`SharedMapSignal` and [classname]`SharedNodeSignal`, aren't supported. + +The method runs each time a browser connects to the signal. Keep the signal in a field and return the same instance on every call. A method that creates a new signal on each call gives each browser a signal of its own, and nothing is shared. + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/signals/MeetingService.java[tags=snippet,indent=0] +---- + +The value of a shared signal is stored as JSON. Use a type that converts to JSON, preferably an immutable one, such as a record. See <>. Vaadin generates a TypeScript type for the value type, like for the other types that a service uses. + +The generated types follow the nullability of the method and of the value type, as described in <<{articles}/flow/react-views/browser-callable-services/type-nullability#,Type Nullability>>. Without `@NullMarked`, the function returns `ListSignal | undefined` for the agenda, for example. The package of the example service has the annotation, so its signals and values aren't nullable. + +The parameters of the method become parameters of the generated function. The browser sends the arguments when it connects to the signal. Return the same instance for the same arguments, so that the browsers that pass them share the signal. In the example above, all the browsers that pass `YES` get the same signal. + + +[[views]] +== Using Signals in Views + +The generated function returns the signal right away, not a `Promise`. Each call creates a separate signal with a connection of its own. Call the function once, for example at the top level of the module: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/signals/meeting-view.tsx[tags=signals,indent=0] +---- + +The generated function of a value signal takes an optional options object, after the parameters of the method. Its `defaultValue` is the value of the signal until the first value arrives from the server. Without it, a non-nullable type starts with its default value, such as an empty string for a `String`. A nullable type starts with `undefined`. A `NumberSignal` starts at `0`, and a `ListSignal` starts with an empty array. + +Read the value with the `value` property. A component that reads it renders again when the value changes, because Vaadin compiles React views with the signals transform of `@preact/signals-react`. + +The signal connects to the server when a component or an effect first reads its value, and gets the current value then. It disconnects when nothing reads it anymore. The values come through the push connection that <<{articles}/flow/react-views/browser-callable-services/reactive-services#,reactive services>> also use. When the connection to the server comes back after a break, the signal connects again. + + +[[value-signals]] +== Value Signals + +A `ValueSignal` has the following members: + +`value`:: +The current value. Assigning a value changes the signal, like [methodname]`set()`. + +[methodname]`set(value)`:: +Changes the value. The browser shows the new value right away. The server replaces its value with it, whatever the other browsers changed in the meantime: the last change wins. + +`error`:: +A signal that holds an error when the server fails to apply a change, for example because the access annotations of the service method deny it. Otherwise, it's `undefined`. For a change to an item of a list signal, the `error` of the list holds the error. + +The following text field shows the topic of the example service, and changes it when the user edits the field: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/signals/meeting-view.tsx[tags=value,indent=0] +---- + +The methods that change a signal return an operation. Its `result` property is a `Promise` that resolves when the browser gets the server's confirmation of the change. When the change fails, the promise doesn't settle, and `error` holds the error. + +Changing a value based on its current value, such as a counter, can lose the changes that other browsers make at the same time. Use the [methodname]`incrementBy()` method of a number signal for counters. + + +[[number-signals]] +== Number Signals + +A `NumberSignal` is a `ValueSignal` for a number. It adds the following methods: + +[methodname]`incrementBy(delta)`:: +Adds the given amount to the value on the server. A negative amount subtracts. The server adds each amount to its current value, so the increments from all browsers count. The browser shows the new value when the server confirms it. + +[methodname]`valueAsInt()`:: +Returns the value without its fractional part. + +The following buttons count the votes of the example service: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/signals/meeting-view.tsx[tags=number,indent=0] +---- + + +[[list-signals]] +== List Signals + +The `value` of a `ListSignal` is an array with a `ValueSignal` for each item. Assigning `value` throws an error. Change the list with the following methods: + +[methodname]`insertFirst(value)`:: +Inserts an item at the beginning of the list. + +[methodname]`insertLast(value)`:: +Inserts an item at the end of the list. + +[methodname]`remove(item)`:: +Removes the item with the given signal from the list. + +The list changes in the browser when the server confirms the change. + +To change an item, change its own signal, for example by assigning its `value`. Each item signal has an `id` that stays the same while the item is in the list, which suits the `key` of a React element. The following view shows the agenda of the example service. It lets the user mark items done, remove them, and add new ones: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/signals/meeting-view.tsx[tags=list,indent=0] +---- + + +[[server-changes]] +== Changes on the Server + +Code on the server can read and change the shared signal like any other shared signal. The browsers that are connected to the signal don't get the changes that server-side code makes. A browser gets them when it connects to the signal again. + + +[[access-control]] +== Access Control + +The access annotations of the service method apply when a browser connects to the signal, and to each change that the browser sends. See <<{articles}/flow/security/react-views/service-access-control#,Service Access Control>>. + +When access is denied on connecting, the browser gets no value, and the signal keeps its default value. When access to a change is denied, the server doesn't apply it, and the `error` property of the signal holds the error. For a change to an item, the `error` property of the list signal holds it. diff --git a/articles/flow/ui-state/index.adoc b/articles/flow/ui-state/index.adoc index 9fc3a3082c..7d3d9a9560 100644 --- a/articles/flow/ui-state/index.adoc +++ b/articles/flow/ui-state/index.adoc @@ -209,5 +209,7 @@ Use *shared signals* when: - You need transactional guarantees for state changes - You're building collaborative features like live dashboards or multi-user editing +React views can use shared signals through browser-callable services. See <>. + [NOTE] When using shared signals for multi-user scenarios, you need to <<{articles}/building-apps/server-push#enabling-push,enable push>> so changes are pushed to all users immediately. diff --git a/articles/flow/ui-state/shared-signals.adoc b/articles/flow/ui-state/shared-signals.adoc index 29795eb2a9..fb9f813197 100644 --- a/articles/flow/ui-state/shared-signals.adoc +++ b/articles/flow/ui-state/shared-signals.adoc @@ -529,4 +529,28 @@ SharedValueSignal readOnlyName = name.asReadonly(); // readOnlyName.set("new") would throw an exception ---- + +[[validators]] +== Validating Changes + +The [methodname]`withValidator()` method returns a new instance of the signal that checks each change made through it with a [classname]`CommandValidator`. The new instance shares the value of the original. The original instance still accepts all changes. + +The validator gets each change as a [classname]`SignalCommand`, such as [classname]`SignalCommand.IncrementCommand`, and returns `true` to accept it. When it returns `false`, the method that made the change throws an [classname]`UnsupportedOperationException`, and the value stays the same: + +[source,java] +---- +SharedNumberSignal votes = new SharedNumberSignal(); +SharedNumberSignal singleVotes = votes.withValidator(command -> + command instanceof SignalCommand.IncrementCommand increment + && Math.abs(increment.delta()) == 1); + +singleVotes.incrementBy(1); // Accepted +singleVotes.incrementBy(5); // Throws UnsupportedOperationException +votes.incrementBy(5); // Accepted, because the original has no validator +---- + +The validator applies also to changes made through the child signals of the instance, such as the entries of a list signal. In a transaction, the validator checks each change, and a rejected change fails the whole transaction. Commands that only check a condition, such as the one that [methodname]`verifyValue()` submits, are always accepted. + +Calling [methodname]`withValidator()` on an instance that already has a validator returns an instance that needs both validators to accept a change. The [methodname]`and()` method of [classname]`CommandValidator` combines two validators in the same way. The [methodname]`asReadonly()` method uses the validator [classname]`CommandValidator.REJECT_ALL`, which rejects all changes. + [discussion-id]`4ED29fCD-42B1-4A1D-8B9E-BE87F8862ED1` diff --git a/articles/hilla/guides/full-stack-signals.adoc b/articles/hilla/guides/full-stack-signals.adoc deleted file mode 100644 index f211ab865a..0000000000 --- a/articles/hilla/guides/full-stack-signals.adoc +++ /dev/null @@ -1,744 +0,0 @@ ---- -title: Full-Stack Signals -page-title: Creating Full-Stack Signals in Vaadin Applications -description: A full-stack signal is designed to share and synchronize states between the server and clients in real time. -meta-description: How to create and use full-stack signals in Vaadin applications. -order: 130 ---- - - -= Full-Stack Signals - -When building a modern web application, you may often need to synchronize the state between the server and clients. You might want to notify all connected clients in a chat application when a new message is posted, or maybe visualize multiple users interacting while editing the same record in a form, or perhaps update clients when the status of an order changes in an e-commerce application. It becomes trickier when you need these updates to be propagated to clients in real time. Fortunately, full-stack signals can help. - -A full-stack signal can be seen as a special data type that holds the shared state. It enables clients to subscribe to it for receiving real-time updates when the state changes. The state can be updated by any client, and the changes are propagated to all other clients which are subscribed to the signal. - -This documentation page describes how to create and use full-stack signals in Vaadin applications. - -[NOTE] -==== -To use full-stack signals in Vaadin projects, you'll need to enable explicitly the experimental feature in Copilot, or add `com.vaadin.experimental.fullstackSignals=true` to the [filename]`src/main/resources/vaadin-featureflags.properties` file. - -Also, the implementation of full-stack signals is currently only available for Vaadin Hilla applications which use the React library to render user interfaces. -==== - - -[[server-side-signal-instance]] -== Server-Side Full-Stack Signal Instance - -The purpose of a full-stack signal is to share and synchronize the state between the server and clients in real time. In Vaadin Hilla applications, you can do that by creating an instance of one of the available full-stack signal types and returning it from your [classname]`@BrowserCallable` annotated services. - -In the following example, an instance of a [classname]`SharedValueSignal` is created and returned from a server-side, browser-callable service: - -[source,java] -.`VoteService.java` ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; - -import com.vaadin.flow.signals.shared.SharedValueSignal; - -@AnonymousAllowed -@BrowserCallable -public class VoteService { - private final SharedValueSignal started = - new SharedValueSignal<>(false, Boolean.class); // <1> - - public SharedValueSignal startedSignal() { - return started; // <2> - } -} ----- - -<1> Create an instance of a [classname]`SharedValueSignal` with an initial value of false. -<2> Return the instance of the [classname]`SharedValueSignal` from the [methodname]`startedSignal` method. - -[CAUTION] -The server-side instance of a full-stack signal is meant to be created once and shared across all clients. Therefore, create the instance as a field in a service class, and return it from the methods. This way, all clients are able to subscribe to the same signal instance and receive real-time updates when the state changes. - -The above example demonstrates a simple polling service that uses a [classname]`SharedValueSignal` to share a [classname]`Boolean` state of whether the voting has started. Clients can subscribe to this signal and receive real-time updates when the state changes. Based on this state, clients can then start or stop the voting process. - -For a complete list of available full-stack signal types, see <>. - - -[[client-subscription]] -== Subscription to a Full-Stack Signal - -For a client to receive real-time updates when the state of a full-stack signal changes, it needs to subscribe to the signal. This can be done by calling any normal Vaadin browser-callable service. - -The following example demonstrates how to subscribe to the `started` signal created in the previous section: - -[source,tsx] -.vote.tsx ----- -import { VoteService } from 'Frontend/generated/endpoints.js'; -import { Button } from '@vaadin/react-components/Button.js'; - -const votingStarted = VoteService.startedSignal({ defaultValue: false }); // <1> - -export default function VoteView() { - return ( - <> - Is voting in progress: {votingStarted.value ? 'Yes' : 'No' /* <2> */ } - - - ); -} ----- - -<1> Subscribe to the `started` signal and set the default value to `false`. This client-side default value is used when rendering the component before the first update from the server-side signal is received. It has no effect on the value of server-side signal. -<2> Render the current state of the signal by accessing its `value` property. -<3> Toggle the state by setting the `value` property of the signal. - - -[[available-full-stack-signal-types]] -== Available Full-Stack Signal Types - -The full-stack signals are designed to be used in various scenarios. Based on the requirements, different types of full-stack signals are used. The server-side signal types are available in the `com.vaadin.flow.signals.shared` package. Their client-side counterparts are available in `@vaadin/hilla-react-signals`. - -The currently available signal types are [classname]`SharedValueSignal`, [classname]`SharedNumberSignal`, and [classname]`SharedListSignal`. These are described in the following sub-sections. - - -[[value-signal]] -=== SharedValueSignal - -The `SharedValueSignal` is a full-stack signal that holds a single value of an arbitrary type. It has to be a JSON-serializable type that's supported by the Hilla framework. - -The following example demonstrates how to create and use a [classname]`SharedValueSignal` in a server-side service: - -[source,java] -.`SomeService.java` ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; - -import com.vaadin.flow.signals.shared.SharedValueSignal; - -@AnonymousAllowed -@BrowserCallable -public class SomeService { - private final SharedValueSignal sharedBoolean = - new SharedValueSignal<>(true, Boolean.class); - - private final SharedValueSignal sharedInteger = - new SharedValueSignal<>(42, Integer.class); - - private final SharedValueSignal sharedString = - new SharedValueSignal<>("Hello World", String.class); - - public SharedValueSignal sharedBoolean() { - return sharedBoolean; - } - - public SharedValueSignal sharedInteger() { - return sharedInteger; - } - - public SharedValueSignal sharedString() { - return sharedString; - } -} ----- - -The above example demonstrates a simple service that uses three [classname]`SharedValueSignal` instances to share a boolean, an integer, and a string value. The possibilities, though, aren't limited to primitive types. Any custom type can be used as long as it's JSON-serializable. Here's an example using a custom type: - -[source,java] -.`PersonService.java` ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; -import org.jspecify.annotations.NonNull; -import com.vaadin.flow.signals.shared.SharedValueSignal; - -@AnonymousAllowed -@BrowserCallable -public class PersonService { - record Person(String name, int age) {} // <1> - - private final Person initialValue = new Person("John Doe", 42); // <2> - - private final SharedValueSignal sharedPerson = - new SharedValueSignal<>(initialValue, Person.class); // <3> - - @NonNull - public SharedValueSignal<@NonNull Person> sharedPerson() { // <4> - return sharedPerson; - } -} ----- - -<1> A record type that is JSON-serializable, in this case a person with their name and age. -<2> The initial value of the signal. This remains the same until an update is submitted. -<3> The signal instance that holds the shared state of the person. -<4> The service method that returns the signal instance. The [classname]`@Nonnull` annotations are used to indicate that both the returned signal and its value may never be null. However, if the signal instance or its value might be null, you can remove the `@Nonnull` annotations. - -Although the above example shows the usage of a record, you can also use classes with mutable properties. There aren't any technical limitations on this, as the wrapped value of the signal is always replaced with a new instance whenever an update is applied to the signals. However, the usage of immutable types is always preferred when dealing with share values. It helps to reduce confusion and potential bugs that might arise from the shared mutable state. - -Having a [classname]`@BrowserCallable`-annotated service with a method that returns a [classname]`SharedValueSignal` instance similar to the above example, enables the client-side code to subscribe to it by calling the service method: - -[source,tsx] -.`person.tsx` ----- -import { Button, VerticalLayout } from '@vaadin/react-components'; - -import { ValueSignal } from '@vaadin/hilla-react-signals'; -import { PersonService } from 'Frontend/generated/endpoints.js'; -import type Person from 'Frontend/generated/com/example/application/services/PersonService/Person.js'; - -const sharedPerson: ValueSignal = - PersonService.sharedPerson({ defaultValue: { name: '', age: 0 } }); // <1> - -export default function PersonView() { - return ( - - Name: {sharedPerson.value.name /* <2> */ } - Age: {sharedPerson.value.age} - - - ); -} ----- -<1> Subscribing to the `sharedPerson` signal and setting the default value to an empty person. -<2> Rendering the name of the person. The value of the signal is the type, `Person` with a `name` property. -<3> Increasing the age of the person by creating a new `Person` object containing an increased age and assigning this new object as the signal's value. This triggers an update to the server-side signal. All other clients that are subscribed to the signal also receive the updated value. - -Given the nature of the signals, only changing the value of the signal causes the signal's subscribers to be notified. Changing the internal properties of the value object doesn't trigger an update. - - -==== Setting the Value - -All signals have a `value` property that can be used both to set and read the value of the signal. However, setting concurrently a shared value among multiple clients can cause them to overwrite each other's changes. [classname]`SharedValueSignal` provides extra methods to set the value in different situations: - -`set(value: T): Operation`:: This sets the signal's value with what's given. It's the same as assigning the `value` property, directly. The value change event that is propagated to the server as the result of this operation doesn't take the last seen value into account. Instead, it overwrites the shared value on the server unconditionally -- a policy known as, _Last Write Wins_. The returned `Operation` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server. -`replace(expected: T, newValue: T): Operation`:: This atomically replaces the value with a new one only if the current value is equal to the expected one. This means that a state change request is sent to the server asking it to "compare and set". At the time of processing this requested change on the server, if the current value is not equal to the expected value, the update is rejected by the server. The returned `Operation` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server. -`update(updater: (current: T) => T): OperationSubscription`:: This tries to update the value by applying the callback function to the current value on the client side. When the new value is calculated, a "compare and set" operation is sent to the server. In case of a concurrent change, the update is rejected, and the callback is run again with an updated current value on the client side. This is repeated until the result can be applied without concurrent changes, or the operation is canceled by calling the `cancel()` function of the returned `OperationSubscription`. This operation is atomic at the time of the server-side processing, meaning that the server only accepts the update if the value is still the same as when the operation was initiated. The returned `OperationSubscription` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server. - -A call to `cancel()` may not always be effective, as a succeeding operation might already be on its way to the server. - -Operations such as `replace` and `update` perform a "compare and set" on the server using the [methodname]`equals` method of the value type to compare the values. Thus, it's important to make sure the value type has a proper implementation of the [methodname]`equals` method. - - -==== Using Operation Results - -The `Operation` and `OperationSubscription` objects returned by the `set`, `replace`, and `update` methods have a `result` property that is a `Promise`. This promise is resolved when the operation is completed and confirmed by the server. The promise can also be rejected if the operation fails, for example, due to a validation error. The promise can be used to chain further operations, as well. - -The following example demonstrates how to use the `result` property of an operation: - -[source,tsx] -.`person.tsx` ----- -const sharedPerson: ValueSignal = - PersonService.sharedPerson({ defaultValue: { name: '', age: 0 } }); - -export default function PersonView() { - return ( - - - - ); -} ----- -<1> The `result` property of the `replace` operation is a `Promise` that can be used to chain further operations. In this case, the promise is used to log a message when the operation is completed successfully, or to log an error message when the operation is rejected by the server. - - -[[number-signal]] -=== SharedNumberSignal - -The [classname]`SharedNumberSignal` is a full-stack signal that holds a numeric value. This value is the [classname]`Double` type in Java, and a `number` type in client-side code. The [classname]`SharedNumberSignal` can be considered a special case of the [classname]`SharedValueSignal` that is optimized for numeric values by introducing built-in support for atomic increment and decrement operations. - -The following example demonstrates how to create and use a [classname]`SharedNumberSignal` in a service class: - -[source,java] -.`CounterService.java` ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; - -import com.vaadin.flow.signals.shared.SharedNumberSignal; - -@AnonymousAllowed -@BrowserCallable -public class CounterService { - private final SharedNumberSignal counter = new SharedNumberSignal(1.0); // <1> - - public SharedNumberSignal counter() { // <2> - return counter; - } -} ----- -<1> Create an instance of a [classname]`SharedNumberSignal` with initial client-side value of `1`. If no value is provided to the constructor, it defaults to `0`. -<2> Return the instance of the [classname]`SharedNumberSignal` from the `counter` method. - -The above example demonstrates a simple counter service that uses a [classname]`SharedNumberSignal` to share a numeric value. The client can subscribe to this signal, and apart from receiving real-time updates, it can initiate atomic increment and decrement operations, as well: - -[source,tsx] -.counter.tsx ----- -import { Button, VerticalLayout } from '@vaadin/react-components'; -import { CounterService } from 'Frontend/generated/endpoints.js'; - -const counter = CounterService.counter(); // <1> - -export default function() { - return ( - - Counter: {counter /* <2> */ } - - - - - ); -} ----- -<1> Subscribe to the `counter` signal. The subscription is done outside the render function to avoid creating a new subscription on each render. -<2> Render the current value of the signal. -<3> Increase the value of the signal using the atomic [methodname]`incrementBy` operation. -<4> Decrease the value of the signal using the atomic [methodname]`incrementBy` operation and providing a negative value. -<5> Reset the value of the signal to `0` by assigning a new value to it. - -The [methodname]`incrementBy` operation is _incrementally atomic_, meaning it guarantees success by reading the current value and applying the increment on the value, atomically. Each operation builds on the previously accepted one, ensuring that `n` increments or decrements are always applied correctly -- even if there are multiple clients trying to update the value, concurrently. - -Since [classname]`SharedNumberSignal` is a [classname]`SharedValueSignal` with the additional atomic operation of [methodname]`incrementBy`, it inherits all methods, such as [methodname]`replace` and [methodname]`update`, making those operations available when using a [classname]`SharedNumberSignal`. - - -[[list-signal]] -=== SharedListSignal - -The [classname]`SharedListSignal` is a full-stack signal that holds a list of values of an arbitrary type. It has to be a JSON-serializable type that's supported by the Hilla framework. Every item in a [classname]`SharedListSignal` is a [classname]`SharedValueSignal`, meaning that any changes to the item's value is propagated to the server and other clients. - -The following example demonstrates how to create and use a [classname]`SharedListSignal` in a server-side service: - -[source,java] -.`TodoService.java` ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; -import com.vaadin.flow.signals.shared.SharedListSignal; - -@AnonymousAllowed -@BrowserCallable -public class TodoService { - record TodoItem(String text, boolean done) {} - - private final SharedListSignal todoItems = - new SharedListSignal<>(TodoItem.class); // <1> - - @Nonnull - public SharedListSignal<@Nonnull TodoItem> todoItems() { // <2> - return todoItems; - } -} ----- -<1> Create an instance of a [classname]`SharedListSignal`. The initial state of a [classname]`SharedListSignal` is an empty list. -<2> Return the instance of the [classname]`SharedListSignal` from the [methodname]`todoItems` method. - -On the client-side code, subscribing to the shared list signal instance is done in a similar way as with the [classname]`SharedValueSignal`. - -The following example demonstrates how to create a to-do list view that enables concurrent users to add tasks to a shared list: - -[source,tsx] -.todo.tsx ----- -import { TodoService } from "Frontend/generated/endpoints.js"; -import { - Button, - TextField, - HorizontalLayout, - VerticalLayout -} from "@vaadin/react-components"; -import { effect, useSignal } from "@vaadin/hilla-react-signals"; - -const todoItems = TodoService.todoItems(); // <1> - -export default function TodoView(){ - const newTodoValue = useSignal(''); - return ( - <> - -

Tasks

- {todoItems.value.length === 0 // <2> - ? No tasks yet... - : todoItems.value.map((item, index) => // <3> -
  • {item.value.text}
  • - ) - } - - newTodoValue.value = e.detail.value}/> - - -
    - - ); -} ----- - -<1> Subscribe to the `todoItems` list signal. -<2> The `value` property of the [classname]`SharedListSignal` holds the list of tasks. The length of the list is checked to display a message when there are no tasks. -<3> The `map` function is used to render the list of tasks. -<4> Add a new task to the list by calling the [methodname]`insertLast` method of the [classname]`SharedListSignal`. - -Since the `todoItems` signal holds the shared list of tasks, any subscribed client to this signal receives real-time updates when the list changes. When a client adds a new task to the list, all other clients receive the update and the list is re-rendered to reflect the changes. The above example, however, doesn't demonstrate how to remove or update tasks in the list. This is covered in the next section. - - -[[list-signal-api]] -==== SharedListSignal API - -The client-side API of the [classname]`SharedListSignal` provides methods to insert and remove items. The [classname]`SharedListSignal` is a sequence of [classname]`SharedValueSignal` entries. Therefore, its API is about how the entries are added to the list or removed, and how the concurrent operations regarding the structure of the entries is handled. - -The currently available methods are [methodname]`insertLast` and [methodname]`remove`. These are described below: - -`insertLast(value: T): Operation`:: Inserts a new value at the end of the list. The returned `Operation` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server. -`remove(item: ValueSignal): Operation`:: Removes the given item from the list. The returned `Operation` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server. - -[NOTE] -Since each item in a `SharedListSignal` is a `SharedValueSignal`, should a change occur to the value of an item, use the `SharedValueSignal` API -- such as `set`, `replace`, or `update` -- on the targeted item. `SharedListSignal` API is only about the organization of items in the list. - -The following example demonstrates how to create a to-do list view that enables concurrent users to add, remove, and update tasks in a shared list, with no changes needed on the server-side: - -[source,tsx] -.todo.tsx ----- -import { TodoService } from "Frontend/generated/endpoints.js"; -import { - Button, - Checkbox, - Icon, - TextField, - TextArea, - HorizontalLayout, - VerticalLayout -} from "@vaadin/react-components"; -import { effect, useSignal, type ValueSignal} from "@vaadin/hilla-react-signals"; - -const todoItems = TodoService.todoItems(); - -function TodoComponent({todoItem, onRemove}: { - todoItem: ValueSignal<{text: string, done: boolean}>, - onRemove: (signal: ValueSignal<{text: string, done: boolean}>) => void, -}) { - const editing = useSignal(false); - const todoText = useSignal(''); - return ( - - {editing.value - ?