or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

computed.mdeffect-scopes.mdeffects.mdindex.mdreactive-objects.mdrefs.mdutilities.mdwatchers.md

effect-scopes.mddocs/

0

# Effect Scopes

1

2

Effect scopes provide a way to group and manage reactive effects for organized cleanup and lifecycle management. They enable batch disposal of effects and nested scope hierarchies.

3

4

## Capabilities

5

6

### effectScope()

7

8

Creates an effect scope object that can capture reactive effects created within it for later disposal.

9

10

```typescript { .api }

11

/**

12

* Creates an effect scope for capturing and managing effects

13

* @param detached - Whether to create a detached scope (not connected to parent)

14

* @returns New EffectScope instance

15

*/

16

function effectScope(detached?: boolean): EffectScope;

17

18

class EffectScope {

19

constructor(public detached?: boolean);

20

21

/**

22

* Whether the scope is currently active

23

*/

24

get active(): boolean;

25

26

/**

27

* Pause all effects in this scope

28

*/

29

pause(): void;

30

31

/**

32

* Resume all effects in this scope

33

*/

34

resume(): void;

35

36

/**

37

* Run a function within this scope, capturing any effects created

38

*/

39

run<T>(fn: () => T): T | undefined;

40

41

/**

42

* Stop all effects in this scope and dispose resources

43

*/

44

stop(fromParent?: boolean): void;

45

}

46

```

47

48

**Usage Examples:**

49

50

```typescript

51

import { ref, effect, effectScope } from "@vue/reactivity";

52

53

const count = ref(0);

54

const name = ref("Alice");

55

56

// Create an effect scope

57

const scope = effectScope();

58

59

scope.run(() => {

60

// Effects created within run() are captured by the scope

61

effect(() => {

62

console.log(`Count: ${count.value}`);

63

});

64

65

effect(() => {

66

console.log(`Name: ${name.value}`);

67

});

68

69

// Nested scopes are also captured

70

const nestedScope = effectScope();

71

nestedScope.run(() => {

72

effect(() => {

73

console.log(`Nested: ${count.value} - ${name.value}`);

74

});

75

});

76

});

77

78

// All effects run initially

79

count.value = 1; // Triggers all 3 effects

80

name.value = "Bob"; // Triggers name and nested effects

81

82

// Stop all effects in the scope at once

83

scope.stop();

84

85

count.value = 2; // No effects run (all stopped)

86

name.value = "Charlie"; // No effects run (all stopped)

87

```

88

89

### Detached Scopes

90

91

Create scopes that are independent of parent scopes:

92

93

```typescript

94

import { ref, effect, effectScope } from "@vue/reactivity";

95

96

const count = ref(0);

97

98

const parentScope = effectScope();

99

100

parentScope.run(() => {

101

effect(() => {

102

console.log(`Parent effect: ${count.value}`);

103

});

104

105

// Detached scope won't be stopped when parent stops

106

const detachedScope = effectScope(true); // detached = true

107

108

detachedScope.run(() => {

109

effect(() => {

110

console.log(`Detached effect: ${count.value}`);

111

});

112

});

113

114

return detachedScope; // Keep reference to manage separately

115

});

116

117

count.value = 1; // Both effects run

118

119

// Stop parent scope

120

parentScope.stop();

121

122

count.value = 2; // Only detached effect runs

123

124

// Must stop detached scope separately

125

// detachedScope.stop();

126

```

127

128

### getCurrentScope()

129

130

Get the currently active effect scope:

131

132

```typescript { .api }

133

/**

134

* Returns the current active effect scope if there is one

135

* @returns Current active EffectScope or undefined

136

*/

137

function getCurrentScope(): EffectScope | undefined;

138

```

139

140

**Usage Examples:**

141

142

```typescript

143

import { effectScope, getCurrentScope, effect } from "@vue/reactivity";

144

145

const scope = effectScope();

146

147

scope.run(() => {

148

console.log("Current scope:", getCurrentScope()); // Logs the scope instance

149

150

effect(() => {

151

const currentScope = getCurrentScope();

152

console.log("Effect is running in scope:", currentScope === scope);

153

});

154

});

155

156

// Outside of scope

157

console.log("Outside scope:", getCurrentScope()); // undefined

158

```

159

160

### onScopeDispose()

161

162

Register callbacks that run when the scope is disposed:

163

164

```typescript { .api }

165

/**

166

* Register a dispose callback on the current active effect scope

167

* @param fn - Callback function to run when scope is disposed

168

* @param failSilently - If true, won't warn when no active scope

169

*/

170

function onScopeDispose(fn: () => void, failSilently?: boolean): void;

171

```

172

173

**Usage Examples:**

174

175

```typescript

176

import { ref, effect, effectScope, onScopeDispose } from "@vue/reactivity";

177

178

const count = ref(0);

179

180

const scope = effectScope();

181

182

scope.run(() => {

183

// Register cleanup for the entire scope

184

onScopeDispose(() => {

185

console.log("Scope is being disposed");

186

});

187

188

// Setup resources that need cleanup

189

const timer = setInterval(() => {

190

console.log(`Timer tick: ${count.value}`);

191

}, 1000);

192

193

// Register cleanup for the timer

194

onScopeDispose(() => {

195

clearInterval(timer);

196

console.log("Timer cleaned up");

197

});

198

199

// Effect that uses the count

200

effect(() => {

201

console.log(`Effect: ${count.value}`);

202

});

203

204

// Register effect-specific cleanup

205

onScopeDispose(() => {

206

console.log("Effect cleanup");

207

});

208

});

209

210

// Let it run for a bit

211

setTimeout(() => {

212

scope.stop(); // Triggers all cleanup callbacks

213

}, 5000);

214

```

215

216

### Nested Scopes

217

218

Create hierarchical scope structures for complex applications:

219

220

```typescript

221

import { ref, effect, effectScope, onScopeDispose } from "@vue/reactivity";

222

223

const globalCount = ref(0);

224

225

// Application-level scope

226

const appScope = effectScope();

227

228

appScope.run(() => {

229

console.log("App scope initialized");

230

231

onScopeDispose(() => {

232

console.log("App scope disposed");

233

});

234

235

// Feature-level scope

236

const featureScope = effectScope();

237

238

featureScope.run(() => {

239

console.log("Feature scope initialized");

240

241

onScopeDispose(() => {

242

console.log("Feature scope disposed");

243

});

244

245

// Component-level scope

246

const componentScope = effectScope();

247

248

componentScope.run(() => {

249

console.log("Component scope initialized");

250

251

onScopeDispose(() => {

252

console.log("Component scope disposed");

253

});

254

255

effect(() => {

256

console.log(`Component watching: ${globalCount.value}`);

257

});

258

});

259

260

effect(() => {

261

console.log(`Feature watching: ${globalCount.value}`);

262

});

263

});

264

265

effect(() => {

266

console.log(`App watching: ${globalCount.value}`);

267

});

268

});

269

270

globalCount.value = 1; // All effects run

271

272

// Stopping app scope stops all nested scopes

273

appScope.stop();

274

// Logs:

275

// "Component scope disposed"

276

// "Feature scope disposed"

277

// "App scope disposed"

278

279

globalCount.value = 2; // No effects run

280

```

281

282

### Pause and Resume Scopes

283

284

Control scope execution without destroying effects:

285

286

```typescript

287

import { ref, effect, effectScope } from "@vue/reactivity";

288

289

const count = ref(0);

290

291

const scope = effectScope();

292

293

scope.run(() => {

294

effect(() => {

295

console.log(`Active effect: ${count.value}`);

296

});

297

298

effect(() => {

299

console.log(`Another effect: ${count.value * 2}`);

300

});

301

});

302

303

count.value = 1; // Both effects run

304

305

// Pause all effects in scope

306

scope.pause();

307

count.value = 2; // No effects run (paused)

308

309

// Resume all effects in scope

310

scope.resume();

311

count.value = 3; // Both effects run again

312

313

// Check if scope is active

314

console.log("Scope active:", scope.active); // true

315

316

scope.stop();

317

console.log("Scope active:", scope.active); // false

318

```

319

320

### Advanced Scope Patterns

321

322

#### Conditional Scope Management

323

324

```typescript

325

import { ref, computed, effectScope, effect } from "@vue/reactivity";

326

327

const isFeatureEnabled = ref(false);

328

const count = ref(0);

329

330

let featureScope: EffectScope | null = null;

331

332

// Watch for feature toggle

333

effect(() => {

334

if (isFeatureEnabled.value) {

335

// Create scope when feature is enabled

336

if (!featureScope) {

337

featureScope = effectScope();

338

339

featureScope.run(() => {

340

effect(() => {

341

console.log(`Feature active: ${count.value}`);

342

});

343

344

// More feature-specific effects...

345

});

346

}

347

} else {

348

// Clean up scope when feature is disabled

349

if (featureScope) {

350

featureScope.stop();

351

featureScope = null;

352

}

353

}

354

});

355

356

// Toggle feature

357

isFeatureEnabled.value = true; // Creates and starts feature effects

358

count.value = 1; // Feature effect runs

359

360

isFeatureEnabled.value = false; // Stops and cleans up feature effects

361

count.value = 2; // No feature effect runs

362

```

363

364

#### Resource Management with Scopes

365

366

```typescript

367

import { ref, effectScope, onScopeDispose } from "@vue/reactivity";

368

369

interface Resource {

370

id: string;

371

cleanup(): void;

372

}

373

374

function useResourceManager() {

375

const resources = new Map<string, Resource>();

376

377

const scope = effectScope();

378

379

const addResource = (id: string, resource: Resource) => {

380

resources.set(id, resource);

381

382

// Register cleanup for this resource

383

onScopeDispose(() => {

384

resource.cleanup();

385

resources.delete(id);

386

console.log(`Resource ${id} cleaned up`);

387

});

388

};

389

390

const removeResource = (id: string) => {

391

const resource = resources.get(id);

392

if (resource) {

393

resource.cleanup();

394

resources.delete(id);

395

}

396

};

397

398

const cleanup = () => {

399

scope.stop(); // Triggers cleanup of all resources

400

};

401

402

return scope.run(() => ({

403

addResource,

404

removeResource,

405

cleanup,

406

resourceCount: () => resources.size

407

}))!;

408

}

409

410

// Usage

411

const manager = useResourceManager();

412

413

// Add some resources

414

manager.addResource("timer1", {

415

id: "timer1",

416

cleanup: () => console.log("Timer 1 stopped")

417

});

418

419

manager.addResource("listener", {

420

id: "listener",

421

cleanup: () => console.log("Event listener removed")

422

});

423

424

console.log("Resource count:", manager.resourceCount()); // 2

425

426

// Clean up all resources at once

427

manager.cleanup();

428

// Logs:

429

// "Timer 1 stopped"

430

// "Event listener removed"

431

// "Resource timer1 cleaned up"

432

// "Resource listener cleaned up"

433

```

434

435

## Types

436

437

```typescript { .api }

438

// Core effect scope class

439

class EffectScope {

440

/**

441

* @param detached - If true, this scope's parent scope will not stop it

442

*/

443

constructor(public detached?: boolean);

444

445

/**

446

* Whether the scope is currently active

447

*/

448

get active(): boolean;

449

450

/**

451

* Pause all effects in this scope

452

*/

453

pause(): void;

454

455

/**

456

* Resume all effects in this scope

457

*/

458

resume(): void;

459

460

/**

461

* Run a function within this scope context

462

* @param fn - Function to run in scope

463

* @returns Function result or undefined if scope is inactive

464

*/

465

run<T>(fn: () => T): T | undefined;

466

467

/**

468

* Activate this scope (internal method)

469

*/

470

on(): void;

471

472

/**

473

* Deactivate this scope (internal method)

474

*/

475

off(): void;

476

477

/**

478

* Stop all effects in this scope and dispose resources

479

* @param fromParent - Whether stop was called from parent scope

480

*/

481

stop(fromParent?: boolean): void;

482

}

483

484

// Internal scope state

485

interface EffectScopeState {

486

parent: EffectScope | undefined;

487

scopes: EffectScope[] | undefined;

488

effects: ReactiveEffect[] | undefined;

489

cleanups: (() => void)[] | undefined;

490

index: number;

491

active: boolean;

492

}

493

```

494

495

## Scope Lifecycle

496

497

Understanding the effect scope lifecycle is important for proper resource management:

498

499

1. **Creation**: `effectScope()` creates a new inactive scope

500

2. **Activation**: `scope.run()` activates the scope and captures effects

501

3. **Collection**: All effects and nested scopes created during `run()` are captured

502

4. **Execution**: Effects run normally while scope is active

503

5. **Pause/Resume**: `pause()` and `resume()` control effect execution without disposal

504

6. **Disposal**: `stop()` permanently deactivates scope and cleans up all resources

505

506

```typescript

507

import { effectScope, effect, onScopeDispose } from "@vue/reactivity";

508

509

const scope = effectScope();

510

console.log("1. Scope created, active:", scope.active); // false

511

512

const result = scope.run(() => {

513

console.log("2. Inside run(), active:", scope.active); // true

514

515

effect(() => {

516

console.log("3. Effect created and captured");

517

});

518

519

onScopeDispose(() => {

520

console.log("5. Cleanup executed");

521

});

522

523

return "result";

524

});

525

526

console.log("4. Run completed, result:", result); // "result"

527

528

scope.stop();

529

// Logs: "5. Cleanup executed"

530

console.log("6. After stop, active:", scope.active); // false

531

```