@canonical/code-standards 0.1.0

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/data/react.ttl ADDED
@@ -0,0 +1,752 @@
1
+ @prefix cs: <http://pragma.canonical.com/codestandards#> .
2
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
3
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
4
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
5
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
6
+
7
+ # React Category
8
+ cs:ReactCategory a cs:Category ;
9
+ rdfs:label "React"@en ;
10
+ rdfs:comment "Standards specific to React development"@en ;
11
+ cs:slug "react" .
12
+
13
+ # Component TSDoc Standard
14
+ cs:ComponentTSDoc a cs:CodeStandard ;
15
+ cs:name "react/component/tsdoc" ;
16
+ cs:hasCategory cs:ReactCategory ;
17
+ cs:description """Component TSDoc documentation must use the description from the design system ontology (DSL). The TSDoc should NOT include @example blocks since stories serve as the examples. The description should be copied verbatim from the DSL, maintaining the original wording and meaning.""" ;
18
+ cs:dos """
19
+ (Do) Use the description from the DSL ontology verbatim.
20
+ ```typescript
21
+ /**
22
+ * The label component is a compact, non-interactive visual element used to
23
+ * categorize content or indicate a status. Its primary role is metadata
24
+ * visualization. While it has similar visual properties to the Chip, it is
25
+ * purely informational and does not trigger actions or allow for removal.
26
+ *
27
+ * @implements dso:global.component.label
28
+ */
29
+ const Label = ({ children, criticality, className, ...props }: LabelProps) => (
30
+ <span className={[componentCssClassName, criticality, className].filter(Boolean).join(" ")} {...props}>
31
+ {children}
32
+ </span>
33
+ );
34
+ ```
35
+ """ ;
36
+ cs:donts """
37
+ (Don't) Write custom descriptions that deviate from the DSL.
38
+ ```typescript
39
+ // Bad: Custom title and paraphrased description
40
+ /**
41
+ * Label component
42
+ *
43
+ * A compact visual element for status indication.
44
+ */
45
+ ```
46
+
47
+ (Don't) Include @example blocks - stories fulfill this role.
48
+ ```typescript
49
+ // Bad: Examples belong in stories, not TSDoc
50
+ /**
51
+ * The label component is a compact...
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * <Label>Default</Label>
56
+ * <Label criticality="warning">Warning</Label>
57
+ * ```
58
+ */
59
+ ```
60
+
61
+ (Don't) Omit the @implements tag that links to the DSL.
62
+ ```typescript
63
+ // Bad: Missing @implements tag
64
+ /**
65
+ * The label component is a compact...
66
+ */
67
+ ```
68
+ """ .
69
+
70
+ # Component Folder Structure Standard
71
+ cs:ComponentFolderStructure a cs:CodeStandard ;
72
+ cs:name "react/component/structure/folder" ;
73
+ cs:extends cs:ComponentFolderStructure ;
74
+ cs:hasCategory cs:ReactCategory ;
75
+ cs:description "Each component must reside in its own folder, which contains all related files: component implementation, tests, type definitions, and optionally stories and styles. Some components may be styleless or may not have stories; in these cases, styles.css and [MyComponent].stories.tsx are optional. Context providers follow a different structure (see react/component/structure/context)." ;
76
+ cs:dos """
77
+ (Do) Place all component-related files within a single folder named after the component.
78
+ ```bash
79
+ [MyComponent]/
80
+ ├── [MyComponent].tsx
81
+ ├── [MyComponent].stories.tsx
82
+ ├── [MyComponent].test.tsx
83
+ ├── index.ts
84
+ ├── styles.css
85
+ └── types.ts
86
+ ```
87
+ """ ;
88
+ cs:donts """
89
+ (Don't) Scatter component files across different parts of the application.
90
+ ```bash
91
+ # Bad: Files are not co-located
92
+ components/
93
+ ├── [MyComponent].tsx
94
+ stories/
95
+ └── [MyComponent].stories.tsx
96
+ styles/
97
+ └── [MyComponent].css
98
+ ```
99
+ """ .
100
+
101
+ # Barrel Exports Standard
102
+ cs:BarrelExports a cs:CodeStandard ;
103
+ cs:name "react/component/barrel-exports" ;
104
+ cs:hasCategory cs:ReactCategory ;
105
+ cs:description "The index.ts file must be a complete barrel export for the component folder, re-exporting all public APIs." ;
106
+ cs:dos """
107
+ (Do) Create an index.ts file that re-exports all public APIs from the component folder.
108
+ ```typescript
109
+ // index.ts
110
+ export { default as [MyComponent] } from './[MyComponent].js';
111
+ export type * from './types.js';
112
+ // If you have multiple components:
113
+ export { default as SubComponent } from './SubComponent.js';
114
+ ```
115
+ """ ;
116
+ cs:donts """
117
+ (Don't) Omit the index.ts file or fail to re-export all public APIs.
118
+ ```typescript
119
+ // Bad: index.ts only exports default, omits types and named exports
120
+ export { default } from './[MyComponent].js';
121
+ ```
122
+
123
+ (Don't) Use `export * from './types.js'` as it allows value exports, which is not expected for types files.
124
+ ```typescript
125
+ // Bad: Using export * from './types.js' allows value exports, which is not allowed
126
+ export * from './types.js';
127
+ ```
128
+ """ .
129
+
130
+ # Component File Naming Standard
131
+ cs:ComponentFileNaming a cs:CodeStandard ;
132
+ cs:name "react/component/file-naming" ;
133
+ cs:hasCategory cs:ReactCategory ;
134
+ cs:description "Component folder and file naming is based on scope. Component-specific files (implementation, stories, tests) must be prefixed with the component's name (e.g., `MyComponent.tsx`, `MyComponent.stories.tsx`). Domain-level files that serve the entire folder (e.g., `Context.tsx`, `styles.css`, `types.ts`) should use generic, descriptive names, as the folder already provides the domain context." ;
135
+ cs:dos """
136
+ (Do) Prefix component-specific files and use generic names for domain-level files.
137
+ ```
138
+ [MyComponent]/
139
+ ├── [MyComponent].tsx # Component-specific
140
+ ├── [MyComponent].stories.tsx # Component-specific
141
+ ├── [MyComponent].test.tsx # Component-specific
142
+ ├── Context.tsx # Domain-level
143
+ ├── types.ts # Domain-level
144
+ └── styles.css # Domain-level
145
+ ```
146
+ """ ;
147
+ cs:donts """
148
+ (Don't) Add redundant prefixes to domain-level files.
149
+ ```
150
+ [MyComponent]/
151
+ ├── [MyComponent].Context.tsx # Bad: Redundant prefix
152
+ ├── [MyComponent].types.ts # Bad: Redundant prefix
153
+ └── [MyComponent].styles.css # Bad: Redundant prefix
154
+ ```
155
+ """ .
156
+
157
+ # Component Props Standard
158
+ cs:ComponentProps a cs:CodeStandard ;
159
+ cs:name "react/component/props" ;
160
+ cs:hasCategory cs:ReactCategory ;
161
+ cs:description """Component props must:
162
+ - Be documented with TSDoc comments
163
+ - Be destructured when used in markup
164
+ - Be spread to the root element when unused
165
+ - Follow type-specific patterns based on what the component renders""" ;
166
+ cs:dos """
167
+ (Do) Document props with TSDoc comments and use proper destructuring and spreading.
168
+ ```typescript
169
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
170
+ /** The button's text content */
171
+ label: string;
172
+ /** Optional icon to display before the label */
173
+ icon?: React.ReactNode;
174
+ /** Visual emphasis of the button */
175
+ emphasis?: ModifierFamily<"emphasis">;
176
+ }
177
+
178
+ const Button = ({
179
+ label,
180
+ icon,
181
+ emphasis,
182
+ className,
183
+ ...props // Spread unused HTML button props
184
+ }: ButtonProps) => (
185
+ <button
186
+ className={[componentCssClassName, emphasis, className].filter(Boolean).join(" ")}
187
+ {...props}
188
+ >
189
+ {icon}
190
+ <span>{label}</span>
191
+ </button>
192
+ );
193
+ ```
194
+ """ ;
195
+ cs:donts """
196
+ (Don't) Mix explicit and spread props or destructure props unnecessarily.
197
+ ```typescript
198
+ // Bad: Mixing explicit props with spread
199
+ const Button = (props: ButtonProps) => (
200
+ <button
201
+ className={props.className}
202
+ onClick={props.onClick} // Should be in ...props
203
+ {...props} // Now duplicates onClick
204
+ >
205
+ {props.label}
206
+ </button>
207
+ );
208
+
209
+ // Bad: Unnecessarily destructuring HTML props
210
+ const Button = ({
211
+ label,
212
+ className,
213
+ onClick, // Should be in ...props
214
+ onFocus, // Should be in ...props
215
+ disabled, // Should be in ...props
216
+ ...props
217
+ }: ButtonProps) => (
218
+ <button
219
+ className={className}
220
+ onClick={onClick} // Explicit when it could be spread
221
+ onFocus={onFocus} // Explicit when it could be spread
222
+ disabled={disabled} // Explicit when it could be spread
223
+ {...props}
224
+ >
225
+ {label}
226
+ </button>
227
+ );
228
+ ```
229
+ """ .
230
+
231
+ # HTML Rendering Components Props Standard
232
+ cs:HTMLRenderingProps a cs:CodeStandard ;
233
+ cs:name "react/component/props/html-rendering" ;
234
+ cs:hasCategory cs:ReactCategory ;
235
+ cs:extends cs:ComponentProps ;
236
+ cs:description "Components that render HTML markup must extend the base HTML element props interface to enable passing native properties through spreading." ;
237
+ cs:dos """
238
+ (Do) Extend the appropriate React HTML props interface and add component-specific props.
239
+ ```typescript
240
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
241
+ /** The button label */
242
+ label: string;
243
+ }
244
+ ```
245
+ """ ;
246
+ cs:donts """
247
+ (Don't) Manually redefine standard HTML attributes that are already available through the base interface.
248
+ ```typescript
249
+ export interface ButtonProps {
250
+ /** The button label */
251
+ label: string;
252
+ onClick?: () => void; // Bad: Duplicates HTML button props
253
+ disabled?: boolean; // Bad: Duplicates HTML button props
254
+ }
255
+ ```
256
+ """ .
257
+
258
+ # Component Naming Standard
259
+ cs:ComponentNaming a cs:CodeStandard ;
260
+ cs:name "react/component/naming" ;
261
+ cs:hasCategory cs:ReactCategory ;
262
+ cs:description "Components must use PascalCase naming and be descriptive of their purpose." ;
263
+ cs:dos """
264
+ (Do) Use PascalCase and descriptive names for components:
265
+ UserProfile
266
+ NavigationBar
267
+ SearchResultList
268
+ """ ;
269
+ cs:donts """
270
+ (Don't) Use non-PascalCase or unclear names:
271
+ userProfile
272
+ navigation_bar
273
+ searchresultlist
274
+ """ .
275
+
276
+ # Hook Naming Standard
277
+ cs:HookNaming a cs:CodeStandard ;
278
+ cs:name "react/hooks/naming" ;
279
+ cs:hasCategory cs:ReactCategory ;
280
+ cs:description "The hook name must start with 'use' and clearly describe its purpose." ;
281
+ cs:dos """
282
+ (Do) Name custom hooks so the hook name starts with 'use' and is descriptive:
283
+ ```typescript
284
+ useWindowSize()
285
+ useAuthentication()
286
+ useFormValidation()
287
+ ```
288
+ """ ;
289
+ cs:donts """
290
+ (Don't) Name hooks without the 'use' prefix at the start of the hook name:
291
+ ```typescript
292
+ windowSize()
293
+ getAuth()
294
+ formValidation()
295
+ ```
296
+ """ .
297
+
298
+ # Component Dependencies Standard
299
+ cs:ComponentDependencies a cs:CodeStandard ;
300
+ cs:name "react/component/dependencies" ;
301
+ cs:hasCategory cs:ReactCategory ;
302
+ cs:description """Component dependencies must follow a strict unidirectional flow:
303
+ - Subcomponents must be in a `common/` folder within the parent component's directory
304
+ - Dependencies can flow downwards (parent to subcomponent) or sideways (between siblings)
305
+ - Dependencies must not flow upwards (subcomponent to parent)""" ;
306
+ cs:dos """
307
+ (Do) Place subcomponents in a `common/` folder inside the parent component directory.
308
+ ```
309
+ Card/
310
+ ├── Card.tsx
311
+ ├── common/
312
+ │ ├── Header/
313
+ │ │ └── Header.tsx
314
+ │ ├── Footer/
315
+ │ │ └── Footer.tsx
316
+ │ └── utils/
317
+ │ └── helpers.ts
318
+ └── index.ts
319
+ ```
320
+
321
+ (Do) Allow subcomponents to depend on siblings or shared utilities within the same component scope.
322
+ ```typescript
323
+ // Header.tsx can import from utils/
324
+ import { helper } from '../utils/helpers.js';
325
+
326
+ // Footer.tsx can import from Header.tsx
327
+ import Header from '../Header.js';
328
+ ```
329
+ """ ;
330
+ cs:donts """
331
+ (Don't) Create dependencies that flow upwards from a subcomponent to its parent.
332
+ ```typescript
333
+ // Bad: Header.tsx in Card/common/ should not import from Card.tsx
334
+ import Card from '../../Card.js';
335
+ ```
336
+
337
+ (Don't) Allow external components to depend on the internal structure of another component.
338
+ ```typescript
339
+ // Bad: AnotherComponent should not import from Card's internal common folder
340
+ import Header from '../Card/common/Header.js';
341
+ ```
342
+ """ .
343
+
344
+ # Subcomponents Export and Consumption API Standard
345
+ cs:SubcomponentsExportAPI a cs:CodeStandard ;
346
+ cs:name "react/component/subcomponent-export-api" ;
347
+ cs:hasCategory cs:ReactCategory ;
348
+ cs:description """Subcomponents are **public** if they are intended to be directly used by the consumer of the parent component to construct a UI.
349
+
350
+ Subcomponents are **private** if they are internal implementation details of the parent component, and are not intended for direct use by the consumer.
351
+
352
+ Public subcomponents must be:
353
+ - Exported by attaching them to the parent component using dot notation.
354
+ - Named semantically.
355
+ - Kept to a single level of nesting.
356
+
357
+ Private subcomponents must remain internal to the component's implementation and not be exported by any file.""" ;
358
+ cs:dos """
359
+ (Do) Export public subcomponents by attaching them to the parent component using dot notation:
360
+ ```typescript
361
+ const Item = (props: ItemProps) => { /* ... */ };
362
+ const Accordion = (props: AccordionProps) => { /* ... */ };
363
+ Accordion.Item = Item;
364
+ export default Accordion;
365
+ ```
366
+
367
+ (Do) Use semantic, self-descriptive names for subcomponents:
368
+ ```typescript
369
+ Accordion.Item
370
+ Card.Header
371
+ Card.Footer
372
+ ```
373
+
374
+ (Do) Keep subcomponent nesting to a single level:
375
+ ```typescript
376
+ <Card>
377
+ <Card.Header />
378
+ <Card.Footer />
379
+ </Card>
380
+ ```
381
+
382
+ """ ;
383
+ cs:donts """
384
+ (Don't) Repeat the parent component name in subcomponent names:
385
+ ```typescript
386
+ Card.CardHeader = Header; // Bad: Redundant 'Card' prefix
387
+ ```
388
+
389
+ (Don't) Map a subcomponent to a different name (renaming):
390
+ ```typescript
391
+ Card.Top = Header; // Bad: Mapping 'Header' to 'Top' is not allowed
392
+ ```
393
+
394
+ (Don't) Use non-semantic or unclear subcomponent names:
395
+ ```
396
+ Card/
397
+ └── common/
398
+ ├── Part/ # Bad: Too vague, not semantic - what part?
399
+ ├── Element/ # Bad: Too vague, not semantic - what element?
400
+ ```
401
+
402
+ (Don't) Nest subcomponents more than one level deep:
403
+ ```typescript
404
+ <Card>
405
+ <Card.Header>
406
+ <Card.Header.Title />
407
+ </Card.Header>
408
+ </Card>
409
+ ```
410
+
411
+ (Don't) Export private subcomponents that are not intended for public use.
412
+ ```typescript
413
+ // Bad: Exporting internal-only subcomponents
414
+ export { InternalHelper };
415
+ ```
416
+ """ .
417
+
418
+ # CSS ClassName Construction Standard
419
+ cs:ClassNameConstruction a cs:CodeStandard ;
420
+ cs:name "react/component/class-name-construction" ;
421
+ cs:hasCategory cs:ReactCategory ;
422
+ cs:description """Component CSS class names must be constructed following a specific pattern:
423
+
424
+ 1. Base Class Constant: A `componentCssClassName` constant must be defined at the top of the component file. This constant holds the component's base class name, including the global `ds` scope (e.g., `"ds button"`).
425
+ 2. Array Construction: The `className` string must be built from an array of classes.
426
+ 3. Class Order: The classes in the array must be ordered from least to most specific to ensure a predictable CSS cascade:
427
+ a. Base Class: The `componentCssClassName` constant.
428
+ b. Modifier Classes: Classes derived from component props (e.g., `emphasis`, `severity`).
429
+ c. Consumer Classes: The `className` prop passed by the consumer.
430
+ 4. Filtering and Joining: The array must be processed with `.filter(Boolean).join(" ")` to remove any falsy values (e.g., undefined props, expressions that evaluate to false) and create the final space-delimited string.""" ;
431
+ cs:dos """
432
+ (Do) Follow the complete pattern for class name construction.
433
+ ```tsx
434
+ const componentCssClassName = "ds badge";
435
+
436
+ const Badge = ({
437
+ value,
438
+ className,
439
+ severity,
440
+ ...props
441
+ }: BadgeProps): React.ReactElement => {
442
+ return (
443
+ <span
444
+ className={[componentCssClassName, severity, className]
445
+ .filter(Boolean)
446
+ .join(" ")}
447
+ {...props}
448
+ >
449
+ {value}
450
+ </span>
451
+ );
452
+ };
453
+ ```
454
+ """ ;
455
+ cs:donts """
456
+ (Don't) Hardcode the base class name inside the JSX.
457
+ ```tsx
458
+ // Bad: Base class "ds badge" is hardcoded.
459
+ <span className={["ds badge", severity, className].filter(Boolean).join(" ")}>
460
+ ```
461
+
462
+ (Don't) Place the consumer `className` prop before other classes.
463
+ ```tsx
464
+ // Bad: Consumer class is first
465
+ <span className={[className, componentCssClassName, severity].filter(Boolean).join(" ")}>
466
+ ```
467
+
468
+ (Don't) Use string concatenation or template literals to add class names.
469
+ ```tsx
470
+ // Bad: Harder to read and maintain, vulnerable to inconsistent formatting
471
+ <span className={`${componentCssClassName} ${severity} ${className}`}>
472
+ ```
473
+ """ .
474
+
475
+ # Wrapper Component Props Standard
476
+ cs:WrapperComponentProps a cs:CodeStandard ;
477
+ cs:name "react/component/props/wrapper" ;
478
+ cs:hasCategory cs:ReactCategory ;
479
+ cs:extends cs:ComponentProps ;
480
+ cs:description "Wrapper components must use namespaced props for inner components and accept unscoped props for the wrapper element." ;
481
+ cs:dos """
482
+ (Do) Use namespaced props for inner components and unscoped props for the wrapper element.
483
+ ```tsx
484
+ interface ThumbnailSectionProps extends SectionProps {
485
+ /** Props for the thumbnail image */
486
+ imageProps: Omit<React.ImgHTMLAttributes<HTMLImageElement>, "alt"> & {
487
+ alt: string;
488
+ };
489
+ }
490
+
491
+ const ThumbnailSection = ({
492
+ imageProps,
493
+ className,
494
+ ...props
495
+ }: ThumbnailSectionProps) => (
496
+ <Section className={[componentCssClassName, className].filter(Boolean).join(" ")} {...props}>
497
+ <img {...imageProps} />
498
+ </Section>
499
+ );
500
+ ```
501
+ """ ;
502
+ cs:donts """
503
+ (Don't) Mix prop scopes between wrapper and inner components.
504
+ ```tsx
505
+ interface ThumbnailSectionProps {
506
+ src: string; // Bad: Unscoped image props
507
+ alt: string; // Bad: Unscoped image props
508
+ width: number; // Bad: Ambiguous - for image or section?
509
+ }
510
+
511
+ const ThumbnailSection = ({ src, alt, width, ...props }: ThumbnailSectionProps) => (
512
+ <Section {...props}>
513
+ <img src={src} alt={alt} width={width} />
514
+ </Section>
515
+ );
516
+ ```
517
+ """ .
518
+
519
+ # Custom Hooks Standard
520
+ cs:CustomHooks a cs:CodeStandard ;
521
+ cs:name "react/hooks/custom" ;
522
+ cs:hasCategory cs:ReactCategory ;
523
+ cs:description """Custom hooks must separate concerns at the domain level. All hooks within a ComponentDomain/hooks directory are considered within the domain scope and should focus on a single concern. Custom hooks must be used when:
524
+ - Logic needs to be shared between components
525
+ - Component logic becomes complex
526
+ - State management needs to be abstracted
527
+ - Side effects need to be encapsulated
528
+
529
+ All of the types for a domain level's hooks must be defined in the `hooks/types.ts` file of that folder.
530
+ Each hook must define a [HookName]Props and [HookName]Result type in `hooks/types.ts`.""" ;
531
+ cs:dos """
532
+ (Do) Create a custom hook that focuses on a single concern within the domain.
533
+ ```typescript
534
+ // [MyComponent]/hooks/useWindowFitment.ts
535
+ const useWindowFitment = ({
536
+ onBestPositionChange,
537
+ autoFit = false,
538
+ }: UseWindowFitmentProps): UseWindowFitmentResult => {
539
+ ```
540
+ (Do) Create hook types in `hooks/types.ts`.
541
+ ```typescript
542
+ // [MyComponent]/hooks/types.ts
543
+ export interface UseWindowFitmentProps {
544
+ /**
545
+ * Whether the popup should automatically fit into the viewport.
546
+ * If true, the hook will try to fit the popup into the viewport if it doesn't fit in the preferred directions.
547
+ * Defaults to false.
548
+ */
549
+ autoFit?: boolean;
550
+ /**
551
+ * An optional callback to be called when the best position of the popup changes.
552
+ */
553
+ onBestPositionChange?: (bestPosition?: BestPosition) => void;
554
+ }
555
+
556
+ export interface UseWindowFitmentResult {
557
+ /**
558
+ * A ref to be attached to the target element.
559
+ */
560
+ targetRef: RefObject<HTMLDivElement | null>;
561
+ /**
562
+ * A ref to be attached to the popup element.
563
+ */
564
+ popupRef: RefObject<HTMLDivElement | null>;
565
+ /**
566
+ * The calculated best possible position of the popup element.
567
+ */
568
+ bestPosition?: BestPosition;
569
+ /**
570
+ * The style object to be applied to the popup element.
571
+ */
572
+ popupPositionStyle: CSSProperties;
573
+ }
574
+ ```
575
+
576
+ """ ;
577
+ cs:donts """
578
+ (Don't) Create a custom hook for simple, non-reusable state.
579
+ ```tsx
580
+ // Bad: Unnecessary abstraction for a simple counter
581
+ const useCounter = () => {
582
+ const [count, setCount] = useState(0);
583
+ return { count, setCount };
584
+ };
585
+ ```
586
+
587
+ (Don't) Mix multiple concerns in a single hook
588
+ ```typescript
589
+ // Bad: Multiple concerns in one hook
590
+ const useUserData = () => {
591
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
592
+ const [profile, setProfile] = useState(null);
593
+ const [settings, setSettings] = useState({});
594
+ const [notifications, setNotifications] = useState([]);
595
+ return { isAuthenticated, profile, settings, notifications };
596
+ };
597
+ ```
598
+ """ .
599
+
600
+ # Context Provider Structure Standard
601
+ cs:ContextProviderStructure a cs:CodeStandard ;
602
+ cs:name "react/component/structure/context" ;
603
+ cs:extends cs:ComponentFolderStructure ;
604
+ cs:hasCategory cs:ReactCategory ;
605
+ cs:description "Context providers must use `Provider.tsx` as the main component file instead of the standard component naming pattern `[MyComponent].tsx`. Provider prop and return types must be defined in the hook-level types file `hooks/types.ts` with `UseProviderStateProps` (containing all `ProviderProps` minus `children`) and `UseProviderStateResult` (matching the context options)." ;
606
+ cs:dos """
607
+ (Do) Organize provider-related files by separating concerns into distinct files: place the context definition in `Context.tsx`, the provider implementation in `Provider.tsx`, and provider-specific hooks in a `hooks/` directory. Create a `hooks/useProviderState.ts` file to centrally manage the state of the provider.`
608
+ ```
609
+ [MyComponent]/
610
+ ├── Context.tsx
611
+ ├── Provider.tsx
612
+ ├── index.ts
613
+ ├── types.ts
614
+ └── common/
615
+ └─ [SubComponent]/
616
+ ├── index.ts
617
+ ├── [SubComponent].tsx
618
+ └── types.ts
619
+ └── hooks/
620
+ ├── index.ts
621
+ ├── types.ts
622
+ └── useProviderState.ts
623
+ ```
624
+ (Do) Create the context type within `types.ts`.
625
+
626
+ ```typescript
627
+ // [MyComponent]/types.ts
628
+ /** The value of the config context */
629
+ export interface ContextOptions {
630
+ /** Whether the baseline grid should be shown */
631
+ showBaselineGrid: boolean;
632
+ /** Toggles the baseline grid's visibility. */
633
+ toggleShowBaselineGrid: () => void;
634
+ }
635
+ ```
636
+
637
+ (Do) create the provider props type within `types.ts`, accepting `children` at a minimum and more props as needed.
638
+
639
+ ```typescript
640
+ // [MyComponent]/types.ts
641
+
642
+ export interface ProviderProps {
643
+ // The child nodes which will have access to the provider state
644
+ children: React.ReactNode;
645
+ // ...other props...
646
+ }
647
+ ```
648
+
649
+ (Do) Create a `Context.tsx` file for the context definition.
650
+ ```tsx
651
+ // [MyComponent]/Context.tsx
652
+ import { createContext } from "react";
653
+ import type { ContextOptions } from "./types.js";
654
+
655
+ const Context = createContext<ContextOptions | undefined>(undefined);
656
+
657
+ export default Context;
658
+ ```
659
+
660
+
661
+ (Do) Use `Provider.tsx` as the main component file. The provider is responsible for wrapping children with the context.
662
+ ```tsx
663
+ // [MyComponent]/Provider.tsx
664
+ import Context from "./Context.js";
665
+ import { useProviderState } from "./hooks/useProviderState.js";
666
+ import type { ProviderProps } from "./types.js";
667
+
668
+ const Provider = ({ children }: ProviderProps) => {
669
+ const state = useProviderState();
670
+ return <Context.Provider value={state}>{children}</Context.Provider>;
671
+ };
672
+
673
+ export default Provider;
674
+ ```
675
+
676
+ (Do) Use `index.ts` to export the `Provider` as a named component that matches the folder name, casting it to the component type.
677
+ ```typescript
678
+ // [MyComponent]/types.ts
679
+ import type { ReactElement } from "react";
680
+
681
+ export type [MyComponent]Component = ((props: ProviderProps) => ReactElement) & {
682
+ SubComponent: (props: SubComponentProps) => ReactElement | null;
683
+ };
684
+
685
+ // [MyComponent]/index.ts
686
+ import Provider from "./Provider.js";
687
+ import type { [MyComponent] } from "./types.js";
688
+
689
+ export const [MyComponent] = Provider as [MyComponent]Component;
690
+ export default [MyComponent];
691
+ ```
692
+
693
+ (Do) Create provider state hook types in `hooks/types.ts` for context providers.
694
+ ```typescript
695
+ // [MyComponent]/hooks/types.ts
696
+ import type { ContextOptions, ProviderProps } from "../types.js";
697
+
698
+ // The props expected by the provider state hook. The children prop is omitted, as it is not passed to the hook - it is only rendered by the provider component.
699
+ export type UseProviderStateProps = Omit<ProviderProps, "children">;
700
+
701
+ // The result of the provider state hook. This should match the context options defined in the main component types file.
702
+ export type UseProviderStateResult = ContextOptions;
703
+ ```
704
+
705
+ (Do) Create a provider state hook implementation.
706
+ ```tsx
707
+ // [MyComponent]/hooks/useProviderState.ts
708
+ import { useContext } from "react";
709
+ import type { UseProviderStateProps, UseProviderStateResult } from "./types.js";
710
+
711
+ /**
712
+ * Hook to manage the state of the provider
713
+ */
714
+ const useProviderState = ({
715
+ // ..props...
716
+ }: UseProviderStateProps): UseProviderStateResult => {
717
+ // centralize the entire provider state here...
718
+ }
719
+ ```
720
+ """ ;
721
+ cs:donts """
722
+ (Don't) Create a separate component file (e.g., `[MyComponent].tsx`) when `Provider.tsx` exists. The provider is the main component.
723
+ ```
724
+ [MyComponent]/
725
+ ├── [MyComponent].tsx
726
+ └── Provider.tsx
727
+ ```
728
+
729
+ (Don't) Nest context-related files in a `context/` subfolder.
730
+ ```
731
+ [MyComponent]/
732
+ └── context/ # Unnecessary nesting
733
+ ├── Context.tsx
734
+ └── Provider.tsx
735
+ ```
736
+
737
+ (Don't) Create multiple provider files within the same component folder.
738
+ ```
739
+ [MyComponent]/
740
+ ├── Provider.tsx
741
+ └── AnotherProvider.tsx # Only one provider per component
742
+ ```
743
+
744
+ (Don't) Mix concerns of the Provider and its state.
745
+
746
+ ```tsx
747
+ exort const Provider = ({ children }: ProviderProps) => {
748
+ const [state, setState] = useState(...); // Bad: State logic mixed in
749
+ return <Context.Provider value={{ state, setState }}>{children}</Context.Provider>;
750
+ };
751
+ ```
752
+ """ .