;
+ });
+}
diff --git a/src/frameworks/react/demos/tutorial-7/src/index.css b/src/frameworks/react/demos/tutorial-7/src/index.css
new file mode 100644
index 0000000..d53b2a3
--- /dev/null
+++ b/src/frameworks/react/demos/tutorial-7/src/index.css
@@ -0,0 +1,73 @@
+@import url(https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&display=swap);
+@import url(https://fonts.googleapis.com/css2?family=Alegreya&family=Alegreya+Sans&family=Merriweather&family=Merriweather+Sans&family=Nunito+Sans&family=Quattrocento&family=Quattrocento+Sans&family=Roboto&family=Roboto+Mono&family=Roboto+Slab&display=swap);
+
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: grid;
+ place-items: center;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ border-color: #c6c6c6;
+ }
+}
diff --git a/src/frameworks/react/demos/tutorial-7/src/index.ts b/src/frameworks/react/demos/tutorial-7/src/index.ts
new file mode 100644
index 0000000..1b46615
--- /dev/null
+++ b/src/frameworks/react/demos/tutorial-7/src/index.ts
@@ -0,0 +1,9 @@
+import React, { StrictMode } from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
+import "./prism.css";
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+ React.createElement(StrictMode, {}, React.createElement(App))
+);
diff --git a/src/frameworks/react/demos/tutorial-7/src/lib/clock.ts b/src/frameworks/react/demos/tutorial-7/src/lib/clock.ts
new file mode 100644
index 0000000..39c17aa
--- /dev/null
+++ b/src/frameworks/react/demos/tutorial-7/src/lib/clock.ts
@@ -0,0 +1,24 @@
+import { Cell, Resource } from "@starbeam/universal";
+
+// #region clock
+export const Clock = Resource(({ on }) => {
+ const now = Cell(new Date());
+ const interval = setInterval(() => {
+ now.set(new Date());
+ });
+
+ on.cleanup(() => clearInterval(interval));
+
+ return {
+ get display() {
+ const formatter = new Intl.DateTimeFormat("en-US", {
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ });
+
+ return formatter.format(now.current);
+ },
+ };
+});
+// #endregion
diff --git a/src/frameworks/react/demos/tutorial-7/src/prism.css b/src/frameworks/react/demos/tutorial-7/src/prism.css
new file mode 100644
index 0000000..e2da571
--- /dev/null
+++ b/src/frameworks/react/demos/tutorial-7/src/prism.css
@@ -0,0 +1,143 @@
+/**
+ * atom-dark theme for `prism.js`
+ * Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
+ * @author Joe Gibson (@gibsjose)
+ */
+
+code[class*="language-"],
+pre[class*="language-"] {
+ color: #c5c8c6;
+ text-shadow: 0 1px rgba(0, 0, 0, 0.3);
+ font-family: Inconsolata, Monaco, Consolas, "Courier New", Courier, monospace;
+ direction: ltr;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ line-height: 1.5;
+
+ -moz-tab-size: 4;
+ -o-tab-size: 4;
+ tab-size: 4;
+
+ -webkit-hyphens: none;
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ hyphens: none;
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+ padding: 1em;
+ margin: 0.5em 0;
+ overflow: auto;
+ border-radius: 0.3em;
+}
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"] {
+ background: #1d1f21;
+}
+
+/* Inline code */
+:not(pre) > code[class*="language-"] {
+ padding: 0.1em;
+ border-radius: 0.3em;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+ color: #7c7c7c;
+}
+
+.token.punctuation {
+ color: #c5c8c6;
+}
+
+.namespace {
+ opacity: 0.7;
+}
+
+.token.property,
+.token.keyword,
+.token.tag {
+ color: #96cbfe;
+}
+
+.token.class-name {
+ color: #ffffb6;
+ text-decoration: underline;
+}
+
+.token.boolean,
+.token.constant {
+ color: #99cc99;
+}
+
+.token.symbol,
+.token.deleted {
+ color: #f92672;
+}
+
+.token.number {
+ color: #ff73fd;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+ color: #a8ff60;
+}
+
+.token.variable {
+ color: #c6c5fe;
+}
+
+.token.operator {
+ color: #ededed;
+}
+
+.token.entity {
+ color: #ffffb6;
+ cursor: help;
+}
+
+.token.url {
+ color: #96cbfe;
+}
+
+.language-css .token.string,
+.style .token.string {
+ color: #87c38a;
+}
+
+.token.atrule,
+.token.attr-value {
+ color: #f9ee98;
+}
+
+.token.function {
+ color: #dad085;
+}
+
+.token.regex {
+ color: #e9c062;
+}
+
+.token.important {
+ color: #fd971f;
+}
+
+.token.important,
+.token.bold {
+ font-weight: bold;
+}
+
+.token.italic {
+ font-style: italic;
+}
diff --git a/src/frameworks/react/demos/tutorial-7/src/stopwatch.ts b/src/frameworks/react/demos/tutorial-7/src/stopwatch.ts
new file mode 100644
index 0000000..fd30032
--- /dev/null
+++ b/src/frameworks/react/demos/tutorial-7/src/stopwatch.ts
@@ -0,0 +1,24 @@
+import { Cell, Formula, Resource } from "@starbeam/universal";
+
+export const Stopwatch = Resource((r) => {
+ const time = Cell(new Date());
+
+ const interval = setInterval(() => {
+ time.set(new Date());
+ }, 1000);
+
+ r.on.cleanup(() => {
+ clearInterval(interval);
+ });
+
+ return Formula(() => {
+ const now = time.current;
+
+ return new Intl.DateTimeFormat("en-US", {
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ hour12: false,
+ }).format(now);
+ });
+});
diff --git a/src/frameworks/react/tutorial/7-resources.md b/src/frameworks/react/tutorial/7-resources.md
new file mode 100644
index 0000000..1893919
--- /dev/null
+++ b/src/frameworks/react/tutorial/7-resources.md
@@ -0,0 +1,107 @@
+# Resources
+
+
+
+So far, all of our reactive objects have been in-memory values that the garbage collector can clean up without any extra work.
+
+But we sometimes have values that represent a live resource that needs to be cleaned up once it's no longer used.
+
+For example, when you use `setInterval` to start a ticking clock, you need to call `clearInterval` once you no longer need it, or your timers will keep running in the background (a "leak").
+
+In this example, we're creating a `Clock`: a custom reactive object whose value is the current time.
+
+## What We're Building
+
+
+
+## The Code: `Clock` Implementation
+
+```snippet {#clock}
+
+```
+
+The `Clock`:
+
+- uses `Cell` to create a reactive variable that holds the current time
+- sets up an interval to update the time every second
+- registers an `on.cleanup` handler to clear the interval when the `Clock` is no longer used.
+- returns an object with a `display` property that formats the time nicely.
+
+The `Clock` is written using Starbeam's _universal_ APIs, which means it will work in any framework with a Starbeam renderer (like React).
+
+The function passed to the `Resource` function is called the "resource constructor". It is called
+when the component that uses the `Clock` is mounted, and whenever any reactive values used in the
+constructor change.
+
+:::tip What are resources?
+
+Resources can:
+
+- create stable values (like cells and functions)
+- register `on.cleanup` handlers, which run when the resource is cleaned up
+- return reactive values or functions that read from the stable reactive values created in the resource constructor.
+
+Resources are cleaned up when:
+
+- React unmounts the component that used the resource (including temporary unmounting in React 18)
+- Any reactive values used in the resource constructor change.
+
+A _resource_ is a self-contained way of creating a custom reactive object that needs to be cleaned
+up.
+
+Once a resource is `use`d, it behaves like any other reactive value. For example, you could access
+its properties in a function that's used in a component's render function, and the component would
+re-render whenever the resource's properties change.
+
+:::
+
+## The Code: Using `Clock` in React
+
+```snippet {#usage}
+
+```
+
+You can use the `Clock` in a React component by calling `use` with the `Clock` resource. The `use`
+function returns a reactive values (just like a `Cell`), and you can use it alongside other reactive
+values.
+
+## Using the `Clock` as a Reactive Value
+
+Instead of returning a formatted string, our resource returns the cell that holds the raw date.
+
+```snippet {#clock}
+
+```
+
+In the component's setup function, we call `use(Clock)`, as before, write a function that
+formats the time nicely, and use it in the component's render function.
+
+```snippet {#usage}
+
+```
+
+:::💡
+
+When a resource returns a reactive value, you don't need to call `.current` twice to get the value.
+The resource's `.current` value is the current value of the reactive value it returns.
+
+:::
+
+The takeaway here is that resources are just a way of creating custom reactive objects that need to
+be cleaned up. ==Once you've created a resource, you can use it just like any other reactive value.==
+
+This is powerful, because it allows you to freely compose reactive values, regardless of whether
+they need to be cleaned up or not.
+
+## Thinking in Resources
+
+Resources are a different way of thinking about most "effects".
+
+For example, instead of thinking about `setInterval` as a side effect, you think about the current
+time as a reactive value that changes over time, and which has cleanup logic.
+
+The only difference between a resource and any other custom reactive object is that you need to
+instantiate it with `use` rather than just calling a function, but once you've done that, you can
+use it just like any other reactive value.