create-react-starter-kit 1.0.6 → 1.0.7
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/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -1
- package/dist/cli-args.js.map +1 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +1 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +8 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/templates/README.md.ejs +1 -1
- package/dist/templates/__when_apiLayer_axios__/src/services/api.ts.ejs +23 -0
- package/dist/templates/__when_apiLayer_axios__/src/services/http/client.ts.ejs +174 -0
- package/dist/templates/__when_apiLayer_axios__/src/services/http/errors.ts.ejs +81 -0
- package/dist/templates/__when_apiLayer_axios__/src/services/http/index.ts.ejs +5 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/api.ts.ejs +22 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/http/client.ts.ejs +195 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/http/errors.ts.ejs +81 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/http/index.ts.ejs +5 -0
- package/dist/templates/__when_buildTool_webpack__/webpack.config.js.ejs +1 -1
- package/dist/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +2 -1
- package/dist/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +4 -3
- package/dist/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +28 -5
- package/dist/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +16 -0
- package/dist/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +1 -3
- package/dist/templates/docs/architecture.md.ejs +44 -25
- package/dist/templates/docs/best-practices.md.ejs +39 -3
- package/dist/templates/docs/examples/README.md.ejs +10 -10
- package/dist/templates/docs/examples/api.md.ejs +165 -8
- package/dist/templates/docs/examples/data-fetching.md.ejs +107 -6
- package/dist/templates/docs/examples/forms.md.ejs +66 -6
- package/dist/templates/docs/examples/state.md.ejs +50 -6
- package/dist/templates/docs/examples/styling.md.ejs +56 -8
- package/dist/templates/docs/examples/testing.md.ejs +63 -9
- package/dist/templates/docs/getting-started.md.ejs +63 -15
- package/dist/templates/docs/selected-stack.md.ejs +110 -8
- package/dist/templates/src/features/feedback/FeedbackForm.tsx.ejs +10 -0
- package/dist/templates/src/features/posts/PostsPanel.tsx.ejs +1 -1
- package/dist/templates/src/routes/PostsPage.tsx.ejs +7 -2
- package/dist/templates/src/types/index.ts.ejs +1 -1
- package/dist/templates/src/types/models.ts.ejs +18 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/README.md.ejs +1 -1
- package/templates/__when_apiLayer_axios__/src/services/api.ts.ejs +23 -0
- package/templates/__when_apiLayer_axios__/src/services/http/client.ts.ejs +174 -0
- package/templates/__when_apiLayer_axios__/src/services/http/errors.ts.ejs +81 -0
- package/templates/__when_apiLayer_axios__/src/services/http/index.ts.ejs +5 -0
- package/templates/__when_apiLayer_fetch__/src/services/api.ts.ejs +22 -0
- package/templates/__when_apiLayer_fetch__/src/services/http/client.ts.ejs +195 -0
- package/templates/__when_apiLayer_fetch__/src/services/http/errors.ts.ejs +81 -0
- package/templates/__when_apiLayer_fetch__/src/services/http/index.ts.ejs +5 -0
- package/templates/__when_buildTool_webpack__/webpack.config.js.ejs +1 -1
- package/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +2 -1
- package/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +4 -3
- package/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +28 -5
- package/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +16 -0
- package/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +1 -3
- package/templates/docs/architecture.md.ejs +44 -25
- package/templates/docs/best-practices.md.ejs +39 -3
- package/templates/docs/examples/README.md.ejs +10 -10
- package/templates/docs/examples/api.md.ejs +165 -8
- package/templates/docs/examples/data-fetching.md.ejs +107 -6
- package/templates/docs/examples/forms.md.ejs +66 -6
- package/templates/docs/examples/state.md.ejs +50 -6
- package/templates/docs/examples/styling.md.ejs +56 -8
- package/templates/docs/examples/testing.md.ejs +63 -9
- package/templates/docs/getting-started.md.ejs +63 -15
- package/templates/docs/selected-stack.md.ejs +110 -8
- package/templates/src/features/feedback/FeedbackForm.tsx.ejs +10 -0
- package/templates/src/features/posts/PostsPanel.tsx.ejs +1 -1
- package/templates/src/routes/PostsPage.tsx.ejs +7 -2
- package/templates/src/types/index.ts.ejs +1 -1
- package/templates/src/types/models.ts.ejs +18 -0
- package/dist/templates/src/services/api.ts.ejs +0 -84
- package/templates/src/services/api.ts.ejs +0 -84
|
@@ -2,16 +2,173 @@
|
|
|
2
2
|
|
|
3
3
|
**Choice:** <%= stackLabels.apiLayer %>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<% if (apiLayer === 'none') { -%>
|
|
6
|
+
## Why there is no Axios/Fetch client
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
You selected **RTK Query**, which already provides:
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- a shared base query (`fetchBaseQuery`)
|
|
11
|
+
- caching, deduplication, and generated hooks
|
|
12
|
+
- a single place for headers via `prepareHeaders`
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
- Surface user-friendly errors in UI; log technical details to monitoring.
|
|
13
|
-
- Do not scatter `fetch`/`axios` calls across components.
|
|
14
|
+
Adding Axios/Fetch beside RTK Query usually creates **two HTTP stacks**, duplicated auth logic, and inconsistent error handling.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
**Where HTTP lives:** `src/store/postsApi.<%= ext %>`
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
### Extending RTK Query
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// add to endpoints
|
|
22
|
+
getPostById: builder.query({
|
|
23
|
+
query: (id) => `/posts/${id}`,
|
|
24
|
+
providesTags: (_result, _error, id) => [{ type: 'Posts', id }],
|
|
25
|
+
}),
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Auth header pattern
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
prepareHeaders: (headers) => {
|
|
32
|
+
const token = localStorage.getItem('access_token');
|
|
33
|
+
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
34
|
+
return headers;
|
|
35
|
+
},
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### When you might still add Axios/Fetch later
|
|
39
|
+
|
|
40
|
+
- Uploading to a third-party host that RTK should not cache
|
|
41
|
+
- Streaming / non-JSON APIs
|
|
42
|
+
- A legacy SDK that must use its own client
|
|
43
|
+
|
|
44
|
+
If that happens, isolate it under `src/services/` and keep RTK as the default for app REST resources.
|
|
45
|
+
|
|
46
|
+
See `docs/examples/data-fetching.md` for UI usage.
|
|
47
|
+
<% } else if (apiLayer === 'axios') { -%>
|
|
48
|
+
## Layout
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
src/services/
|
|
52
|
+
http/
|
|
53
|
+
client.<%= ext %> # axios instance + interceptors
|
|
54
|
+
errors.<%= ext %> # ApiError normalization
|
|
55
|
+
index.<%= ext %> # public exports
|
|
56
|
+
api.<%= ext %> # domain methods (fetchPosts, createFeedback)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## How to use
|
|
60
|
+
|
|
61
|
+
### From a query library
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { fetchPosts, createFeedback } from '@/services/api';
|
|
65
|
+
|
|
66
|
+
// TanStack Query
|
|
67
|
+
queryFn: ({ signal }) => fetchPosts(signal)
|
|
68
|
+
|
|
69
|
+
// mutation
|
|
70
|
+
await createFeedback({ name, email, message })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Direct low-level calls (rare)
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { apiGet, apiPost, ApiError } from '@/services/http';
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const post = await apiGet(`/posts/${id}`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof ApiError && error.isUnauthorized) {
|
|
82
|
+
// redirect to login / refresh token
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What the Axios setup includes
|
|
88
|
+
|
|
89
|
+
| Concern | Implementation |
|
|
90
|
+
|---------|----------------|
|
|
91
|
+
| Base URL | `env.apiBaseUrl` |
|
|
92
|
+
| Timeout | 15s |
|
|
93
|
+
| Auth | `Authorization: Bearer <token>` from `localStorage.access_token` |
|
|
94
|
+
| Request tracing | `X-Request-Id` on every request |
|
|
95
|
+
| Errors | Normalized `ApiError` with `status`, `code`, `requestId` |
|
|
96
|
+
| 401 handling | Dispatches `auth:unauthorized` browser event |
|
|
97
|
+
| Verbs | `apiGet` / `apiPost` / `apiPut` / `apiPatch` / `apiDelete` |
|
|
98
|
+
| Cancellation | Pass `{ signal }` from AbortController / query libs |
|
|
99
|
+
|
|
100
|
+
## Best practices
|
|
101
|
+
|
|
102
|
+
- **Components never import axios.** Use domain helpers or at least `@/services/http`.
|
|
103
|
+
- Keep token reads inside the HTTP layer (`getAccessToken`), not in every feature.
|
|
104
|
+
- Listen once for `auth:unauthorized` near the app shell to centralize logout/redirect.
|
|
105
|
+
- Type responses at the boundary (`apiGet<Post[]>`).
|
|
106
|
+
- Log technical details; show user-friendly messages in UI.
|
|
107
|
+
- Prefer adding methods to `api.<%= ext %>` over spreading URLs across features.
|
|
108
|
+
|
|
109
|
+
## Extending with a refresh-token flow
|
|
110
|
+
|
|
111
|
+
1. Detect `ApiError.isUnauthorized` in the response interceptor.
|
|
112
|
+
2. Queue in-flight requests while refreshing.
|
|
113
|
+
3. Retry the original request with the new access token.
|
|
114
|
+
4. Fail the queue and redirect if refresh fails.
|
|
115
|
+
|
|
116
|
+
Keep that logic in `http/client.<%= ext %>` so features stay unaware.
|
|
117
|
+
|
|
118
|
+
## Example: add a new domain method
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// src/services/api.ts
|
|
122
|
+
export async function fetchPost(id: number, signal?: AbortSignal) {
|
|
123
|
+
return apiGet<Post>(`/posts/${id}`, { signal });
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
<% } else { -%>
|
|
127
|
+
## Layout
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
src/services/
|
|
131
|
+
http/
|
|
132
|
+
client.<%= ext %> # fetch wrapper + timeout/auth/errors
|
|
133
|
+
errors.<%= ext %> # ApiError normalization
|
|
134
|
+
index.<%= ext %>
|
|
135
|
+
api.<%= ext %> # domain methods
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How to use
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { fetchPosts, createFeedback } from '@/services/api';
|
|
142
|
+
import { apiGet, ApiError } from '@/services/http';
|
|
143
|
+
|
|
144
|
+
await fetchPosts(signal);
|
|
145
|
+
await createFeedback(payload);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await apiGet('/posts/1');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof ApiError && error.isRetryable) {
|
|
151
|
+
// show retry UI
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## What the Fetch setup includes
|
|
157
|
+
|
|
158
|
+
| Concern | Implementation |
|
|
159
|
+
|---------|----------------|
|
|
160
|
+
| Base URL | `env.apiBaseUrl` |
|
|
161
|
+
| Timeout | AbortController (15s default) |
|
|
162
|
+
| Auth | Bearer token from `localStorage.access_token` |
|
|
163
|
+
| Request tracing | `X-Request-Id` |
|
|
164
|
+
| Errors | Normalized `ApiError` |
|
|
165
|
+
| 401 handling | `auth:unauthorized` event |
|
|
166
|
+
| Verbs | get/post/put/patch/delete helpers |
|
|
167
|
+
|
|
168
|
+
## Best practices
|
|
169
|
+
|
|
170
|
+
- Prefer domain helpers over raw `request()` in components.
|
|
171
|
+
- Pass `signal` from TanStack Query / user cancellation.
|
|
172
|
+
- Keep JSON as the default; use `rawBody: true` for `FormData` uploads.
|
|
173
|
+
- Centralize auth and error mapping — do not re-implement per feature.
|
|
174
|
+
<% } -%>
|
|
@@ -1,18 +1,119 @@
|
|
|
1
1
|
# Data fetching example
|
|
2
2
|
|
|
3
3
|
**Library:** <%= stackLabels.serverState %>
|
|
4
|
+
<% if (apiLayer !== 'none') { -%>
|
|
4
5
|
**Transport:** <%= stackLabels.apiLayer %>
|
|
6
|
+
<% } else { -%>
|
|
7
|
+
**Transport:** RTK Query `fetchBaseQuery` (no separate Axios/Fetch client)
|
|
8
|
+
<% } -%>
|
|
5
9
|
|
|
6
|
-
**Live demo:**
|
|
10
|
+
**Live demo:** `/posts` → `src/features/posts/PostsPanel.<%= jsxExt %>`
|
|
7
11
|
|
|
8
12
|
## When to use
|
|
9
13
|
|
|
10
|
-
Any remote read/write that benefits from caching, deduplication, retries, or background refresh
|
|
14
|
+
Any remote read/write that benefits from caching, deduplication, retries, or background refresh:
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
- lists and detail pages
|
|
17
|
+
- polling dashboards
|
|
18
|
+
- mutations that should refresh related queries
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
## How to use in this project
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
23
|
+
### 1. Define endpoints
|
|
17
24
|
|
|
18
|
-
See `src/
|
|
25
|
+
See `src/store/postsApi.<%= ext %>`:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export const postsApi = createApi({
|
|
29
|
+
reducerPath: 'postsApi',
|
|
30
|
+
baseQuery: fetchBaseQuery({
|
|
31
|
+
baseUrl: env.apiBaseUrl,
|
|
32
|
+
prepareHeaders: (headers) => {
|
|
33
|
+
const token = localStorage.getItem('access_token');
|
|
34
|
+
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
35
|
+
return headers;
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
tagTypes: ['Posts'],
|
|
39
|
+
endpoints: (builder) => ({
|
|
40
|
+
getPosts: builder.query({
|
|
41
|
+
query: () => '/posts?_limit=5',
|
|
42
|
+
providesTags: ['Posts'],
|
|
43
|
+
}),
|
|
44
|
+
createFeedback: builder.mutation({
|
|
45
|
+
query: (body) => ({ url: '/posts', method: 'POST', body }),
|
|
46
|
+
invalidatesTags: ['Posts'],
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Consume hooks in UI
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
const { data, isLoading, error, refetch, isFetching } = useGetPostsQuery();
|
|
56
|
+
const [createFeedback] = useCreateFeedbackMutation();
|
|
57
|
+
await createFeedback(values).unwrap();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Best practices (RTK Query)
|
|
61
|
+
|
|
62
|
+
- Prefer **tags** over manual `refetch()` for coherence after writes.
|
|
63
|
+
- Keep `prepareHeaders` as the single place for auth.
|
|
64
|
+
- Split large APIs with `injectEndpoints` as the app grows.
|
|
65
|
+
- Use `transformResponse` for shaping DTO → UI models at the boundary.
|
|
66
|
+
- Handle errors with `error` from the hook; surface user-friendly messages in UI.
|
|
67
|
+
<% } else if (serverState === 'tanstack-query') { -%>
|
|
68
|
+
### Query example
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
72
|
+
queryKey: ['posts'],
|
|
73
|
+
queryFn: ({ signal }) => fetchPosts(signal),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`fetchPosts` lives in `src/services/api.<%= ext %>` and already goes through the shared HTTP client.
|
|
78
|
+
|
|
79
|
+
### Mutation example
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const mutation = useMutation({
|
|
83
|
+
mutationFn: createFeedback,
|
|
84
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Best practices (TanStack Query)
|
|
89
|
+
|
|
90
|
+
- Hierarchical keys: `['posts']`, `['posts', postId]`, `['users', userId, 'posts']`
|
|
91
|
+
- Always accept/pass `AbortSignal` for cancellable requests
|
|
92
|
+
- Set sensible `staleTime` (already defaulted in `providers.<%= jsxExt %>`)
|
|
93
|
+
- Keep `queryFn` pure: no React state writes inside it
|
|
94
|
+
<% } else if (serverState === 'swr') { -%>
|
|
95
|
+
### Example
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
const { data, error, isLoading, mutate } = useSWR('/posts?_limit=5');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The shared fetcher in `providers.<%= jsxExt %>` calls `apiGet`.
|
|
102
|
+
|
|
103
|
+
### Best practices (SWR)
|
|
104
|
+
|
|
105
|
+
- Use stable keys (URL or serialized params)
|
|
106
|
+
- Call `mutate` after writes to revalidate
|
|
107
|
+
- Prefer the global fetcher; override per-hook only when necessary
|
|
108
|
+
<% } else { -%>
|
|
109
|
+
Manual fetching is used in the posts panel. As soon as you add caching needs, introduce TanStack Query, SWR, or RTK Query.
|
|
110
|
+
<% } -%>
|
|
111
|
+
|
|
112
|
+
## Best practice (all libraries)
|
|
113
|
+
|
|
114
|
+
Separate **server cache** from **client UI state**. Key queries by resource identity, not by component instance.
|
|
115
|
+
|
|
116
|
+
## Related docs
|
|
117
|
+
|
|
118
|
+
- HTTP transport details: `docs/examples/api.md`
|
|
119
|
+
- Architecture boundaries: `docs/architecture.md`
|
|
@@ -3,16 +3,76 @@
|
|
|
3
3
|
**Forms:** <%= stackLabels.forms %>
|
|
4
4
|
**Validation:** <%= stackLabels.validation %>
|
|
5
5
|
|
|
6
|
-
**Live demo:**
|
|
6
|
+
**Live demo:** `/feedback` → `src/features/feedback/FeedbackForm.<%= jsxExt %>`
|
|
7
|
+
**Schemas:** `src/lib/validation.<%= ext %>`
|
|
7
8
|
|
|
8
9
|
## When to use
|
|
9
10
|
|
|
10
|
-
Multi-field input with client-side rules and async submit
|
|
11
|
+
Multi-field input with client-side rules and async submit:
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
- contact / feedback forms
|
|
14
|
+
- settings screens
|
|
15
|
+
- multi-step wizards (add a stepper on top of the same schema)
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
## How to use in this project
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
1. Define the schema once in `src/lib/validation.<%= ext %>`.
|
|
20
|
+
2. Bind inputs with <%= stackLabels.forms %>.
|
|
21
|
+
3. On submit, call the API layer:
|
|
22
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
23
|
+
- RTK mutation: `useCreateFeedbackMutation()` → `.unwrap()`
|
|
24
|
+
<% } else { -%>
|
|
25
|
+
- `createFeedback()` from `@/services/api`
|
|
26
|
+
<% } -%>
|
|
27
|
+
4. Show success with `role="status"` and reset the form.
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
<% if (forms === 'react-hook-form') { -%>
|
|
30
|
+
### React Hook Form pattern
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
const {
|
|
34
|
+
register,
|
|
35
|
+
handleSubmit,
|
|
36
|
+
formState: { errors, isSubmitting },
|
|
37
|
+
} = useForm({
|
|
38
|
+
defaultValues,
|
|
39
|
+
resolver: zodResolver(feedbackSchema), // when using Zod
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const onSubmit = handleSubmit(async (values) => {
|
|
43
|
+
await createFeedback(values);
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
<% } else if (forms === 'formik') { -%>
|
|
47
|
+
### Formik pattern
|
|
48
|
+
|
|
49
|
+
Use `<Formik>` + `<Field>` and pass a Yup schema via `validationSchema` when selected.
|
|
50
|
+
<% } else if (forms === 'tanstack-form') { -%>
|
|
51
|
+
### TanStack Form pattern
|
|
52
|
+
|
|
53
|
+
Create the form with `useForm`, validate with your schema inside `onSubmit`, and bind each field through `form.Field`.
|
|
54
|
+
<% } else { -%>
|
|
55
|
+
### Native form pattern
|
|
56
|
+
|
|
57
|
+
Keep values in `useState`, run the schema/validator on submit, then call the API helper.
|
|
58
|
+
<% } -%>
|
|
59
|
+
|
|
60
|
+
## Best practices
|
|
61
|
+
|
|
62
|
+
- **Single source of truth for rules** — schemas in `src/lib`, not duplicated `if` checks in JSX.
|
|
63
|
+
- Validate **on submit** first; add on-blur validation when users need earlier feedback.
|
|
64
|
+
- Map API validation errors (422) back onto fields when the backend returns field paths.
|
|
65
|
+
- Disable the submit button while `isSubmitting` to prevent double posts.
|
|
66
|
+
- Keep forms accessible: real `<label>`, `aria-invalid`, and visible error text.
|
|
67
|
+
- Do not store form drafts in global client state unless the draft must survive across routes.
|
|
68
|
+
|
|
69
|
+
## Testing tips
|
|
70
|
+
|
|
71
|
+
- Assert validation messages for empty submit.
|
|
72
|
+
- Mock the network boundary (API helper or RTK mutation) and assert the success status region.
|
|
73
|
+
- Prefer `userEvent` over `fireEvent` for realistic typing.
|
|
74
|
+
|
|
75
|
+
## Related
|
|
76
|
+
|
|
77
|
+
- HTTP / RTK details: `docs/examples/api.md`
|
|
78
|
+
- Live UI: `/feedback`
|
|
@@ -2,16 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
**Library:** <%= stackLabels.stateManagement %>
|
|
4
4
|
|
|
5
|
-
**Live demo:**
|
|
5
|
+
**Live demo:** `/counter` → `src/features/counter/CounterPanel.<%= jsxExt %>`
|
|
6
|
+
**Store wiring:** `src/store/`
|
|
6
7
|
|
|
7
8
|
## When to use
|
|
8
9
|
|
|
9
|
-
Shared interactive state that is not fetched from an API
|
|
10
|
+
Shared interactive state that is **not** fetched from an API:
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
- wizard steps, UI toggles, selected tabs
|
|
13
|
+
- ephemeral counters / filters that reset often
|
|
14
|
+
- cross-route UI that should survive navigation but not a full reload
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
If the data comes from your backend, prefer **<%= stackLabels.serverState %>** instead.
|
|
14
17
|
|
|
15
|
-
##
|
|
18
|
+
## How to use in this project
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
<% if (stateManagement === 'redux') { -%>
|
|
21
|
+
1. Counter slice lives in `src/store/index.<%= ext %>` (`counterSlice` + `store`).
|
|
22
|
+
2. Read/write with `useSelector` / `useDispatch` from the feature panel (or add typed hooks later).
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
26
|
+
import { increment, type RootState } from '@/store';
|
|
27
|
+
|
|
28
|
+
const value = useSelector((s: RootState) => s.counter.value);
|
|
29
|
+
const dispatch = useDispatch();
|
|
30
|
+
dispatch(increment());
|
|
31
|
+
```
|
|
32
|
+
<% } else if (stateManagement === 'zustand') { -%>
|
|
33
|
+
Create/consume the store from `src/store/index.<%= ext %>` with selectors so components only re-render on the slices they need.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useCounterStore } from '@/store';
|
|
37
|
+
|
|
38
|
+
const value = useCounterStore((s) => s.value);
|
|
39
|
+
const increment = useCounterStore((s) => s.increment);
|
|
40
|
+
```
|
|
41
|
+
<% } else if (stateManagement === 'jotai') { -%>
|
|
42
|
+
Define atoms in `src/store/index.<%= ext %>` and compose them. Wrap the tree with `Provider` (already done in `providers.<%= jsxExt %>`).
|
|
43
|
+
<% } else if (stateManagement === 'context') { -%>
|
|
44
|
+
Use the provided context + reducer pattern in `src/store/context`. Keep the context value stable; expose actions through a custom hook.
|
|
45
|
+
<% } else { -%>
|
|
46
|
+
No global client-state library was selected. Prefer React local state until multiple distant trees need the same data.
|
|
47
|
+
<% } -%>
|
|
48
|
+
|
|
49
|
+
## Best practices
|
|
50
|
+
|
|
51
|
+
- **Feature-local first.** Promote to global only when props drilling becomes painful across routes.
|
|
52
|
+
- **One concern per store/slice.** Avoid a single “appState” blob.
|
|
53
|
+
- **Do not mirror server entities** in client state when you already use <%= stackLabels.serverState %>.
|
|
54
|
+
- Prefer selectors/hooks over reading the entire store in leaf components.
|
|
55
|
+
- Keep side effects (HTTP, analytics) out of reducers; trigger them from thunks/listeners/components.
|
|
56
|
+
|
|
57
|
+
## Common pitfalls
|
|
58
|
+
|
|
59
|
+
- Putting API responses into Redux/Zustand “because we might need them later”
|
|
60
|
+
- Updating global state on every keystroke in a form (forms belong to <%= stackLabels.forms %>)
|
|
61
|
+
- Forgetting to reset ephemeral UI state when leaving a flow
|
|
@@ -5,19 +5,67 @@
|
|
|
5
5
|
|
|
6
6
|
## When to use
|
|
7
7
|
|
|
8
|
-
Establish design tokens early; use a component library for complex accessible primitives.
|
|
8
|
+
Establish design tokens early; use a component library for complex accessible primitives (dialogs, menus, focus traps).
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## How to use in this project
|
|
11
|
+
|
|
12
|
+
| Piece | Location |
|
|
13
|
+
|-------|----------|
|
|
14
|
+
| Global styles / tokens | `src/styles/global.<%= styling === 'sass' && uiLibrary !== 'shadcn' ? 'scss' : 'css' %>` |
|
|
15
|
+
| Button adapter | `src/components/ui/UiButton.<%= jsxExt %>` |
|
|
16
|
+
| Layout chrome | `src/components/layout/` |
|
|
17
|
+
|
|
18
|
+
<% if (styling === 'tailwind') { -%>
|
|
19
|
+
### Tailwind
|
|
20
|
+
|
|
21
|
+
Utility classes are available across the app. Prefer composing utilities in components over one-off CSS files unless the rule is truly global.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<button className="rounded-md bg-slate-900 px-3 py-2 text-white hover:bg-slate-800">
|
|
25
|
+
Save
|
|
26
|
+
</button>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Best practices**
|
|
30
|
+
|
|
31
|
+
- Extract repeated class clusters into small components (`UiButton`) instead of giant `className` strings everywhere.
|
|
32
|
+
- Keep design tokens (colors, spacing) consistent; avoid magic hex values in features.
|
|
33
|
+
<% } else if (styling === 'css-modules') { -%>
|
|
34
|
+
### CSS Modules
|
|
35
|
+
|
|
36
|
+
Colocate `*.module.css` next to the component. Import the `styles` object and reference `styles.root`.
|
|
37
|
+
<% } else if (styling === 'styled-components') { -%>
|
|
38
|
+
### Styled Components
|
|
39
|
+
|
|
40
|
+
Create styled primitives next to the component. Prefer transient props (`$active`) so DOM attributes stay clean.
|
|
41
|
+
<% } else { -%>
|
|
42
|
+
### Sass/SCSS
|
|
43
|
+
|
|
44
|
+
Shared variables/mixins belong under `src/styles/`. Feature-specific partials can live next to the feature.
|
|
45
|
+
<% } -%>
|
|
11
46
|
|
|
12
47
|
<% if (uiLibrary === 'shadcn') { -%>
|
|
13
|
-
|
|
48
|
+
## shadcn/ui notes
|
|
49
|
+
|
|
50
|
+
Treat shadcn-style primitives as **owned source** — customize freely, keep `cn()` helpers consistent, and avoid patching node_modules.
|
|
14
51
|
<% } else if (uiLibrary === 'none') { -%>
|
|
15
|
-
|
|
52
|
+
## No UI kit
|
|
53
|
+
|
|
54
|
+
Invest in a small set of shared primitives (`UiButton`, layout) before adding a full kit. This keeps accessibility and spacing consistent.
|
|
16
55
|
<% } else { -%>
|
|
17
|
-
|
|
56
|
+
## <%= stackLabels.uiLibrary %> notes
|
|
57
|
+
|
|
58
|
+
Wrap third-party components in thin adapters (`UiButton`) so swapping kits later is cheaper and theme overrides stay centralized (see `providers.<%= jsxExt %>`).
|
|
18
59
|
<% } -%>
|
|
19
60
|
|
|
20
|
-
##
|
|
61
|
+
## Best practices
|
|
62
|
+
|
|
63
|
+
- One visual language: spacing, type scale, and radius should feel intentional.
|
|
64
|
+
- Prefer composition over deep CSS specificity wars.
|
|
65
|
+
- Ensure focus states are visible for keyboard users.
|
|
66
|
+
- Keep marketing/landing flair out of app chrome unless product requires it.
|
|
67
|
+
|
|
68
|
+
## Related
|
|
21
69
|
|
|
22
|
-
|
|
23
|
-
|
|
70
|
+
- Architecture: `docs/architecture.md`
|
|
71
|
+
- Accessibility checklist: `docs/best-practices.md`
|
|
@@ -8,20 +8,74 @@
|
|
|
8
8
|
|
|
9
9
|
## When to use
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
| Layer | Use for | Avoid using for |
|
|
12
|
+
|-------|---------|-----------------|
|
|
13
|
+
| Unit / RTL | logic, component behavior, form validation | full auth/payment journeys |
|
|
14
|
+
| E2E | critical user journeys across routes | asserting every CSS class |
|
|
15
|
+
| Visual | regressions in appearance / states | business-rule correctness |
|
|
14
16
|
|
|
15
|
-
##
|
|
17
|
+
## How to run
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
<% if (unitTesting !== 'none') { -%>
|
|
20
|
+
```bash
|
|
21
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> test
|
|
22
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> test:watch
|
|
23
|
+
```
|
|
24
|
+
<% } -%>
|
|
25
|
+
<% if (e2eTesting === 'playwright') { -%>
|
|
26
|
+
```bash
|
|
27
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> test:e2e:install # once
|
|
28
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> test:e2e
|
|
29
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> test:e2e:ui
|
|
30
|
+
```
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
E2E base URL: `http://127.0.0.1:<%= buildTool === 'webpack' ? 3000 : 5173 %>`
|
|
33
|
+
<% } else if (e2eTesting === 'cypress') { -%>
|
|
34
|
+
```bash
|
|
35
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> test:e2e
|
|
36
|
+
```
|
|
37
|
+
<% } -%>
|
|
38
|
+
<% if (visualTesting === 'storybook') { -%>
|
|
39
|
+
```bash
|
|
40
|
+
<%= packageManager === 'npm' ? 'npm run' : packageManager %> storybook
|
|
41
|
+
```
|
|
42
|
+
<% } -%>
|
|
43
|
+
|
|
44
|
+
## Project layout
|
|
20
45
|
|
|
21
46
|
- Setup: `tests/setup.<%= ext %>` (when applicable)
|
|
22
|
-
-
|
|
47
|
+
- Unit suites: `tests/*.test.<%= jsxExt %>`
|
|
23
48
|
<% if (e2eTesting === 'playwright') { -%>
|
|
24
|
-
- E2E: `tests/e2e
|
|
49
|
+
- E2E: `tests/e2e/*.spec.<%= ext %>` (excluded from unit runner)
|
|
25
50
|
<% } else if (e2eTesting === 'cypress') { -%>
|
|
26
|
-
- E2E: `cypress/e2e
|
|
51
|
+
- E2E: `cypress/e2e/*`
|
|
52
|
+
<% } -%>
|
|
53
|
+
<% if (visualTesting === 'storybook') { -%>
|
|
54
|
+
- Stories: `src/**/*.stories.<%= jsxExt %>`
|
|
27
55
|
<% } -%>
|
|
56
|
+
|
|
57
|
+
## Best practices
|
|
58
|
+
|
|
59
|
+
- Query the way users do: `getByRole`, label text, placeholder — not implementation details.
|
|
60
|
+
- Mock at the **network/domain boundary**:
|
|
61
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
62
|
+
- mock RTK hooks/endpoints for unit tests when you are not integration-testing the store
|
|
63
|
+
<% } else { -%>
|
|
64
|
+
- mock `@/services/api` helpers rather than axios/fetch internals
|
|
65
|
+
<% } -%>
|
|
66
|
+
- Keep e2e **thin**: home load, posts happy path, feedback submit.
|
|
67
|
+
- Do not let unit tests import Playwright specs (unit config excludes `tests/e2e`).
|
|
68
|
+
- For async UI, prefer `findBy*` / `waitFor` over arbitrary `sleep`.
|
|
69
|
+
|
|
70
|
+
## Example unit test shape
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
renderWithProviders(<FeedbackForm />);
|
|
74
|
+
await user.click(screen.getByRole('button', { name: /Send feedback/i }));
|
|
75
|
+
expect(screen.getAllByText(/required|valid email/i).length).toBeGreaterThan(0);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Related
|
|
79
|
+
|
|
80
|
+
- Strategy & CI: `docs/best-practices.md`
|
|
81
|
+
- Forms guide: `docs/examples/forms.md`
|