Skip to content

Commit 7999d50

Browse files
docs: fix missing files on "sharing state with islands" page (#3377)
close #3376
1 parent 169f156 commit 7999d50

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
description: |
3+
When you need to have state shared between islands, this page provides a few recipes.
4+
---
5+
6+
All of this content is lifted from this great
7+
[example](https://fresh-with-signals.deno.dev/) by Luca. The source can be found
8+
[here](https://github.com/lucacasonato/fresh-with-signals).
9+
10+
## Multiple Sibling Islands with Independent State
11+
12+
Imagine we have `Counter.tsx` like this:
13+
14+
```tsx islands/Counter.tsx
15+
import { useSignal } from "@preact/signals";
16+
import { Button } from "../components/Button.tsx";
17+
18+
interface CounterProps {
19+
start: number;
20+
}
21+
22+
// This island is used to display a counter and increment/decrement it. The
23+
// state for the counter is stored locally in this island.
24+
export default function Counter(props: CounterProps) {
25+
const count = useSignal(props.start);
26+
return (
27+
<div class="flex gap-2 items-center w-full">
28+
<p class="flex-grow-1 font-bold text-xl">{count}</p>
29+
<Button onClick={() => count.value--}>-1</Button>
30+
<Button onClick={() => count.value++}>+1</Button>
31+
</div>
32+
);
33+
}
34+
```
35+
36+
Note how `useSignal` is within the `Counter` component. Then if we instantiate
37+
some counters like this...
38+
39+
```tsx routes/index.tsx
40+
<Counter start={3} />
41+
<Counter start={4} />
42+
```
43+
44+
they'll keep track of their own independent state. Not much sharing going on
45+
here, yet.
46+
47+
## Multiple Sibling Islands with Shared State
48+
49+
But we can switch things up by looking at a `SynchronizedSlider.tsx` like this:
50+
51+
```tsx islands/SynchronizedSlider.tsx
52+
import { Signal } from "@preact/signals";
53+
54+
interface SliderProps {
55+
slider: Signal<number>;
56+
}
57+
58+
// This island displays a slider with a value equal to the `slider` signal's
59+
// value. When the slider is moved, the `slider` signal is updated.
60+
export default function SynchronizedSlider(props: SliderProps) {
61+
return (
62+
<input
63+
class="w-full"
64+
type="range"
65+
min={1}
66+
max={100}
67+
value={props.slider.value}
68+
onInput={(e) => (props.slider.value = Number(e.currentTarget.value))}
69+
/>
70+
);
71+
}
72+
```
73+
74+
Now if we were to do the following...
75+
76+
```tsx routes/index.tsx
77+
export default function Home() {
78+
const sliderSignal = useSignal(50);
79+
return (
80+
<div>
81+
<SynchronizedSlider slider={sliderSignal} />
82+
<SynchronizedSlider slider={sliderSignal} />
83+
<SynchronizedSlider slider={sliderSignal} />
84+
</div>
85+
);
86+
}
87+
```
88+
89+
they would all use the same value.
90+
91+
## Independent Islands
92+
93+
We can also create a `signal` in a utility file and export it for consumption
94+
across multiple places.
95+
96+
```ts utils/cart.ts
97+
import { signal } from "@preact/signals";
98+
99+
export const cart = signal<string[]>([]);
100+
```
101+
102+
```tsx islands/AddToCart.tsx
103+
import { Button } from "../components/Button.tsx";
104+
import { cart } from "../utils/cart.ts";
105+
106+
interface AddToCartProps {
107+
product: string;
108+
}
109+
110+
// This island is used to add a product to the cart state.
111+
export default function AddToCart(props: AddToCartProps) {
112+
return (
113+
<Button
114+
onClick={() => (cart.value = [...cart.value, props.product])}
115+
class="w-full"
116+
>
117+
Add{cart.value.includes(props.product) ? " another" : ""} "{props.product}
118+
" to cart
119+
</Button>
120+
);
121+
}
122+
```
123+
124+
```tsx islands/Cart.tsx
125+
import { Button } from "../components/Button.tsx";
126+
import { cart } from "../utils/cart.ts";
127+
import * as icons from "../components/Icons.tsx";
128+
129+
// This island is used to display the cart contents and remove items from it.
130+
export default function Cart() {
131+
return (
132+
<h1 class="text-xl flex items-center justify-center">
133+
Cart
134+
</h1>
135+
136+
<ul class="w-full bg-gray-50 mt-2 p-2 rounded-sm min-h-[6.5rem]">
137+
{cart.value.length === 0 && (
138+
<li class="text-center my-4">
139+
<div class="text-gray-400">
140+
<icons.Cart class="w-8 h-8 inline-block" />
141+
<div>
142+
Your cart is empty.
143+
</div>
144+
</div>
145+
</li>
146+
)}
147+
{cart.value.map((product, index) => (
148+
<CartItem product={product} index={index} />
149+
))}
150+
</ul>
151+
);
152+
}
153+
154+
interface CartItemProps {
155+
product: string;
156+
index: number;
157+
}
158+
159+
function CartItem(props: CartItemProps) {
160+
const remove = () => {
161+
const newCart = [...cart.value];
162+
newCart.splice(props.index, 1);
163+
cart.value = newCart;
164+
};
165+
166+
return (
167+
<li class="flex items-center justify-between gap-1">
168+
<icons.Lemon class="text-gray-500" />
169+
<div class="flex-1">
170+
{props.product}
171+
</div>
172+
<Button onClick={remove} aria-label="Remove" class="border-none">
173+
<icons.X class="inline-block w-4 h-4" />
174+
</Button>
175+
</li>
176+
);
177+
}
178+
```
179+
180+
Now we can add the islands to our site by doing the following:
181+
182+
```tsx routes/cart.tsx
183+
<AddToCart product="Lemon" />
184+
<AddToCart product="Lime" />
185+
<Cart />
186+
```
187+
188+
What happens as a result? The `cart` signal is shared across the two `AddToCart`
189+
islands _and_ the `Cart` island.

0 commit comments

Comments
 (0)