Skip to content

Commit f386f57

Browse files
author
Rich Harris
committed
updated context example
1 parent 3f0e3a1 commit f386f57

File tree

12 files changed

+282
-214
lines changed

12 files changed

+282
-214
lines changed

content/tutorial/02-advanced-svelte/08-context/01-context-api/README.md

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,89 @@
22
title: setContext and getContext
33
---
44

5-
> This exercise doesn't currently work. You can switch to the old tutorial instead: https://svelte.dev/tutorial/context-api
5+
The context API provides a mechanism for components to 'talk' to each other without passing around data and functions as props, or dispatching lots of events. It's an advanced feature, but a useful one. In this exercise, we're going to recreate [Schotter](https://collections.vam.ac.uk/item/O221321/schotter-print-nees-georg/) by George Nees — one of the pioneers of generative art — using the context API.
66

7-
The context API provides a mechanism for components to 'talk' to each other without passing around data and functions as props, or dispatching lots of events. It's an advanced feature, but a useful one.
7+
Inside `Canvas.svelte`, there's an `addItem` function that adds an item to the canvas. We can make it available to components inside `<Canvas>`, like `<Square>`, with `setContext`:
88

9-
Take this example app using a [Mapbox GL](https://docs.mapbox.com/mapbox-gl-js/overview/) map. We'd like to display the markers, using the `<MapMarker>` component, but we don't want to have to pass around a reference to the underlying Mapbox instance as a prop on each component.
9+
```svelte
10+
/// file: Canvas.svelte
11+
<script>
12+
import { +++setContext+++, afterUpdate, onMount, tick } from 'svelte';
1013
11-
There are two halves to the context API — `setContext` and `getContext`. If a component calls `setContext(key, context)`, then any _child_ component can retrieve the context with `const context = getContext(key)`.
14+
// ...
1215
13-
Let's set the context first. In `Map.svelte`, import `setContext` from `svelte` and `key` from `mapbox.js` and call `setContext`:
16+
onMount(() => {
17+
ctx = canvas.getContext('2d');
18+
});
1419
15-
```js
16-
/// file: Map.svelte
17-
import { onMount, setContext } from 'svelte';
18-
import { mapbox, key } from './mapbox.js';
19-
20-
setContext(key, {
21-
getMap: () => map
22-
});
23-
```
24-
25-
The context object can be anything you like. Like [lifecycle functions](/tutorial/onmount), `setContext` and `getContext` must be called during component initialisation. Calling it afterwards - for example inside `onMount` - will throw an error. In this example, since `map` isn't created until the component has mounted, our context object contains a `getMap` function rather than `map` itself.
20+
+++ setContext('canvas', {
21+
addItem
22+
});+++
2623
27-
On the other side of the equation, in `MapMarker.svelte`, we can now get a reference to the Mapbox instance:
24+
function addItem(fn) {...}
2825
29-
```js
30-
/// file: MapMarker.svelte
31-
import { getContext } from 'svelte';
32-
import { mapbox, key } from './mapbox.js';
33-
34-
const { getMap } = getContext(key);
35-
const map = getMap();
26+
function draw() {...}
27+
</script>
3628
```
3729

38-
The markers can now add themselves to the map.
30+
Inside child components, we can now get the context with, well, `getContext`:
3931

40-
> A more finished version of `<MapMarker>` would also handle removal and prop changes, but we're only demonstrating context here.
32+
```svelte
33+
/// file: Square.svelte
34+
<script>
35+
+++import { getContext } from 'svelte';+++
4136
42-
## Context keys
37+
export let x;
38+
export let y;
39+
export let size;
40+
export let rotate;
4341
44-
In `mapbox.js` you'll see this line:
42+
+++getContext('canvas').addItem(draw);+++
4543
46-
```js
47-
/// file: mapbox.js
48-
const key = {};
44+
function draw(ctx) {...}
45+
</script>
4946
```
5047

51-
We can use anything as a key — we could do `setContext('mapbox', ...)` for example. The downside of using a string is that different component libraries might accidentally use the same one; using an object literal means the keys are guaranteed not to conflict in any circumstance (since an object only has referential equality to itself, i.e. `{} !== {}` whereas `"x" === "x"`), even when you have multiple different contexts operating across many component layers.
48+
So far, so... boring. Let's add some randomness to the grid:
49+
50+
```svelte
51+
/// file: App.svelte
52+
<div class="container">
53+
<Canvas width={800} height={1200}>
54+
{#each Array(12) as _, c}
55+
{#each Array(22) as _, r}
56+
<Square
57+
x={180 + c * 40+++ + jitter(r * 2)+++}
58+
y={180 + r * 40+++ + jitter(r * 2)+++}
59+
size={40}
60+
+++rotate={jitter(r * 0.05)}+++
61+
/>
62+
{/each}
63+
{/each}
64+
</Canvas>
65+
</div>
66+
```
5267

53-
## Contexts vs. stores
68+
Like [lifecycle functions](/tutorial/onmount), `setContext` and `getContext` must be called during component initialisation. (The context key (`'canvas'` in this case) can be anything you like, including non-strings, which is useful for controlling who can access the context.)
5469

55-
Contexts and stores seem similar. They differ in that stores are available to _any_ part of an app, while a context is only available to _a component and its descendants_. This can be helpful if you want to use several instances of a component without the state of one interfering with the state of the others.
70+
Your context object can include anything, including stores. This allows you to pass values that change over time to child components:
5671

57-
In fact, you might use the two together. Since context is not reactive, values that change over time should be represented as stores:
72+
```js
73+
/// no-file
74+
// in a parent component
75+
import { setContext } from 'svelte';
76+
import { writable } from 'svelte/store';
5877

78+
setContext('my-context', {
79+
count: writable(0)
80+
});
81+
```
5982
```js
6083
/// no-file
61-
const { these, are, stores } = getContext(...);
84+
// in a child component
85+
import { getContext } from 'svelte';
86+
87+
const { count } = getContext('my-context');
88+
89+
$: console.log({ count });
6290
```
Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
<script>
2-
import Map from './Map.svelte';
3-
import MapMarker from './MapMarker.svelte';
2+
import Canvas from './Canvas.svelte';
3+
import Square from './Square.svelte';
4+
5+
// we use a seeded random number generator to get consistent jitter
6+
let seed = 1;
7+
8+
function random() {
9+
seed *= 16807;
10+
seed %= 2147483647;
11+
return (seed - 1) / 2147483646;
12+
}
13+
14+
function jitter(amount) {
15+
return amount * (random() - 0.5);
16+
}
417
</script>
518

6-
<Map lat={35} lon={-84} zoom={3.5}>
7-
<MapMarker
8-
lat={37.8225}
9-
lon={-122.0024}
10-
label="Svelte Body Shaping"
11-
/>
12-
<MapMarker
13-
lat={33.8981}
14-
lon={-118.4169}
15-
label="Svelte Barbershop & Essentials"
16-
/>
17-
<MapMarker
18-
lat={29.723}
19-
lon={-95.4189}
20-
label="Svelte Waxing Studio"
21-
/>
22-
<MapMarker
23-
lat={28.3378}
24-
lon={-81.3966}
25-
label="Svelte 30 Nutritional Consultants"
26-
/>
27-
<MapMarker
28-
lat={40.6483}
29-
lon={-74.0237}
30-
label="Svelte Brands LLC"
31-
/>
32-
<MapMarker
33-
lat={40.6986}
34-
lon={-74.41}
35-
label="Svelte Medical Systems"
36-
/>
37-
</Map>
19+
<div class="container">
20+
<Canvas width={800} height={1200}>
21+
{#each Array(12) as _, c}
22+
{#each Array(22) as _, r}
23+
<Square
24+
x={180 + c * 40}
25+
y={180 + r * 40}
26+
size={40}
27+
/>
28+
{/each}
29+
{/each}
30+
</Canvas>
31+
</div>
32+
33+
<style>
34+
.container {
35+
height: 100%;
36+
aspect-ratio: 2 / 3;
37+
margin: 0 auto;
38+
background: rgb(224, 219, 213);
39+
filter: drop-shadow(0.5em 0.5em 1em rgba(0, 0, 0, 0.1));
40+
}
41+
</style>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<script>
2+
import { afterUpdate, onMount, tick } from 'svelte';
3+
4+
export let width = 100;
5+
export let height = 100;
6+
7+
let canvas, ctx;
8+
let items = new Set();
9+
let scheduled = false;
10+
11+
onMount(() => {
12+
ctx = canvas.getContext('2d');
13+
});
14+
15+
function addItem(fn) {
16+
onMount(() => {
17+
items.add(fn);
18+
return () => items.delete(fn);
19+
});
20+
21+
afterUpdate(async () => {
22+
if (scheduled) return;
23+
24+
scheduled = true;
25+
await tick();
26+
scheduled = false;
27+
28+
draw();
29+
});
30+
}
31+
32+
function draw() {
33+
ctx.clearRect(0, 0, width, height);
34+
items.forEach(fn => fn(ctx));
35+
}
36+
</script>
37+
38+
<canvas bind:this={canvas} {width} {height}>
39+
<slot />
40+
</canvas>
41+
42+
<style>
43+
canvas {
44+
width: 100%;
45+
height: 100%;
46+
}
47+
</style>

content/tutorial/02-advanced-svelte/08-context/01-context-api/app-a/src/lib/Map.svelte

Lines changed: 0 additions & 48 deletions
This file was deleted.

content/tutorial/02-advanced-svelte/08-context/01-context-api/app-a/src/lib/MapMarker.svelte

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
export let x;
3+
export let y;
4+
export let size;
5+
export let rotate;
6+
7+
function draw(ctx) {
8+
ctx.save();
9+
10+
ctx.translate(x, y);
11+
ctx.rotate(rotate);
12+
13+
ctx.strokeStyle = 'black';
14+
ctx.strokeRect(-size / 2, -size / 2, size, size);
15+
16+
ctx.restore();
17+
}
18+
</script>

content/tutorial/02-advanced-svelte/08-context/01-context-api/app-a/src/lib/mapbox.js

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script>
2+
import Canvas from './Canvas.svelte';
3+
import Square from './Square.svelte';
4+
5+
// we use a seeded random number generator to get consistent jitter
6+
let seed = 1;
7+
8+
function random() {
9+
seed *= 16807;
10+
seed %= 2147483647;
11+
return (seed - 1) / 2147483646;
12+
}
13+
14+
function jitter(amount) {
15+
return amount * (random() - 0.5);
16+
}
17+
</script>
18+
19+
<div class="container">
20+
<Canvas width={800} height={1200}>
21+
{#each Array(12) as _, c}
22+
{#each Array(22) as _, r}
23+
<Square
24+
x={180 + c * 40 + jitter(r * 2)}
25+
y={180 + r * 40 + jitter(r * 2)}
26+
size={40}
27+
rotate={jitter(r * 0.05)}
28+
/>
29+
{/each}
30+
{/each}
31+
</Canvas>
32+
</div>
33+
34+
<style>
35+
.container {
36+
height: 100%;
37+
aspect-ratio: 2 / 3;
38+
margin: 0 auto;
39+
background: rgb(224, 219, 213);
40+
filter: drop-shadow(0.5em 0.5em 1em rgba(0, 0, 0, 0.1));
41+
}
42+
</style>

0 commit comments

Comments
 (0)