One of the reasons deco is fast is our edge first approach to creating websites.
This means that all code you write runs on our servers instead of running on
slow, inconsitent user devices (browser). However, sometimes we need to provide
extra interactivity to our websites, like adding onClick
, useState
or
useEffect
event handlers. In this guide you will learn how to create
components that run on the browser. Make sure to read our performance tips
before creating any JavaScript on the browser to avoid common pitfails with
client-side JavaScript
Summary
- Making components interactive
- Islands usage limitations
- Sharing state among islands.
- Considerations and tips
Making components interactive
Suppose you have the following component. A counter that allows the user to add/subtract to the displayed value.
This component can be implemented with the following code:
import { useState } from "preact/hooks";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count - 1)}>
-
</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
}
Creating a file called Counter.tsx
and placing it into the islands
folder
gives us the following result on the screen:
This is called an island
!
Although adding islands to your project seems tempting, keep in mind that
islands slow down websites and harm TBT metric, so
before moving any component to the island
folder, make sure that your final
interactivity:
- Isn't achieved through page navigation with links or form submissions...
- Isn't an interaction built purely with CSS...
- Requires manipulation of elements or the current page's state (e.g., using onClick, onChange, useEffect, another hook, or an event listener)
Islands usage limitations
Islands are Preact components. This means they accept props
. However, these
values must be one of:
- Primitive types
string
,boolean
,bigint
, andnull
- Simple objects with
string
keys and serializable values - Arrays of serializable values
Uint8Array
- JSX Elements (ONLY as props.children)
- Preact Signals (if the signal value is serializable)
- Most numbers (
Infinity
,-Infinity
, andNaN
are converted tonull
)
Complex objects such as Date, functions, and custom classes are not accepted as islands props.
Using Signals Instead of State
useState
requires working with a separate function for value updates. Preact
also uses Signals
for handling
state. A signal
has a reference that holds a value, but it also has a .value
attribute that allows updating this value.
Within a component, if the state is only used locally, you can use the
useSignal
hook to create these elements that can be used in the function body
or in the JSX returned, as in the example below.
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
return (
<div>
<button onClick={() => count.value--}>
-
</button>
<span>{count}</span>
<button onClick={() => count.value++}>
+
</button>
</div>
);
}
Sharing state among islands.
In normal Preact development, sharing state between components is usually done via the Context API. This works fine for a full client-side application. However, since we are using islands architecture, sharing state among islands require a new approach.
Signals are also a great way of sharing state between islands, since one can publish and subscribe for change events in a concise API.
To use signals,
import { signal } from "@preact/signals";
Now, use the global scope to create, mutate and subscribe to a signal:
import { signal } from "@preact/signals";
const count = signal(0);
// Read a signal’s value by accessing .value:
console.log(count.value); // 0
// Update a signal’s value:
count.value += 1;
// The signal's value has changed:
console.log(count.value); // 1
To define side-effects over signal changes, use the effect
, batch
,
computed
, or useComputed
operations. Refer to the
signals documentation for more
details. Also, take a look at
sharing state between islands.
Note that sharing state via the
Context
API will NOT work, since the context will be outside the islands, and thus, only available on the server.
Considerations and tips
Making a component an island will at least double its size in bytes. The server renders the HTML for this element and sends it to the browser, but it also sends essentially the same HTML plus the JS to be injected on the client side. Therefore, try to create only the necessary islands, as they make the rendering process more resource-intensive.
Futher read: