Skip to content

Commit 831f353

Browse files
committed
feat!: make Effect.run fire-and-forget
Change Effect.run to return unit (fire-and-forget) and introduce Effect.runWithDisposer for cases where manual disposal is needed. - Effect.run now returns unit - Effect.runWithDisposer returns disposer - Update all tests to use runWithDisposer - Update documentation and examples BREAKING CHANGE: Effect.run now returns unit instead of disposer. If you need to manually dispose an effect, use Effect.runWithDisposer instead. Migration guide: Before: ```rescript let disposer = Effect.run(() => { Console.log(Signal.get(count)) None }) disposer.dispose() ``` After: ```rescript let disposer = Effect.runWithDisposer(() => { Console.log(Signal.get(count)) None }) disposer.dispose() ``` If you don't need the disposer, no changes are required: ```rescript Effect.run(() => { Console.log(Signal.get(count)) None }) ```
1 parent 7a43ed9 commit 831f353

7 files changed

Lines changed: 91 additions & 40 deletions

File tree

README.md

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,13 @@ let count = Signal.make(0)
4444
let doubled = Computed.make(() => Signal.get(count) * 2)
4545
4646
// Run a side effect (executes when dependencies change)
47-
let disposer = Effect.run(() => {
47+
Effect.run(() => {
4848
Console.log(`Count: ${Int.toString(Signal.get(count))}, Doubled: ${Int.toString(Signal.get(doubled))}`)
4949
None
5050
})
5151
5252
// Update the signal
5353
Signal.set(count, 5) // Logs: "Count: 5, Doubled: 10"
54-
55-
// Clean up when done
56-
disposer.dispose()
5754
```
5855

5956
## Usage
@@ -167,13 +164,29 @@ Effects run side effects in response to signal changes. They execute immediately
167164
```rescript
168165
let count = Signal.make(0)
169166
170-
let disposer = Effect.run(() => {
167+
// Fire-and-forget effect
168+
Effect.run(() => {
171169
Console.log(`Count is: ${Int.toString(Signal.get(count))}`)
172170
None
173171
})
174172
175173
Signal.set(count, 1) // Logs: "Count is: 1"
176-
disposer.dispose()
174+
```
175+
176+
#### Effect with Disposer
177+
178+
Use `Effect.runWithDisposer` when you need to manually stop the effect:
179+
180+
```rescript
181+
let count = Signal.make(0)
182+
183+
let disposer = Effect.runWithDisposer(() => {
184+
Console.log(`Count is: ${Int.toString(Signal.get(count))}`)
185+
None
186+
})
187+
188+
Signal.set(count, 1) // Logs: "Count is: 1"
189+
disposer.dispose() // Stop tracking
177190
```
178191

179192
#### Effect with Cleanup
@@ -183,7 +196,7 @@ Effects can return a cleanup function that runs before the next execution and on
183196
```rescript
184197
let url = Signal.make("/api/data")
185198
186-
let disposer = Effect.run(() => {
199+
let disposer = Effect.runWithDisposer(() => {
187200
let currentUrl = Signal.get(url)
188201
189202
// Start async operation
@@ -203,7 +216,7 @@ disposer.dispose() // Final cleanup
203216
#### Named Effects for Debugging
204217

205218
```rescript
206-
let disposer = Effect.run(
219+
Effect.run(
207220
() => {
208221
Console.log(Signal.get(count))
209222
None
@@ -221,7 +234,7 @@ let showDetails = Signal.make(false)
221234
let userData = Signal.make({name: "John"})
222235
let adminData = Signal.make({role: "admin"})
223236
224-
let disposer = Effect.run(() => {
237+
Effect.run(() => {
225238
if Signal.get(showDetails) {
226239
Console.log(Signal.get(userData)) // Tracked
227240
} else {
@@ -243,7 +256,7 @@ let firstName = Signal.make("John")
243256
let lastName = Signal.make("Doe")
244257
let runCount = ref(0)
245258
246-
let disposer = Effect.run(() => {
259+
Effect.run(() => {
247260
Console.log(Signal.get(firstName) ++ " " ++ Signal.get(lastName))
248261
runCount := runCount.contents + 1
249262
None
@@ -275,7 +288,7 @@ Read signal values without creating dependencies. Useful when you need a value b
275288
let count = Signal.make(0)
276289
let threshold = Signal.make(10)
277290
278-
let disposer = Effect.run(() => {
291+
Effect.run(() => {
279292
let current = Signal.get(count)
280293
let limit = Signal.untrack(() => Signal.get(threshold))
281294
@@ -393,18 +406,26 @@ Run side effects in response to signal changes.
393406
```rescript
394407
type disposer = {dispose: unit => unit}
395408
396-
// Run an effect
409+
// Run a fire-and-forget effect
397410
let run: (
398411
unit => option<unit => unit>,
399412
~name: option<string>=?
413+
) => unit
414+
415+
// Run an effect with manual disposal
416+
let runWithDisposer: (
417+
unit => option<unit => unit>,
418+
~name: option<string>=?
400419
) => disposer
401420
```
402421

403422
**Parameters:**
404423
- `fn`: Effect function to execute. Can return `None` or `Some(cleanupFn)`
405424
- `~name`: Optional name for debugging
406425

407-
**Returns:** A disposer object with a `dispose()` method
426+
**`Effect.run`**: Fire-and-forget effect. Returns `unit`.
427+
428+
**`Effect.runWithDisposer`**: Returns a disposer object with a `dispose()` method for manual cleanup.
408429

409430
**Note:** Effects run immediately and re-run whenever tracked dependencies change. Cleanup functions run before re-execution and on disposal.
410431

@@ -428,7 +449,7 @@ let isValid = Computed.make(() => {
428449
})
429450
430451
// Effect for auto-save
431-
let disposer = Effect.run(() => {
452+
Effect.run(() => {
432453
if Signal.get(isValid) {
433454
saveToLocalStorage(Signal.get(formData))
434455
}
@@ -443,7 +464,7 @@ let userId = Signal.make(1)
443464
let userData = Signal.make(None)
444465
let isLoading = Signal.make(false)
445466
446-
let disposer = Effect.run(() => {
467+
Effect.run(() => {
447468
let id = Signal.get(userId)
448469
449470
Signal.set(isLoading, true)
@@ -503,7 +524,7 @@ let movePoint = (dx, dy, dz) => {
503524
}
504525
505526
// Effect only runs once per movePoint call
506-
let disposer = Effect.run(() => {
527+
Effect.run(() => {
507528
Console.log(
508529
`Position: (${Int.toString(Signal.get(x))}, ${Int.toString(Signal.get(y))}, ${Int.toString(Signal.get(z))})`
509530
)
@@ -520,7 +541,7 @@ let config = Signal.make({theme: "dark", locale: "en"})
520541
// Frequently changing data
521542
let data = Signal.make([])
522543
523-
let disposer = Effect.run(() => {
544+
Effect.run(() => {
524545
let items = Signal.get(data)
525546
526547
// Read config without tracking—we'll manually refresh when config changes

docs-website/src/pages/Pages__ApiEffect.res

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,44 @@ let make = () => {
2020
</div>
2121
<Typography
2222
text={static(
23-
"Creates and immediately runs an effect. The effect re-runs whenever any signal or computed it reads changes.",
23+
"Creates and immediately runs a fire-and-forget effect. The effect re-runs whenever any signal or computed it reads changes.",
2424
)}
2525
/>
2626
<CodeBlock
2727
language="rescript"
2828
code={`let count = Signal.make(0)
2929
30-
let disposer = Effect.run(() => {
30+
Effect.run(() => {
3131
Console.log(\`Count is: \${Signal.get(count)->Int.toString}\`)
32+
None
3233
})
3334
3435
// Logs: "Count is: 0"
3536
Signal.set(count, 1)
3637
// Logs: "Count is: 1"`}
38+
/>
39+
<div class="heading-anchor" id="effect-run-with-disposer">
40+
<Typography text={static("Effect.runWithDisposer(fn)")} variant={H3} />
41+
<a class="anchor-link" href="#effect-run-with-disposer"> {"#"->Component.text} </a>
42+
</div>
43+
<Typography
44+
text={static(
45+
"Creates an effect and returns a disposer for manual cleanup. Use this when you need to stop the effect later.",
46+
)}
47+
/>
48+
<CodeBlock
49+
language="rescript"
50+
code={`let count = Signal.make(0)
51+
52+
let disposer = Effect.runWithDisposer(() => {
53+
Console.log(\`Count is: \${Signal.get(count)->Int.toString}\`)
54+
None
55+
})
56+
57+
Signal.set(count, 1)
58+
// Logs: "Count is: 1"
59+
60+
disposer.dispose() // Stop the effect`}
3761
/>
3862
<div class="heading-anchor" id="effect-run-named">
3963
<Typography text={static("Effect.run(~name, fn)")} variant={H3} />
@@ -44,6 +68,7 @@ Signal.set(count, 1)
4468
language="rescript"
4569
code={`Effect.run(~name="logger", () => {
4670
Console.log(Signal.get(count))
71+
None
4772
})`}
4873
/>
4974
<Separator />
@@ -85,12 +110,12 @@ Signal.set(count, 1)
85110
</div>
86111
<Typography
87112
text={static(
88-
"Effect.run returns a disposer object. Call dispose() to stop the effect and run any cleanup function.",
113+
"Effect.runWithDisposer returns a disposer object. Call dispose() to stop the effect and run any cleanup function.",
89114
)}
90115
/>
91116
<CodeBlock
92117
language="rescript"
93-
code={`let disposer = Effect.run(() => {
118+
code={`let disposer = Effect.runWithDisposer(() => {
94119
Console.log(Signal.get(count))
95120
Some(() => Console.log("Cleanup!"))
96121
})

docs-website/src/pages/Pages__GettingStarted.res

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Computed.get(doubled) // 10`}
103103
code={`let count = Signal.make(0)
104104
Effect.run(() => {
105105
Console.log(\`Count changed to: \${Signal.get(count)->Int.toString}\`)
106+
None
106107
})`}
107108
/>
108109
<EditOnGitHub pageName="Pages__GettingStarted" />

src/signals/Signals__Effects.res

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Scheduler = Signals__Scheduler
44

55
type disposer = {dispose: unit => unit}
66

7-
let run = (fn: unit => option<unit => unit>, ~name: option<string>=?): disposer => {
7+
let runWithDisposer = (fn: unit => option<unit => unit>, ~name: option<string>=?): disposer => {
88
let observerId = Id.make()
99
let cleanup: ref<option<unit => unit>> = ref(None)
1010

@@ -59,3 +59,7 @@ let run = (fn: unit => option<unit => unit>, ~name: option<string>=?): disposer
5959

6060
{dispose: dispose}
6161
}
62+
63+
let run = (fn: unit => option<unit => unit>, ~name: option<string>=?): unit => {
64+
let _ = runWithDisposer(fn, ~name?)
65+
}

tests/ComputedTests.res

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ let tests = suite(
133133
let show = Signal.make(true)
134134
let result = ref(0)
135135

136-
let disposer = Effect.run(() => {
136+
let disposer = Effect.runWithDisposer(() => {
137137
if Signal.get(show) {
138138
result := Signal.get(doubled)
139139
}

tests/EffectTests.res

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ let tests = suite(
77
[
88
test("effect runs initially", () => {
99
let runCount = ref(0)
10-
let disposer = Effect.run(() => {
10+
let disposer = Effect.runWithDisposer(() => {
1111
runCount := runCount.contents + 1
1212
None
1313
})
@@ -17,7 +17,7 @@ let tests = suite(
1717
test("effect runs when dependency changes", () => {
1818
let count = Signal.make(0)
1919
let runCount = ref(0)
20-
let disposer = Effect.run(() => {
20+
let disposer = Effect.runWithDisposer(() => {
2121
let _ = Signal.get(count)
2222
runCount := runCount.contents + 1
2323
None
@@ -30,7 +30,7 @@ let tests = suite(
3030
let a = Signal.make(1)
3131
let b = Signal.make(2)
3232
let sum = ref(0)
33-
let disposer = Effect.run(() => {
33+
let disposer = Effect.runWithDisposer(() => {
3434
sum := Signal.get(a) + Signal.get(b)
3535
None
3636
})
@@ -43,7 +43,7 @@ let tests = suite(
4343
test("effect cleanup runs on re-execution", () => {
4444
let count = Signal.make(0)
4545
let cleanupCount = ref(0)
46-
let disposer = Effect.run(() => {
46+
let disposer = Effect.runWithDisposer(() => {
4747
let _ = Signal.get(count)
4848
Some(() => cleanupCount := cleanupCount.contents + 1)
4949
})
@@ -54,7 +54,7 @@ let tests = suite(
5454
}),
5555
test("effect cleanup runs on disposal", () => {
5656
let cleaned = ref(false)
57-
let disposer = Effect.run(() => Some(() => cleaned := true))
57+
let disposer = Effect.runWithDisposer(() => Some(() => cleaned := true))
5858
disposer.dispose()
5959
assertTrue(cleaned.contents, ~message="Cleanup should run on disposal")
6060
}),
@@ -63,7 +63,7 @@ let tests = suite(
6363
let a = Signal.make(1)
6464
let b = Signal.make(2)
6565
let result = ref(0)
66-
let disposer = Effect.run(() => {
66+
let disposer = Effect.runWithDisposer(() => {
6767
result := if Signal.get(toggle) {
6868
Signal.get(a)
6969
} else {
@@ -81,12 +81,12 @@ let tests = suite(
8181
let outer = Signal.make(0)
8282
let outerRuns = ref(0)
8383
let innerRuns = ref(0)
84-
let disposer1 = Effect.run(() => {
84+
let disposer1 = Effect.runWithDisposer(() => {
8585
let _ = Signal.get(outer)
8686
outerRuns := outerRuns.contents + 1
8787
None
8888
})
89-
let disposer2 = Effect.run(() => {
89+
let disposer2 = Effect.runWithDisposer(() => {
9090
let _ = Signal.get(outer)
9191
innerRuns := innerRuns.contents + 1
9292
None
@@ -103,7 +103,7 @@ let tests = suite(
103103
let base = Signal.make(2)
104104
let doubled = Computed.make(() => Signal.get(base) * 2)
105105
let result = ref(0)
106-
let disposer = Effect.run(() => {
106+
let disposer = Effect.runWithDisposer(() => {
107107
result := Signal.get(doubled)
108108
None
109109
})
@@ -116,7 +116,7 @@ let tests = suite(
116116
test("effect disposal stops tracking", () => {
117117
let count = Signal.make(0)
118118
let runCount = ref(0)
119-
let disposer = Effect.run(() => {
119+
let disposer = Effect.runWithDisposer(() => {
120120
let _ = Signal.get(count)
121121
runCount := runCount.contents + 1
122122
None
@@ -134,7 +134,7 @@ let tests = suite(
134134
test("effect with array mutation tracking", () => {
135135
let items = Signal.make([1, 2, 3])
136136
let length = ref(0)
137-
let disposer = Effect.run(() => {
137+
let disposer = Effect.runWithDisposer(() => {
138138
length := Array.length(Signal.get(items))
139139
None
140140
})
@@ -148,7 +148,7 @@ let tests = suite(
148148
let tracked = Signal.make(1)
149149
let untracked = Signal.make(10)
150150
let runCount = ref(0)
151-
let disposer = Effect.run(() => {
151+
let disposer = Effect.runWithDisposer(() => {
152152
let _ = Signal.get(tracked)
153153
let _ = Signal.peek(untracked)
154154
runCount := runCount.contents + 1
@@ -165,7 +165,7 @@ let tests = suite(
165165
result
166166
}),
167167
test("multiple disposals are safe", () => {
168-
let disposer = Effect.run(() => None)
168+
let disposer = Effect.runWithDisposer(() => None)
169169
disposer.dispose()
170170
disposer.dispose()
171171
Pass

0 commit comments

Comments
 (0)