or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-hooks.mdconcurrency-helpers.mdcore-hooks.mdfamily-patterns.mdindex.mdloadable-system.mdmemory-management.mdroot-provider.mdstate-definition.md

concurrency-helpers.mddocs/

0

# Concurrency Helpers

1

2

Utilities for coordinating multiple async operations and handling loading states. These helpers provide fine-grained control over how async selectors behave and allow for sophisticated async coordination patterns.

3

4

## Capabilities

5

6

### NoWait Pattern

7

8

Wraps a Recoil value to avoid suspense and error boundaries, returning a Loadable instead.

9

10

```typescript { .api }

11

/**

12

* Returns a selector that has the value of the provided atom or selector as a Loadable.

13

* This means you can use noWait() to avoid entering an error or suspense state in

14

* order to manually handle those cases.

15

*/

16

function noWait<T>(state: RecoilValue<T>): RecoilValueReadOnly<Loadable<T>>;

17

```

18

19

**Usage Examples:**

20

21

```typescript

22

import React from 'react';

23

import { noWait, useRecoilValue } from 'recoil';

24

25

// Component that handles async state manually

26

function UserProfile({ userId }) {

27

const userProfileLoadable = useRecoilValue(noWait(userProfileState(userId)));

28

29

switch (userProfileLoadable.state) {

30

case 'hasValue':

31

return <div>Welcome, {userProfileLoadable.contents.name}!</div>;

32

case 'loading':

33

return <div>Loading profile...</div>;

34

case 'hasError':

35

throw userProfileLoadable.contents; // Re-throw if needed

36

return <div>Error: {userProfileLoadable.contents.message}</div>;

37

}

38

}

39

40

// Selector that handles errors gracefully

41

const safeUserDataState = selector({

42

key: 'safeUserDataState',

43

get: ({get}) => {

44

const userLoadable = get(noWait(userState));

45

const preferencesLoadable = get(noWait(userPreferencesState));

46

47

return {

48

user: userLoadable.state === 'hasValue' ? userLoadable.contents : null,

49

preferences: preferencesLoadable.state === 'hasValue' ? preferencesLoadable.contents : {},

50

errors: {

51

user: userLoadable.state === 'hasError' ? userLoadable.contents : null,

52

preferences: preferencesLoadable.state === 'hasError' ? preferencesLoadable.contents : null,

53

}

54

};

55

},

56

});

57

```

58

59

### Wait for All

60

61

Waits for all provided Recoil values to resolve, similar to Promise.all().

62

63

```typescript { .api }

64

/**

65

* Waits for all values to resolve, returns unwrapped values

66

*/

67

function waitForAll<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(

68

param: RecoilValues

69

): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;

70

71

function waitForAll<RecoilValues extends { [key: string]: RecoilValue<any> }>(

72

param: RecoilValues

73

): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;

74

75

type UnwrapRecoilValues<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {

76

[P in keyof T]: UnwrapRecoilValue<T[P]>;

77

};

78

```

79

80

**Usage Examples:**

81

82

```typescript

83

import { waitForAll, selector, useRecoilValue } from 'recoil';

84

85

// Wait for multiple async selectors (array form)

86

const dashboardDataState = selector({

87

key: 'dashboardDataState',

88

get: ({get}) => {

89

const [user, posts, notifications] = get(waitForAll([

90

userState,

91

userPostsState,

92

userNotificationsState,

93

]));

94

95

return {

96

user,

97

posts,

98

notifications,

99

summary: `${posts.length} posts, ${notifications.length} notifications`,

100

};

101

},

102

});

103

104

// Wait for multiple async selectors (object form)

105

const userDashboardState = selector({

106

key: 'userDashboardState',

107

get: ({get}) => {

108

const data = get(waitForAll({

109

profile: userProfileState,

110

settings: userSettingsState,

111

activity: userActivityState,

112

}));

113

114

return {

115

...data,

116

lastLogin: data.activity.lastLogin,

117

displayName: data.profile.displayName || data.profile.email,

118

};

119

},

120

});

121

122

// Component using waitForAll

123

function Dashboard() {

124

const dashboardData = useRecoilValue(dashboardDataState);

125

126

return (

127

<div>

128

<h1>Welcome, {dashboardData.user.name}</h1>

129

<p>{dashboardData.summary}</p>

130

<PostsList posts={dashboardData.posts} />

131

<NotificationsList notifications={dashboardData.notifications} />

132

</div>

133

);

134

}

135

```

136

137

### Wait for Any

138

139

Waits for any of the provided values to resolve, returns all as Loadables.

140

141

```typescript { .api }

142

/**

143

* Waits for any value to resolve, returns all as Loadables

144

*/

145

function waitForAny<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(

146

param: RecoilValues

147

): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;

148

149

function waitForAny<RecoilValues extends { [key: string]: RecoilValue<any> }>(

150

param: RecoilValues

151

): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;

152

153

type UnwrapRecoilValueLoadables<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {

154

[P in keyof T]: Loadable<UnwrapRecoilValue<T[P]>>;

155

};

156

```

157

158

**Usage Examples:**

159

160

```typescript

161

import { waitForAny, selector, useRecoilValue } from 'recoil';

162

163

// Show data as soon as any source is available

164

const quickDataState = selector({

165

key: 'quickDataState',

166

get: ({get}) => {

167

const [cacheLoadable, apiLoadable] = get(waitForAny([

168

cachedDataState,

169

freshApiDataState,

170

]));

171

172

// Use cached data if available, otherwise wait for API

173

if (cacheLoadable.state === 'hasValue') {

174

return {

175

data: cacheLoadable.contents,

176

source: 'cache',

177

fresh: false,

178

};

179

}

180

181

if (apiLoadable.state === 'hasValue') {

182

return {

183

data: apiLoadable.contents,

184

source: 'api',

185

fresh: true,

186

};

187

}

188

189

// Still loading

190

throw new Promise(() => {}); // Suspend until something resolves

191

},

192

});

193

194

// Race between multiple data sources

195

const raceDataState = selector({

196

key: 'raceDataState',

197

get: ({get}) => {

198

const sources = get(waitForAny({

199

primary: primaryApiState,

200

fallback: fallbackApiState,

201

cache: cacheState,

202

}));

203

204

// Return first available source

205

for (const [sourceName, loadable] of Object.entries(sources)) {

206

if (loadable.state === 'hasValue') {

207

return {

208

data: loadable.contents,

209

source: sourceName,

210

};

211

}

212

}

213

214

// Check for errors

215

const errors = Object.entries(sources)

216

.filter(([_, loadable]) => loadable.state === 'hasError')

217

.map(([name, loadable]) => ({ source: name, error: loadable.contents }));

218

219

if (errors.length === Object.keys(sources).length) {

220

throw new Error(`All sources failed: ${errors.map(e => e.source).join(', ')}`);

221

}

222

223

// Still loading

224

throw new Promise(() => {});

225

},

226

});

227

```

228

229

### Wait for None

230

231

Returns all values as Loadables immediately without waiting for any to resolve.

232

233

```typescript { .api }

234

/**

235

* Returns loadables immediately without waiting for any to resolve

236

*/

237

function waitForNone<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(

238

param: RecoilValues

239

): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;

240

241

function waitForNone<RecoilValues extends { [key: string]: RecoilValue<any> }>(

242

param: RecoilValues

243

): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;

244

```

245

246

**Usage Examples:**

247

248

```typescript

249

import { waitForNone, selector, useRecoilValue } from 'recoil';

250

251

// Check loading states of multiple async operations

252

const loadingStatusState = selector({

253

key: 'loadingStatusState',

254

get: ({get}) => {

255

const [userLoadable, postsLoadable, notificationsLoadable] = get(waitForNone([

256

userState,

257

postsState,

258

notificationsState,

259

]));

260

261

return {

262

user: userLoadable.state,

263

posts: postsLoadable.state,

264

notifications: notificationsLoadable.state,

265

allLoaded: [userLoadable, postsLoadable, notificationsLoadable]

266

.every(l => l.state === 'hasValue'),

267

anyErrors: [userLoadable, postsLoadable, notificationsLoadable]

268

.some(l => l.state === 'hasError'),

269

};

270

},

271

});

272

273

// Progressive loading component

274

function ProgressiveLoader() {

275

const status = useRecoilValue(loadingStatusState);

276

277

return (

278

<div>

279

<div>User: {status.user}</div>

280

<div>Posts: {status.posts}</div>

281

<div>Notifications: {status.notifications}</div>

282

{status.allLoaded && <div>✅ All data loaded!</div>}

283

{status.anyErrors && <div>❌ Some data failed to load</div>}

284

</div>

285

);

286

}

287

288

// Incremental data display

289

const incrementalDataState = selector({

290

key: 'incrementalDataState',

291

get: ({get}) => {

292

const dataLoadables = get(waitForNone({

293

essential: essentialDataState,

294

secondary: secondaryDataState,

295

optional: optionalDataState,

296

}));

297

298

const result = {

299

essential: null,

300

secondary: null,

301

optional: null,

302

loadingCount: 0,

303

errorCount: 0,

304

};

305

306

Object.entries(dataLoadables).forEach(([key, loadable]) => {

307

switch (loadable.state) {

308

case 'hasValue':

309

result[key] = loadable.contents;

310

break;

311

case 'loading':

312

result.loadingCount++;

313

break;

314

case 'hasError':

315

result.errorCount++;

316

break;

317

}

318

});

319

320

return result;

321

},

322

});

323

```

324

325

### Wait for All Settled

326

327

Waits for all values to settle (resolve or reject), returning all as Loadables.

328

329

```typescript { .api }

330

/**

331

* Waits for all values to settle (resolve or reject), returns all as Loadables

332

*/

333

function waitForAllSettled<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(

334

param: RecoilValues

335

): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;

336

337

function waitForAllSettled<RecoilValues extends { [key: string]: RecoilValue<any> }>(

338

param: RecoilValues

339

): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;

340

```

341

342

**Usage Examples:**

343

344

```typescript

345

import { waitForAllSettled, selector, useRecoilValue } from 'recoil';

346

347

// Aggregate results even when some fail

348

const aggregateDataState = selector({

349

key: 'aggregateDataState',

350

get: ({get}) => {

351

const results = get(waitForAllSettled([

352

criticalDataState,

353

optionalDataState,

354

supplementaryDataState,

355

]));

356

357

const [criticalLoadable, optionalLoadable, supplementaryLoadable] = results;

358

359

// Must have critical data

360

if (criticalLoadable.state !== 'hasValue') {

361

throw criticalLoadable.state === 'hasError'

362

? criticalLoadable.contents

363

: new Error('Critical data still loading');

364

}

365

366

return {

367

critical: criticalLoadable.contents,

368

optional: optionalLoadable.state === 'hasValue' ? optionalLoadable.contents : null,

369

supplementary: supplementaryLoadable.state === 'hasValue' ? supplementaryLoadable.contents : null,

370

errors: {

371

optional: optionalLoadable.state === 'hasError' ? optionalLoadable.contents : null,

372

supplementary: supplementaryLoadable.state === 'hasError' ? supplementaryLoadable.contents : null,

373

},

374

};

375

},

376

});

377

378

// Report generation that includes partial results

379

const reportState = selector({

380

key: 'reportState',

381

get: ({get}) => {

382

const sections = get(waitForAllSettled({

383

summary: summaryDataState,

384

details: detailsDataState,

385

charts: chartsDataState,

386

appendix: appendixDataState,

387

}));

388

389

const report = {

390

timestamp: new Date().toISOString(),

391

sections: {},

392

errors: [],

393

warnings: [],

394

};

395

396

Object.entries(sections).forEach(([sectionName, loadable]) => {

397

switch (loadable.state) {

398

case 'hasValue':

399

report.sections[sectionName] = loadable.contents;

400

break;

401

case 'hasError':

402

report.errors.push({

403

section: sectionName,

404

error: loadable.contents.message,

405

});

406

break;

407

case 'loading':

408

report.warnings.push(`Section ${sectionName} is still loading`);

409

break;

410

}

411

});

412

413

return report;

414

},

415

});

416

417

// Component that shows partial results

418

function ReportViewer() {

419

const report = useRecoilValue(reportState);

420

421

return (

422

<div>

423

<h1>Report ({report.timestamp})</h1>

424

425

{Object.entries(report.sections).map(([name, data]) => (

426

<div key={name}>

427

<h2>{name}</h2>

428

<pre>{JSON.stringify(data, null, 2)}</pre>

429

</div>

430

))}

431

432

{report.errors.length > 0 && (

433

<div>

434

<h2>Errors</h2>

435

{report.errors.map((error, i) => (

436

<div key={i}>

437

{error.section}: {error.error}

438

</div>

439

))}

440

</div>

441

)}

442

443

{report.warnings.length > 0 && (

444

<div>

445

<h2>Warnings</h2>

446

{report.warnings.map((warning, i) => (

447

<div key={i}>{warning}</div>

448

))}

449

</div>

450

)}

451

</div>

452

);

453

}

454

```

455

456

## Coordination Patterns

457

458

**Common Use Cases:**

459

460

1. **Progressive Loading**: Use `waitForNone` to show data as it becomes available

461

2. **Fallback Chains**: Use `waitForAny` to implement fallback data sources

462

3. **Data Aggregation**: Use `waitForAll` when all data is required

463

4. **Resilient Loading**: Use `waitForAllSettled` when some failures are acceptable

464

5. **Manual Error Handling**: Use `noWait` to handle errors without suspense boundaries