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

loadable-system.mddocs/

0

# Loadable System

1

2

System for handling async state with loading, error, and success states. Loadables provide a unified interface for working with synchronous values, promises, and error states without using React suspense boundaries.

3

4

## Capabilities

5

6

### Loadable Types

7

8

Core loadable interface and its variants for different states.

9

10

```typescript { .api }

11

/**

12

* Discriminated union representing the state of async operations

13

*/

14

type Loadable<T> = ValueLoadable<T> | LoadingLoadable<T> | ErrorLoadable<T>;

15

16

interface BaseLoadable<T> {

17

/** Get the value, throwing if not available */

18

getValue: () => T;

19

/** Convert to a Promise */

20

toPromise: () => Promise<T>;

21

/** Get value or throw error/promise */

22

valueOrThrow: () => T;

23

/** Get error or throw if not error state */

24

errorOrThrow: () => any;

25

/** Get promise or throw if not loading */

26

promiseOrThrow: () => Promise<T>;

27

/** Check equality with another loadable */

28

is: (other: Loadable<any>) => boolean;

29

/** Transform the loadable value */

30

map: <S>(map: (from: T) => Loadable<S> | Promise<S> | S) => Loadable<S>;

31

}

32

33

interface ValueLoadable<T> extends BaseLoadable<T> {

34

state: 'hasValue';

35

contents: T;

36

/** Get value if available, undefined otherwise */

37

valueMaybe: () => T;

38

/** Get error if available, undefined otherwise */

39

errorMaybe: () => undefined;

40

/** Get promise if available, undefined otherwise */

41

promiseMaybe: () => undefined;

42

}

43

44

interface LoadingLoadable<T> extends BaseLoadable<T> {

45

state: 'loading';

46

contents: Promise<T>;

47

valueMaybe: () => undefined;

48

errorMaybe: () => undefined;

49

promiseMaybe: () => Promise<T>;

50

}

51

52

interface ErrorLoadable<T> extends BaseLoadable<T> {

53

state: 'hasError';

54

contents: any;

55

valueMaybe: () => undefined;

56

errorMaybe: () => any;

57

promiseMaybe: () => undefined;

58

}

59

```

60

61

**Usage Examples:**

62

63

```typescript

64

import React from 'react';

65

import { useRecoilValueLoadable } from 'recoil';

66

67

// Component handling all loadable states

68

function AsyncDataDisplay({ dataState }) {

69

const dataLoadable = useRecoilValueLoadable(dataState);

70

71

switch (dataLoadable.state) {

72

case 'hasValue':

73

return <div>Data: {JSON.stringify(dataLoadable.contents)}</div>;

74

75

case 'loading':

76

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

77

78

case 'hasError':

79

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

80

}

81

}

82

83

// Using loadable methods

84

function LoadableMethodsExample({ dataState }) {

85

const dataLoadable = useRecoilValueLoadable(dataState);

86

87

// Safe value access

88

const value = dataLoadable.valueMaybe();

89

const error = dataLoadable.errorMaybe();

90

const promise = dataLoadable.promiseMaybe();

91

92

return (

93

<div>

94

{value && <div>Value: {JSON.stringify(value)}</div>}

95

{error && <div>Error: {error.message}</div>}

96

{promise && <div>Loading...</div>}

97

</div>

98

);

99

}

100

101

// Transform loadable values

102

function TransformedLoadable({ userState }) {

103

const userLoadable = useRecoilValueLoadable(userState);

104

105

// Transform the loadable to get display name

106

const displayNameLoadable = userLoadable.map(user =>

107

user.displayName || user.email || 'Anonymous'

108

);

109

110

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

111

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

112

}

113

114

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

115

}

116

```

117

118

### RecoilLoadable Namespace

119

120

Factory functions and utilities for creating and working with loadables.

121

122

```typescript { .api }

123

namespace RecoilLoadable {

124

/**

125

* Factory to make a Loadable object. If a Promise is provided the Loadable will

126

* be in a 'loading' state until the Promise is either resolved or rejected.

127

*/

128

function of<T>(x: T | Promise<T> | Loadable<T>): Loadable<T>;

129

130

/**

131

* Factory to make a Loadable object in an error state

132

*/

133

function error(x: any): ErrorLoadable<any>;

134

135

/**

136

* Factory to make a loading Loadable which never resolves

137

*/

138

function loading(): LoadingLoadable<any>;

139

140

/**

141

* Factory to make a Loadable which is resolved when all of the Loadables provided

142

* to it are resolved or any one has an error. The value is an array of the values

143

* of all of the provided Loadables. This is comparable to Promise.all() for Loadables.

144

*/

145

function all<Inputs extends any[] | [Loadable<any>]>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;

146

function all<Inputs extends {[key: string]: any}>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;

147

148

/**

149

* Returns true if the provided parameter is a Loadable type

150

*/

151

function isLoadable(x: any): x is Loadable<any>;

152

}

153

154

type UnwrapLoadables<T extends any[] | { [key: string]: any }> = {

155

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

156

};

157

158

type UnwrapLoadable<T> = T extends Loadable<infer R> ? R : T extends Promise<infer P> ? P : T;

159

```

160

161

**Usage Examples:**

162

163

```typescript

164

import { RecoilLoadable, selector } from 'recoil';

165

166

// Create loadables from various inputs

167

const exampleSelector = selector({

168

key: 'exampleSelector',

169

get: () => {

170

// From value

171

const valueLoadable = RecoilLoadable.of('hello');

172

173

// From promise

174

const promiseLoadable = RecoilLoadable.of(

175

fetch('/api/data').then(r => r.json())

176

);

177

178

// Error loadable

179

const errorLoadable = RecoilLoadable.error(new Error('Something went wrong'));

180

181

// Loading loadable that never resolves

182

const loadingLoadable = RecoilLoadable.loading();

183

184

return { valueLoadable, promiseLoadable, errorLoadable, loadingLoadable };

185

},

186

});

187

188

// Combine multiple loadables

189

const combinedDataSelector = selector({

190

key: 'combinedDataSelector',

191

get: async ({get}) => {

192

const userLoadable = get(noWait(userState));

193

const settingsLoadable = get(noWait(settingsState));

194

const preferencesLoadable = get(noWait(preferencesState));

195

196

// Wait for all to resolve

197

const combinedLoadable = RecoilLoadable.all([

198

userLoadable,

199

settingsLoadable,

200

preferencesLoadable,

201

]);

202

203

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

204

const [user, settings, preferences] = combinedLoadable.contents;

205

return { user, settings, preferences };

206

}

207

208

// Propagate loading or error state

209

return combinedLoadable.contents;

210

},

211

});

212

213

// Combine object of loadables

214

const dashboardDataSelector = selector({

215

key: 'dashboardDataSelector',

216

get: async ({get}) => {

217

const loadables = {

218

user: get(noWait(userState)),

219

posts: get(noWait(postsState)),

220

notifications: get(noWait(notificationsState)),

221

};

222

223

const combinedLoadable = RecoilLoadable.all(loadables);

224

225

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

226

return {

227

...combinedLoadable.contents,

228

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

229

};

230

}

231

232

throw combinedLoadable.contents;

233

},

234

});

235

236

// Type checking

237

function processUnknownValue(value: unknown) {

238

if (RecoilLoadable.isLoadable(value)) {

239

switch (value.state) {

240

case 'hasValue':

241

console.log('Loadable value:', value.contents);

242

break;

243

case 'loading':

244

console.log('Loadable is loading');

245

break;

246

case 'hasError':

247

console.log('Loadable error:', value.contents);

248

break;

249

}

250

} else {

251

console.log('Not a loadable:', value);

252

}

253

}

254

```

255

256

### Loadable Patterns

257

258

Common patterns for working with loadables in complex scenarios.

259

260

**Usage Examples:**

261

262

```typescript

263

import React from 'react';

264

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

265

266

// Fallback chain with loadables

267

const dataWithFallbackSelector = selector({

268

key: 'dataWithFallbackSelector',

269

get: ({get}) => {

270

const primaryLoadable = get(noWait(primaryDataState));

271

const secondaryLoadable = get(noWait(secondaryDataState));

272

const cacheLoadable = get(noWait(cacheDataState));

273

274

// Try primary first

275

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

276

return RecoilLoadable.of({

277

data: primaryLoadable.contents,

278

source: 'primary',

279

});

280

}

281

282

// Try secondary

283

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

284

return RecoilLoadable.of({

285

data: secondaryLoadable.contents,

286

source: 'secondary',

287

});

288

}

289

290

// Use cache as last resort

291

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

292

return RecoilLoadable.of({

293

data: cacheLoadable.contents,

294

source: 'cache',

295

stale: true,

296

});

297

}

298

299

// All are loading or have errors

300

if (primaryLoadable.state === 'loading' ||

301

secondaryLoadable.state === 'loading') {

302

return RecoilLoadable.loading();

303

}

304

305

// Return the primary error as it's most important

306

return RecoilLoadable.error(primaryLoadable.contents);

307

},

308

});

309

310

// Partial data accumulator

311

const partialDataSelector = selector({

312

key: 'partialDataSelector',

313

get: ({get}) => {

314

const loadables = {

315

essential: get(noWait(essentialDataState)),

316

important: get(noWait(importantDataState)),

317

optional: get(noWait(optionalDataState)),

318

};

319

320

const result = {

321

essential: null,

322

important: null,

323

optional: null,

324

status: 'partial',

325

};

326

327

// Must have essential data

328

if (loadables.essential.state !== 'hasValue') {

329

if (loadables.essential.state === 'hasError') {

330

return RecoilLoadable.error(loadables.essential.contents);

331

}

332

return RecoilLoadable.loading();

333

}

334

335

result.essential = loadables.essential.contents;

336

337

// Include other data if available

338

if (loadables.important.state === 'hasValue') {

339

result.important = loadables.important.contents;

340

}

341

342

if (loadables.optional.state === 'hasValue') {

343

result.optional = loadables.optional.contents;

344

}

345

346

// Mark as complete if we have everything

347

if (result.important && result.optional) {

348

result.status = 'complete';

349

}

350

351

return RecoilLoadable.of(result);

352

},

353

});

354

355

// Loadable transformation chain

356

const processedDataSelector = selector({

357

key: 'processedDataSelector',

358

get: ({get}) => {

359

const dataLoadable = get(noWait(rawDataState));

360

361

// Chain transformations on the loadable

362

return dataLoadable

363

.map(data => data.filter(item => item.active))

364

.map(data => data.map(item => ({

365

...item,

366

displayName: item.name.toUpperCase(),

367

})))

368

.map(data => data.sort((a, b) => a.priority - b.priority));

369

},

370

});

371

372

// Component with sophisticated loadable handling

373

function SmartDataComponent() {

374

const dataLoadable = useRecoilValue(noWait(partialDataSelector));

375

376

if (dataLoadable.state === 'loading') {

377

return <div>Loading essential data...</div>;

378

}

379

380

if (dataLoadable.state === 'hasError') {

381

return <div>Failed to load: {dataLoadable.contents.message}</div>;

382

}

383

384

const data = dataLoadable.contents;

385

386

return (

387

<div>

388

<div>Essential: {JSON.stringify(data.essential)}</div>

389

390

{data.important ? (

391

<div>Important: {JSON.stringify(data.important)}</div>

392

) : (

393

<div>Loading important data...</div>

394

)}

395

396

{data.optional ? (

397

<div>Optional: {JSON.stringify(data.optional)}</div>

398

) : (

399

<div>Optional data unavailable</div>

400

)}

401

402

<div>Status: {data.status}</div>

403

</div>

404

);

405

}

406

```

407

408

## Error Handling Patterns

409

410

**Graceful Degradation:**

411

- Use loadables to provide partial functionality when some data fails

412

- Implement fallback chains for resilient data loading

413

- Show appropriate loading states while preserving usability

414

415

**Error Recovery:**

416

- Transform error loadables into default values where appropriate

417

- Implement retry mechanisms using loadable state information

418

- Provide user-friendly error messages with recovery options