Backend Requests
Between attributes and actions, Datastar provides you with everything you need to build hypermedia-driven applications. Using this approach, the backend drives state to the frontend and acts as the single source of truth, determining what actions the user can take next.
Sending Signals #
By default, all signals (except for local signals whose keys begin with an underscore) are sent in an object with every backend request. When using a GET
request, the signals are sent as a datastar
query parameter, otherwise they are sent as a JSON body.
By sending all signals in every request, the backend has full access to the frontend state. This is by design. It is not recommended to send partial signals, but if you must, you can use the filterSignals
option to filter the signals sent to the backend.
Nesting Signals #
Signals can be nested, making it easier to target signals in a more granular way on the backend.
Using dot-notation:
1<div data-signals-foo.bar="1"></div>
Using object syntax:
1<div data-signals="{foo: {bar: 1}}"></div>
Using two-way binding:
1<input data-bind-foo.bar />
A practical use-case of nested signals is when you have repetition of state on a page. The following example tracks the open/closed state of a menu on both desktop and mobile devices, and the toggleAll() action to toggle the state of all menus at once.
Reading Signals #
To read signals from the backend, JSON decode the datastar
query param for GET
requests, and the request body for all other methods.
All SDKs provide a helper function to read signals. Here’s how you would use the Go SDK to read the nested signal foo.bar
in a request.
1import ("github.com/starfederation/datastar/sdk/go/datastar")
2
3type Signals struct {
4 Foo struct {
5 Bar string `json:"bar"`
6 } `json:"foo"`
7}
8
9signals := &Signals{}
10if err := datastar.ReadSignals(request, signals); err != nil {
11 http.Error(w, err.Error(), http.StatusBadRequest)
12 return
13}
SSE Events #
Datastar can stream zero or more Server-Sent Events (SSE) from the web server to the browser. There’s no special backend plumbing required to use SSE, just some special syntax. Fortunately, SSE is straightforward and provides us with some advantages, in addition to allowing us to send multiple events in a single response (in contrast to sending text/html
or application/json
responses).
First, set up your backend in the language of your choice. Familiarize yourself with sending SSE events, or use one of the backend SDKs to get up and running even faster. We’re going to use the SDKs in the examples below, which set the appropriate headers and format the events for us.
The following code would exist in a controller action endpoint in your backend.
1;; Import the SDK's api and your adapter
2(require
3 '[starfederation.datastar.clojure.api :as d*]
4 '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])
5
6
7;; in a ring handler
8(defn handler [request]
9 ;; Create a SSE response
10 (->sse-response request
11 {on-open
12 (fn [sse]
13 ;; Merge html fragments into the DOM
14 (d*/patch-elements! sse
15 "<div id=\"question\">What do you put in a toaster?</div>")
16
17 ;; Merge signals into the signals
18 (d*/patch-signals! sse "{response: '', answer: 'bread'}"))}))
1using StarFederation.Datastar.DependencyInjection;
2
3// Adds Datastar as a service
4builder.Services.AddDatastar();
5
6app.MapGet("/", async (IDatastarService datastarService) =>
7{
8 // Patches elements into the DOM.
9 await datastarService.PatchElementsAsync(@"<div id=""question"">What do you put in a toaster?</div>");
10
11 // Merges signals into the signals.
12 await sse.PatchSignalsAsync("{response: '', answer: 'bread'}");
13});
1import ("github.com/starfederation/datastar/sdk/go/datastar")
2
3// Creates a new `ServerSentEventGenerator` instance.
4sse := datastar.NewSSE(w,r)
5
6// Patches elements into the DOM.
7sse.PatchElements(
8 `<div id="question">What do you put in a toaster?</div>`
9)
10
11// Merges signals into the signals.
12sse.PatchSignals([]byte(`{response: '', answer: 'bread'}`))
1import starfederation.datastar.utils.ServerSentEventGenerator;
2
3// Creates a new `ServerSentEventGenerator` instance.
4AbstractResponseAdapter responseAdapter = new HttpServletResponseAdapter(response);
5ServerSentEventGenerator generator = new ServerSentEventGenerator(responseAdapter);
6
7// Patches elements into the DOM.
8generator.send(PatchElements.builder()
9 .data("<div id=\"question\">What do you put in a toaster?</div>")
10 .build()
11);
12
13generator.send(PatchSignals.builder()
14 .data("{\"response\": \"\", \"answer\": \"\"}")
15 .build()
16);
1use starfederation\datastar\ServerSentEventGenerator;
2
3// Creates a new `ServerSentEventGenerator` instance.
4$sse = new ServerSentEventGenerator();
5
6// Patches elements into the DOM.
7$sse->patchElements(
8 '<div id="question">What do you put in a toaster?</div>'
9);
10
11// Merges signals into the signals.
12$sse->PatchSignals(['response' => '', 'answer' => 'bread']);
1from datastar_py import ServerSentEventGenerator as SSE
2from datastar_py.litestar import DatastarResponse
3
4async def endpoint():
5 return DatastarResponse([
6 SSE.patch_elements('<div id="question">What do you put in a toaster?</div>'),
7 SSE.patch_signals({"response": "", "answer": "bread"})
8 ])
1require 'datastar'
2
3# Create a Datastar::Dispatcher instance
4
5datastar = Datastar.new(request:, response:)
6
7# In a Rack handler, you can instantiate from the Rack env
8# datastar = Datastar.from_rack_env(env)
9
10# Start a streaming response
11datastar.stream do |sse|
12 # Patches elements into the DOM
13 sse.patch_elements %(<div id="question">What do you put in a toaster?</div>)
14
15 # Patches signals
16 sse.patch_signals(response: '', answer: 'bread')
17end
1use datastar::prelude::*;
2use async_stream::stream;
3
4Sse(stream! {
5 // Patches elements into the DOM.
6 yield PatchElements::new("<div id='question'>What do you put in a toaster?</div>").into();
7
8 // Merges signals into the signals.
9 yield PatchSignals::new("{response: '', answer: 'bread'}").into();
10})
1// Creates a new `ServerSentEventGenerator` instance (this also sends required headers)
2ServerSentEventGenerator.stream(req, res, (stream) => {
3 // Patches elements into the DOM.
4 stream.patchElements(`<div id="question">What do you put in a toaster?</div>`);
5
6 // Merges signals into the signals.
7 stream.patchSignals({'response': '', 'answer': 'bread'});
8});
The PatchElements()
function updates the provided HTML element into the DOM, replacing the element with id="question"
. An element with the ID question
must already exist in the DOM.
The PatchSignals()
function updates the response
and answer
signals into the frontend signals.
With our backend in place, we can now use the data-on-click
attribute to trigger the @get()
action, which sends a GET
request to the /actions/quiz
endpoint on the server when a button is clicked.
1<div
2 data-signals="{response: '', answer: ''}"
3 data-computed-correct="$response.toLowerCase() == $answer"
4>
5 <div id="question"></div>
6 <button data-on-click="@get('/actions/quiz')">Fetch a question</button>
7 <button
8 data-show="$answer != ''"
9 data-on-click="$response = prompt('Answer:') ?? ''"
10 >
11 BUZZ
12 </button>
13 <div data-show="$response != ''">
14 You answered “<span data-text="$response"></span>”.
15 <span data-show="$correct">That is correct ✅</span>
16 <span data-show="!$correct">
17 The correct answer is “<span data-text="$answer"></span>” 🤷
18 </span>
19 </div>
20</div>
Now when the Fetch a question
button is clicked, the server will respond with an event to modify the question
element in the DOM and an event to modify the response
and answer
signals. We’re driving state from the backend!
data-indicator
#
The data-indicator
attribute sets the value of a signal to true
while the request is in flight, otherwise false
. We can use this signal to show a loading indicator, which may be desirable for slower responses.
Backend Actions #
We’re not limited to sending just GET
requests. Datastar provides backend actions for each of the methods available: @get()
, @post()
, @put()
, @patch()
and @delete()
.
Here’s how we can send an answer to the server for processing, using a POST
request.
One of the benefits of using SSE is that we can send multiple events (patch elements and patch signals) in a single response.
1generator.send(PatchElements.builder()
2 .data("<div id=\"question\">...</div>")
3 .build()
4);
5generator.send(PatchElements.builder()
6 .data("<div id=\"instructions\">...</div>")
7 .build()
8);
9generator.send(PatchSignals.builder()
10 .data("{\"answer\": \"...\", \"prize\": \"...\"}")
11 .build()
12);
In addition to your browser’s dev tools, the Datastar Inspector can be used to monitor and inspect SSE events received by Datastar.
Read more about SSE events in the reference.
Congratulations #
You’ve actually read the entire guide! You should now know how to use Datastar to build reactive applications that communicate with the backend using backend requests and SSE events.
Feel free to dive into the reference and explore the examples next, to learn more about what you can do with Datastar.