@deslop/coding-standards 0.1.873 → 0.1.885
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/install.js +1 -1
- package/dist/oxlint.d.ts +34 -2
- package/dist/oxlint.js +58 -0
- package/package.json +1 -1
- package/skills/engineering/SKILL.md +18 -29
package/dist/install.js
CHANGED
|
@@ -24685,7 +24685,7 @@ const runWith = (command, config) => {
|
|
|
24685
24685
|
};
|
|
24686
24686
|
//#endregion
|
|
24687
24687
|
//#region package.json
|
|
24688
|
-
var version = "0.1.
|
|
24688
|
+
var version = "0.1.885";
|
|
24689
24689
|
//#endregion
|
|
24690
24690
|
//#region src/install.ts
|
|
24691
24691
|
const cli = pipe(make("deslop-coding-standards", {}, fnUntraced(function* () {
|
package/dist/oxlint.d.ts
CHANGED
|
@@ -4020,8 +4020,10 @@ export declare const oxlint: {
|
|
|
4020
4020
|
'shadcn/no-restyle'?: undefined;
|
|
4021
4021
|
'shadcn/require-static-classes'?: undefined;
|
|
4022
4022
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4023
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4023
4024
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4024
4025
|
'react/rules-of-hooks'?: undefined;
|
|
4026
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4025
4027
|
'effecttsgo/async-function'?: undefined;
|
|
4026
4028
|
'typescript/require-await'?: undefined;
|
|
4027
4029
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4034,8 +4036,10 @@ export declare const oxlint: {
|
|
|
4034
4036
|
'shadcn/no-restyle': "off";
|
|
4035
4037
|
'shadcn/require-static-classes': "off";
|
|
4036
4038
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4039
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4037
4040
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4038
4041
|
'react/rules-of-hooks'?: undefined;
|
|
4042
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4039
4043
|
'effecttsgo/async-function'?: undefined;
|
|
4040
4044
|
'typescript/require-await'?: undefined;
|
|
4041
4045
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4048,8 +4052,10 @@ export declare const oxlint: {
|
|
|
4048
4052
|
'shadcn/no-restyle'?: undefined;
|
|
4049
4053
|
'shadcn/require-static-classes'?: undefined;
|
|
4050
4054
|
'@deslop/coding-standards/no-destructured-parameter': "off";
|
|
4055
|
+
'@deslop/coding-standards/no-native-control': "off";
|
|
4051
4056
|
'@deslop/coding-standards/no-renamed-import': "off";
|
|
4052
4057
|
'react/rules-of-hooks'?: undefined;
|
|
4058
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4053
4059
|
'effecttsgo/async-function'?: undefined;
|
|
4054
4060
|
'typescript/require-await'?: undefined;
|
|
4055
4061
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4062,8 +4068,10 @@ export declare const oxlint: {
|
|
|
4062
4068
|
'shadcn/no-restyle'?: undefined;
|
|
4063
4069
|
'shadcn/require-static-classes'?: undefined;
|
|
4064
4070
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4071
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4065
4072
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4066
4073
|
'react/rules-of-hooks': "off";
|
|
4074
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4067
4075
|
'effecttsgo/async-function'?: undefined;
|
|
4068
4076
|
'typescript/require-await'?: undefined;
|
|
4069
4077
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4076,8 +4084,26 @@ export declare const oxlint: {
|
|
|
4076
4084
|
'shadcn/no-restyle'?: undefined;
|
|
4077
4085
|
'shadcn/require-static-classes'?: undefined;
|
|
4078
4086
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4087
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4079
4088
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4080
4089
|
'react/rules-of-hooks'?: undefined;
|
|
4090
|
+
'@deslop/coding-standards/no-discarded-failure': "off";
|
|
4091
|
+
'effecttsgo/async-function'?: undefined;
|
|
4092
|
+
'typescript/require-await'?: undefined;
|
|
4093
|
+
'typescript/strict-void-return'?: undefined;
|
|
4094
|
+
'unicorn/no-null'?: undefined;
|
|
4095
|
+
};
|
|
4096
|
+
} | {
|
|
4097
|
+
files: string[];
|
|
4098
|
+
rules: {
|
|
4099
|
+
'import/no-default-export'?: undefined;
|
|
4100
|
+
'shadcn/no-restyle'?: undefined;
|
|
4101
|
+
'shadcn/require-static-classes'?: undefined;
|
|
4102
|
+
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4103
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4104
|
+
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4105
|
+
'react/rules-of-hooks'?: undefined;
|
|
4106
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4081
4107
|
'effecttsgo/async-function': "off";
|
|
4082
4108
|
'typescript/require-await': "error";
|
|
4083
4109
|
'typescript/strict-void-return': "off";
|
|
@@ -4090,12 +4116,14 @@ export declare const oxlint: {
|
|
|
4090
4116
|
'@deslop/coding-standards/no-constant-function': "error";
|
|
4091
4117
|
'@deslop/coding-standards/no-deep-pipe': "error";
|
|
4092
4118
|
'@deslop/coding-standards/no-destructured-parameter': "error";
|
|
4119
|
+
'@deslop/coding-standards/no-discarded-failure': "error";
|
|
4093
4120
|
'@deslop/coding-standards/no-double-nullish-check': "error";
|
|
4094
4121
|
'@deslop/coding-standards/no-effect-property-arrow': "error";
|
|
4095
4122
|
'@deslop/coding-standards/no-error-message-assertion': "error";
|
|
4096
4123
|
'@deslop/coding-standards/no-fake-ref-state': "error";
|
|
4097
4124
|
'@deslop/coding-standards/no-hand-written-guard': "error";
|
|
4098
4125
|
'@deslop/coding-standards/no-let': "error";
|
|
4126
|
+
'@deslop/coding-standards/no-native-control': "error";
|
|
4099
4127
|
'@deslop/coding-standards/no-native-emptiness-check': "error";
|
|
4100
4128
|
'@deslop/coding-standards/no-native-method-call': "error";
|
|
4101
4129
|
'@deslop/coding-standards/no-nullish-ternary': "error";
|
|
@@ -4308,11 +4336,15 @@ export declare const oxlint: {
|
|
|
4308
4336
|
}];
|
|
4309
4337
|
'import/no-relative-parent-imports': "error";
|
|
4310
4338
|
'no-restricted-imports': ["error", {
|
|
4311
|
-
paths: {
|
|
4339
|
+
paths: ({
|
|
4312
4340
|
importNames: string[];
|
|
4313
4341
|
message: string;
|
|
4314
4342
|
name: string;
|
|
4315
|
-
}
|
|
4343
|
+
} | {
|
|
4344
|
+
importNames?: undefined;
|
|
4345
|
+
message: string;
|
|
4346
|
+
name: string;
|
|
4347
|
+
})[];
|
|
4316
4348
|
patterns: {
|
|
4317
4349
|
message: string;
|
|
4318
4350
|
regex: string;
|
package/dist/oxlint.js
CHANGED
|
@@ -1021,6 +1021,27 @@ const noDestructuredParameter = defineRule({
|
|
|
1021
1021
|
meta: { type: "problem" }
|
|
1022
1022
|
});
|
|
1023
1023
|
//#endregion
|
|
1024
|
+
//#region src/rules/no-discarded-failure.ts
|
|
1025
|
+
const noDiscardedFailure = defineRule({
|
|
1026
|
+
create: (context) => ({ MemberExpression: (node) => {
|
|
1027
|
+
if (!importedMember({
|
|
1028
|
+
context,
|
|
1029
|
+
importedName: "Effect",
|
|
1030
|
+
node
|
|
1031
|
+
})) return;
|
|
1032
|
+
if (!exists(memberName(node), (name) => contains$1([
|
|
1033
|
+
"ignore",
|
|
1034
|
+
"orDie",
|
|
1035
|
+
"orDieWith"
|
|
1036
|
+
], name))) return;
|
|
1037
|
+
context.report({
|
|
1038
|
+
message: "Map the failure to the service's error; an expected failure is never ignored or turned into a defect.",
|
|
1039
|
+
node
|
|
1040
|
+
});
|
|
1041
|
+
} }),
|
|
1042
|
+
meta: { type: "problem" }
|
|
1043
|
+
});
|
|
1044
|
+
//#endregion
|
|
1024
1045
|
//#region src/rules/no-double-nullish-check.ts
|
|
1025
1046
|
function nullishOperand(node) {
|
|
1026
1047
|
if (node.type === "Literal" && node.value === null) return some("null");
|
|
@@ -1284,6 +1305,30 @@ const noLet = defineRule({
|
|
|
1284
1305
|
meta: { type: "problem" }
|
|
1285
1306
|
});
|
|
1286
1307
|
//#endregion
|
|
1308
|
+
//#region src/rules/no-native-control.ts
|
|
1309
|
+
const primitives = {
|
|
1310
|
+
button: "Button",
|
|
1311
|
+
input: "Input",
|
|
1312
|
+
select: "Select",
|
|
1313
|
+
textarea: "Textarea"
|
|
1314
|
+
};
|
|
1315
|
+
const controlStyle = /(?:^|:)(?:hover|focus|focus-visible|active):|(?:^|:)(?:rounded|shadow|ring)(?:-|$)|(?:^|:)p[blrtxy]?-(?!0$)|(?:^|:)bg-(?!transparent$)|(?:^|:)border(?:-[blrtxy])?$|(?:^|:)border-(?!0$|[blrtxy]-0$)/u;
|
|
1316
|
+
const noNativeControl = defineRule({
|
|
1317
|
+
create: (context) => ({ JSXOpeningElement: (node) => {
|
|
1318
|
+
if (node.name.type !== "JSXIdentifier") return;
|
|
1319
|
+
const name = node.name.name;
|
|
1320
|
+
map(get$2(primitives, name), (primitive) => {
|
|
1321
|
+
const styled = pipe(node.attributes, findFirst((attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "className"), flatMap((attribute) => attribute.type === "JSXAttribute" && attribute.value?.type === "Literal" ? fromNullishOr(attribute.value.value) : none()), exists((value) => some$1(split(value, /\s+/u), matches(controlStyle))));
|
|
1322
|
+
if (name !== "select" && !styled) return;
|
|
1323
|
+
context.report({
|
|
1324
|
+
message: `Use the ${primitive} UI primitive instead of a styled native control.`,
|
|
1325
|
+
node
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
} }),
|
|
1329
|
+
meta: { type: "problem" }
|
|
1330
|
+
});
|
|
1331
|
+
//#endregion
|
|
1287
1332
|
//#region src/rules/no-native-emptiness-check.ts
|
|
1288
1333
|
function emptinessPredicate(node) {
|
|
1289
1334
|
const comparesLength = node.left.type === "MemberExpression" && contains(memberName(node.left), "length") && node.right.type === "Literal" && node.right.value === 0;
|
|
@@ -2477,6 +2522,7 @@ const oxlint = defineConfig({
|
|
|
2477
2522
|
files: ["**/components/ui/**", "**/packages/ui/**"],
|
|
2478
2523
|
rules: {
|
|
2479
2524
|
"@deslop/coding-standards/no-destructured-parameter": "off",
|
|
2525
|
+
"@deslop/coding-standards/no-native-control": "off",
|
|
2480
2526
|
"@deslop/coding-standards/no-renamed-import": "off"
|
|
2481
2527
|
}
|
|
2482
2528
|
},
|
|
@@ -2484,6 +2530,10 @@ const oxlint = defineConfig({
|
|
|
2484
2530
|
files: ["**/*.ts"],
|
|
2485
2531
|
rules: { "react/rules-of-hooks": "off" }
|
|
2486
2532
|
},
|
|
2533
|
+
{
|
|
2534
|
+
files: ["**/*.test.ts", "**/*.test.tsx"],
|
|
2535
|
+
rules: { "@deslop/coding-standards/no-discarded-failure": "off" }
|
|
2536
|
+
},
|
|
2487
2537
|
{
|
|
2488
2538
|
files: ["**/*.tsx"],
|
|
2489
2539
|
rules: {
|
|
@@ -2508,12 +2558,14 @@ const oxlint = defineConfig({
|
|
|
2508
2558
|
"@deslop/coding-standards/no-constant-function": "error",
|
|
2509
2559
|
"@deslop/coding-standards/no-deep-pipe": "error",
|
|
2510
2560
|
"@deslop/coding-standards/no-destructured-parameter": "error",
|
|
2561
|
+
"@deslop/coding-standards/no-discarded-failure": "error",
|
|
2511
2562
|
"@deslop/coding-standards/no-double-nullish-check": "error",
|
|
2512
2563
|
"@deslop/coding-standards/no-effect-property-arrow": "error",
|
|
2513
2564
|
"@deslop/coding-standards/no-error-message-assertion": "error",
|
|
2514
2565
|
"@deslop/coding-standards/no-fake-ref-state": "error",
|
|
2515
2566
|
"@deslop/coding-standards/no-hand-written-guard": "error",
|
|
2516
2567
|
"@deslop/coding-standards/no-let": "error",
|
|
2568
|
+
"@deslop/coding-standards/no-native-control": "error",
|
|
2517
2569
|
"@deslop/coding-standards/no-native-emptiness-check": "error",
|
|
2518
2570
|
"@deslop/coding-standards/no-native-method-call": "error",
|
|
2519
2571
|
"@deslop/coding-standards/no-nullish-ternary": "error",
|
|
@@ -2730,6 +2782,10 @@ const oxlint = defineConfig({
|
|
|
2730
2782
|
message: reactLegacyMessage,
|
|
2731
2783
|
name: "react"
|
|
2732
2784
|
},
|
|
2785
|
+
{
|
|
2786
|
+
message: "Read and write data through Effect Atom.",
|
|
2787
|
+
name: "@tanstack/react-query"
|
|
2788
|
+
},
|
|
2733
2789
|
{
|
|
2734
2790
|
importNames: ["vi"],
|
|
2735
2791
|
message: "A Layer is the seam; never use vi.",
|
|
@@ -3206,12 +3262,14 @@ var oxlint_default = definePlugin({
|
|
|
3206
3262
|
"no-constant-function": noConstantFunction,
|
|
3207
3263
|
"no-deep-pipe": noDeepPipe,
|
|
3208
3264
|
"no-destructured-parameter": noDestructuredParameter,
|
|
3265
|
+
"no-discarded-failure": noDiscardedFailure,
|
|
3209
3266
|
"no-double-nullish-check": noDoubleNullishCheck,
|
|
3210
3267
|
"no-effect-property-arrow": noEffectPropertyArrow,
|
|
3211
3268
|
"no-error-message-assertion": noErrorMessageAssertion,
|
|
3212
3269
|
"no-fake-ref-state": noFakeRefState,
|
|
3213
3270
|
"no-hand-written-guard": noHandWrittenGuard,
|
|
3214
3271
|
"no-let": noLet,
|
|
3272
|
+
"no-native-control": noNativeControl,
|
|
3215
3273
|
"no-native-emptiness-check": noNativeEmptinessCheck,
|
|
3216
3274
|
"no-native-method-call": noNativeMethodCall,
|
|
3217
3275
|
"no-nullish-ternary": noNullishTernary,
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ name: engineering
|
|
|
3
3
|
description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product/test code, including prototypes.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
**Scope.** Apply these rules and repository-specific CODING_STANDARDS.md.
|
|
6
|
+
**Scope.** Apply these rules and repository-specific CODING_STANDARDS.md. Write each line right the first time: the examples exist so lint and review find nothing, and lint is only the fallback.
|
|
7
7
|
|
|
8
8
|
**Dependency APIs.** Match the manifest and lockfile versions. Before adding an API wrapper or workaround, read the upstream source per workflow's research procedure. Library APIs do not define coding policy.
|
|
9
9
|
|
|
@@ -16,16 +16,16 @@ description: 'TypeScript and Effect code conventions paired with shared lint. Us
|
|
|
16
16
|
**Experiments.** Write with these idioms; reserve the full refactor/cleanup pass for retained delivery. Keep disposable source isolated and shared rules intact.
|
|
17
17
|
|
|
18
18
|
- **Reuse first.** Check existing code, installed modules and platform capabilities before adding code. Implement only the missing behavior, without dropping required behavior, safety or accessibility. Workflow owns the scope. A refactor or cleanup request applies every rule to every line of the owned files.
|
|
19
|
-
- **Inward layers.** Domain and service code never import HTTP, RPC, or other transport types.
|
|
19
|
+
- **Inward layers.** Domain and service code never import HTTP, RPC, or other transport types, and domain schemas carry no transport annotation such as an HTTP status.
|
|
20
20
|
- **Building blocks.** For a consequential new interface, start with a realistic caller operation and its ordering, failure and lifetime obligations; hide useful complexity, not forwarded names. Offer services, Layers and functions the caller composes like any Effect module, never a bundle such as `serve(app)`. No mandatory architecture document or alternatives round.
|
|
21
21
|
- **No machinery.** Add hooks, guards or generators only for a required contract or a recurring defect.
|
|
22
22
|
- **Behavior kept.** A refactor keeps observable behavior at every consumer. Report accepted differences only when they exist: a change no consumer observes, or one a rule here causes, such as an error keeping its cause. Printed diagnostics are not a contract. Any other behavior change follows workflow's scope.
|
|
23
23
|
- **Reachable bugs.** Fix a bug that real input reaches when it lies inside workflow's scope, and check every consumer of the changed output. For a hard bug, use one reproduction and one prediction per probe. Remove edits based on disproven hypotheses, then rerun the original scenario. Delete handling only for input the contract excludes; keep and report code that looks intentional.
|
|
24
|
-
- **Performance.** Optimize only when the request or a required contract needs it
|
|
24
|
+
- **Performance.** Optimize only when the request or a required contract needs it. Measure one real action on realistic input before and after the change, at the layer that owns the symptom: spans for backend calls, React commits for rerenders, a browser profile for main-thread work. Report run count and variation; a difference within noise is unproved.
|
|
25
25
|
- **Type safety.** A fix never weakens types to make a symptom go away: no widened or erased type, cast, dropped generic, or loosened exported type.
|
|
26
26
|
- **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
|
|
27
27
|
|
|
28
|
-
**Happy path.** Failures flow through the error channel: no catch, retry, fallback, backup, or defensive check unless the request or an existing contract needs it.
|
|
28
|
+
**Happy path.** Failures flow through the error channel: no catch, retry, fallback, backup, or defensive check unless the request or an existing contract needs it. Map an expected failure to the service's error; never `Effect.ignore` it or turn it into a defect with `Effect.orDie`.
|
|
29
29
|
|
|
30
30
|
**Trusted data.** Validate and transform once at the boundary with Effect Schema. Carry narrowed values forward. Never re-check what the schema, the declared type, an earlier filter or every caller guarantees.
|
|
31
31
|
|
|
@@ -72,7 +72,7 @@ _input => onSubmit(_input.value) // or input2: an outer input is shadowed
|
|
|
72
72
|
|
|
73
73
|
**Nearest sibling.** Extend the nearest implementation; match permissions, errors, refresh and tests. Reuse its helpers and change only affected state.
|
|
74
74
|
|
|
75
|
-
**Once.** Extract repeated logic once; one fact has one owner.
|
|
75
|
+
**Once.** Extract repeated logic once; one fact has one owner. A domain enum, such as a status, has one schema that every other schema reuses, and a cache key or atom family key carries the full identity of what it caches.
|
|
76
76
|
|
|
77
77
|
```ts
|
|
78
78
|
// good
|
|
@@ -96,7 +96,7 @@ pipe(
|
|
|
96
96
|
|
|
97
97
|
**Present needs.** No speculative option, abstraction, layer, export, file or check. Configure only what callers vary.
|
|
98
98
|
|
|
99
|
-
**Dead code.** Every superseded or unused file, export, type, branch, config, validation, doc, test, and dependency goes in the same change.
|
|
99
|
+
**Dead code.** Every superseded or unused file, export, type, branch, config, validation, doc, test, and dependency goes in the same change. Docs, guides and generated bundles that describe changed behavior change in the same commit.
|
|
100
100
|
|
|
101
101
|
## Lint pairs
|
|
102
102
|
|
|
@@ -149,22 +149,7 @@ Array.appendAll(
|
|
|
149
149
|
)
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
**Choices.** `Match` for a union, never `switch`.
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
// good
|
|
156
|
-
pipe(
|
|
157
|
-
Match.value(usage.kind),
|
|
158
|
-
Match.when('input', () => 'in'),
|
|
159
|
-
Match.when('output', () => 'out'),
|
|
160
|
-
Match.exhaustive
|
|
161
|
-
)
|
|
162
|
-
// bad
|
|
163
|
-
switch (usage.kind) {
|
|
164
|
-
case 'input':
|
|
165
|
-
return 'in'
|
|
166
|
-
}
|
|
167
|
-
```
|
|
152
|
+
**Choices.** `Match` with `Match.exhaustive` for a union, never `switch`.
|
|
168
153
|
|
|
169
154
|
**Options.** Use Option operators, never .value. Use Option where it composes; keep plain optional values otherwise. This is our convention, not a claim narrowing is unsafe.
|
|
170
155
|
|
|
@@ -573,7 +558,7 @@ const handle = useCallback(() => setOpen(true), [])
|
|
|
573
558
|
const state = useState(false)
|
|
574
559
|
```
|
|
575
560
|
|
|
576
|
-
**Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves that code to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)`, and
|
|
561
|
+
**Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves that code to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)`, and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them, so never pass them to `Atom.fn`.
|
|
577
562
|
|
|
578
563
|
```tsx
|
|
579
564
|
// good
|
|
@@ -588,18 +573,22 @@ onClick={() => Effect.runPromise(saveApp(draft))}
|
|
|
588
573
|
useAtomValue(Atom.make(loadApp(appId))) // a new atom on every render
|
|
589
574
|
```
|
|
590
575
|
|
|
591
|
-
**Outcomes.**
|
|
576
|
+
**Outcomes.** Read data with `useAtomSuspense`. The route or section that owns a screen wraps it in one `Suspense` boundary for loading and one error boundary that shows the failure with a retry. Components never branch on loading or failure, and never turn a failure into an empty value. Take a write's pending and failure state from its atom's result, never from copied React state.
|
|
592
577
|
|
|
593
578
|
```tsx
|
|
594
579
|
// good
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
580
|
+
function AppFrame(props: {appId: AppId}) {
|
|
581
|
+
return <Frame app={useAtomSuspense(appAtom(props.appId)).value} />
|
|
582
|
+
}
|
|
583
|
+
<ErrorBoundary fallback={<LoadError onRetry={refresh} />}>
|
|
584
|
+
<Suspense fallback={<Spinner />}>
|
|
585
|
+
<AppFrame appId={appId} />
|
|
586
|
+
</Suspense>
|
|
587
|
+
</ErrorBoundary>
|
|
600
588
|
<Button disabled={saveResult.waiting} onClick={() => save(draft)}>Save</Button>
|
|
601
589
|
// bad
|
|
602
590
|
app.data ? <AppFrame app={app.data} /> : <Spinner /> // a failed load spins forever
|
|
591
|
+
const members = query.data ?? [] // a failed load looks like no members
|
|
603
592
|
const [pending, setPending] = useState(false) // copies the write atom's waiting state
|
|
604
593
|
```
|
|
605
594
|
|