autoworkflow 3.1.5 → 3.6.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Quarkus Skill
|
|
2
|
+
|
|
3
|
+
## Project Setup
|
|
4
|
+
\`\`\`bash
|
|
5
|
+
# Create new project
|
|
6
|
+
quarkus create app com.example:myapp \\
|
|
7
|
+
--extension='resteasy-reactive-jackson,hibernate-orm-panache,jdbc-postgresql,smallrye-jwt'
|
|
8
|
+
|
|
9
|
+
# Run in dev mode (hot reload)
|
|
10
|
+
quarkus dev
|
|
11
|
+
|
|
12
|
+
# Build native executable
|
|
13
|
+
quarkus build --native
|
|
14
|
+
\`\`\`
|
|
15
|
+
|
|
16
|
+
## REST Resource with JAX-RS
|
|
17
|
+
\`\`\`java
|
|
18
|
+
@Path("/api/v1/users")
|
|
19
|
+
@Produces(MediaType.APPLICATION_JSON)
|
|
20
|
+
@Consumes(MediaType.APPLICATION_JSON)
|
|
21
|
+
public class UserResource {
|
|
22
|
+
|
|
23
|
+
@Inject
|
|
24
|
+
UserService userService;
|
|
25
|
+
|
|
26
|
+
@GET
|
|
27
|
+
public List<UserResponse> listUsers(
|
|
28
|
+
@QueryParam("page") @DefaultValue("0") int page,
|
|
29
|
+
@QueryParam("size") @DefaultValue("20") int size) {
|
|
30
|
+
return userService.findAll(page, size);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@GET
|
|
34
|
+
@Path("/{id}")
|
|
35
|
+
public UserResponse getUser(@PathParam("id") String id) {
|
|
36
|
+
return userService.findById(id)
|
|
37
|
+
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@POST
|
|
41
|
+
public Response createUser(@Valid CreateUserRequest request) {
|
|
42
|
+
UserResponse user = userService.create(request);
|
|
43
|
+
return Response.status(Response.Status.CREATED)
|
|
44
|
+
.entity(user)
|
|
45
|
+
.location(URI.create("/api/v1/users/" + user.id()))
|
|
46
|
+
.build();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@PUT
|
|
50
|
+
@Path("/{id}")
|
|
51
|
+
public UserResponse updateUser(
|
|
52
|
+
@PathParam("id") String id,
|
|
53
|
+
@Valid UpdateUserRequest request) {
|
|
54
|
+
return userService.update(id, request);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@DELETE
|
|
58
|
+
@Path("/{id}")
|
|
59
|
+
public Response deleteUser(@PathParam("id") String id) {
|
|
60
|
+
userService.delete(id);
|
|
61
|
+
return Response.noContent().build();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
## Reactive REST Resource
|
|
67
|
+
\`\`\`java
|
|
68
|
+
@Path("/api/v1/users")
|
|
69
|
+
@Produces(MediaType.APPLICATION_JSON)
|
|
70
|
+
@Consumes(MediaType.APPLICATION_JSON)
|
|
71
|
+
public class UserResource {
|
|
72
|
+
|
|
73
|
+
@Inject
|
|
74
|
+
UserService userService;
|
|
75
|
+
|
|
76
|
+
@GET
|
|
77
|
+
public Uni<List<UserResponse>> listUsers() {
|
|
78
|
+
return userService.findAllAsync();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@GET
|
|
82
|
+
@Path("/{id}")
|
|
83
|
+
public Uni<UserResponse> getUser(@PathParam("id") String id) {
|
|
84
|
+
return userService.findByIdAsync(id)
|
|
85
|
+
.onItem().ifNull().failWith(() -> new NotFoundException("User not found"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@POST
|
|
89
|
+
public Uni<Response> createUser(@Valid CreateUserRequest request) {
|
|
90
|
+
return userService.createAsync(request)
|
|
91
|
+
.onItem().transform(user -> Response.status(Response.Status.CREATED)
|
|
92
|
+
.entity(user)
|
|
93
|
+
.build());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## Panache Entity (Active Record Pattern)
|
|
99
|
+
\`\`\`java
|
|
100
|
+
@Entity
|
|
101
|
+
@Table(name = "users")
|
|
102
|
+
public class User extends PanacheEntityBase {
|
|
103
|
+
|
|
104
|
+
@Id
|
|
105
|
+
@GeneratedValue(strategy = GenerationType.UUID)
|
|
106
|
+
public String id;
|
|
107
|
+
|
|
108
|
+
@Column(unique = true, nullable = false)
|
|
109
|
+
public String email;
|
|
110
|
+
|
|
111
|
+
@Column(nullable = false)
|
|
112
|
+
public String name;
|
|
113
|
+
|
|
114
|
+
@Column(name = "password_hash", nullable = false)
|
|
115
|
+
public String passwordHash;
|
|
116
|
+
|
|
117
|
+
@Column(name = "is_active")
|
|
118
|
+
public boolean isActive = true;
|
|
119
|
+
|
|
120
|
+
@Column(name = "created_at")
|
|
121
|
+
public Instant createdAt;
|
|
122
|
+
|
|
123
|
+
// Static finder methods
|
|
124
|
+
public static Optional<User> findByEmail(String email) {
|
|
125
|
+
return find("email", email).firstResultOptional();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public static List<User> findActive() {
|
|
129
|
+
return list("isActive", true);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public static PanacheQuery<User> findActiveQuery() {
|
|
133
|
+
return find("isActive", Sort.by("createdAt").descending(), true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Lifecycle callbacks
|
|
137
|
+
@PrePersist
|
|
138
|
+
void onCreate() {
|
|
139
|
+
createdAt = Instant.now();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Usage
|
|
144
|
+
User user = new User();
|
|
145
|
+
user.email = "test@example.com";
|
|
146
|
+
user.name = "Test";
|
|
147
|
+
user.persist();
|
|
148
|
+
|
|
149
|
+
Optional<User> found = User.findByEmail("test@example.com");
|
|
150
|
+
List<User> page = User.findActiveQuery().page(0, 20).list();
|
|
151
|
+
long count = User.count("isActive", true);
|
|
152
|
+
\`\`\`
|
|
153
|
+
|
|
154
|
+
## Panache Repository Pattern
|
|
155
|
+
\`\`\`java
|
|
156
|
+
@ApplicationScoped
|
|
157
|
+
public class UserRepository implements PanacheRepositoryBase<User, String> {
|
|
158
|
+
|
|
159
|
+
public Optional<User> findByEmail(String email) {
|
|
160
|
+
return find("email", email).firstResultOptional();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public List<User> findActive(int page, int size) {
|
|
164
|
+
return find("isActive", Sort.by("createdAt").descending(), true)
|
|
165
|
+
.page(page, size)
|
|
166
|
+
.list();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public long countActive() {
|
|
170
|
+
return count("isActive", true);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Reactive repository
|
|
175
|
+
@ApplicationScoped
|
|
176
|
+
public class UserRepository implements PanacheRepositoryBase<User, String> {
|
|
177
|
+
|
|
178
|
+
public Uni<Optional<User>> findByEmailAsync(String email) {
|
|
179
|
+
return find("email", email).firstResultOptional();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public Uni<List<User>> findActiveAsync() {
|
|
183
|
+
return find("isActive", true).list();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
## Service with CDI
|
|
189
|
+
\`\`\`java
|
|
190
|
+
@ApplicationScoped
|
|
191
|
+
public class UserService {
|
|
192
|
+
|
|
193
|
+
@Inject
|
|
194
|
+
UserRepository userRepository;
|
|
195
|
+
|
|
196
|
+
@Inject
|
|
197
|
+
PasswordEncoder passwordEncoder;
|
|
198
|
+
|
|
199
|
+
public List<UserResponse> findAll(int page, int size) {
|
|
200
|
+
return userRepository.findActive(page, size)
|
|
201
|
+
.stream()
|
|
202
|
+
.map(UserResponse::from)
|
|
203
|
+
.toList();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public Optional<UserResponse> findById(String id) {
|
|
207
|
+
return userRepository.findByIdOptional(id)
|
|
208
|
+
.map(UserResponse::from);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Transactional
|
|
212
|
+
public UserResponse create(CreateUserRequest request) {
|
|
213
|
+
if (userRepository.findByEmail(request.email()).isPresent()) {
|
|
214
|
+
throw new ConflictException("Email already exists");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
User user = new User();
|
|
218
|
+
user.email = request.email();
|
|
219
|
+
user.name = request.name();
|
|
220
|
+
user.passwordHash = passwordEncoder.encode(request.password());
|
|
221
|
+
userRepository.persist(user);
|
|
222
|
+
|
|
223
|
+
return UserResponse.from(user);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@Transactional
|
|
227
|
+
public UserResponse update(String id, UpdateUserRequest request) {
|
|
228
|
+
User user = userRepository.findByIdOptional(id)
|
|
229
|
+
.orElseThrow(() -> new NotFoundException("User not found"));
|
|
230
|
+
|
|
231
|
+
if (request.name() != null) {
|
|
232
|
+
user.name = request.name();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return UserResponse.from(user);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@Transactional
|
|
239
|
+
public void delete(String id) {
|
|
240
|
+
if (!userRepository.deleteById(id)) {
|
|
241
|
+
throw new NotFoundException("User not found");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
\`\`\`
|
|
246
|
+
|
|
247
|
+
## Exception Handling
|
|
248
|
+
\`\`\`java
|
|
249
|
+
@Provider
|
|
250
|
+
public class ExceptionMappers {
|
|
251
|
+
|
|
252
|
+
@ServerExceptionMapper
|
|
253
|
+
public Response handleNotFound(NotFoundException e) {
|
|
254
|
+
return Response.status(Response.Status.NOT_FOUND)
|
|
255
|
+
.entity(new ErrorResponse("NOT_FOUND", e.getMessage()))
|
|
256
|
+
.build();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@ServerExceptionMapper
|
|
260
|
+
public Response handleConflict(ConflictException e) {
|
|
261
|
+
return Response.status(Response.Status.CONFLICT)
|
|
262
|
+
.entity(new ErrorResponse("CONFLICT", e.getMessage()))
|
|
263
|
+
.build();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@ServerExceptionMapper
|
|
267
|
+
public Response handleValidation(ConstraintViolationException e) {
|
|
268
|
+
Map<String, String> errors = e.getConstraintViolations().stream()
|
|
269
|
+
.collect(Collectors.toMap(
|
|
270
|
+
v -> v.getPropertyPath().toString(),
|
|
271
|
+
ConstraintViolation::getMessage
|
|
272
|
+
));
|
|
273
|
+
return Response.status(Response.Status.BAD_REQUEST)
|
|
274
|
+
.entity(new ValidationErrorResponse("VALIDATION_ERROR", errors))
|
|
275
|
+
.build();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public record ErrorResponse(String code, String message) {}
|
|
280
|
+
public record ValidationErrorResponse(String code, Map<String, String> errors) {}
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
## Configuration
|
|
284
|
+
\`\`\`properties
|
|
285
|
+
# application.properties
|
|
286
|
+
|
|
287
|
+
# Database
|
|
288
|
+
quarkus.datasource.db-kind=postgresql
|
|
289
|
+
quarkus.datasource.jdbc.url=\${DATABASE_URL}
|
|
290
|
+
quarkus.datasource.username=\${DATABASE_USER}
|
|
291
|
+
quarkus.datasource.password=\${DATABASE_PASSWORD}
|
|
292
|
+
|
|
293
|
+
# Hibernate
|
|
294
|
+
quarkus.hibernate-orm.database.generation=validate
|
|
295
|
+
|
|
296
|
+
# JWT Security
|
|
297
|
+
mp.jwt.verify.publickey.location=publicKey.pem
|
|
298
|
+
mp.jwt.verify.issuer=https://example.com
|
|
299
|
+
|
|
300
|
+
# Native build optimizations
|
|
301
|
+
quarkus.native.additional-build-args=--initialize-at-run-time=org.example.MyClass
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## Testing
|
|
305
|
+
\`\`\`java
|
|
306
|
+
@QuarkusTest
|
|
307
|
+
class UserResourceTest {
|
|
308
|
+
|
|
309
|
+
@Test
|
|
310
|
+
void testGetUser() {
|
|
311
|
+
given()
|
|
312
|
+
.when().get("/api/v1/users/1")
|
|
313
|
+
.then()
|
|
314
|
+
.statusCode(200)
|
|
315
|
+
.body("email", equalTo("test@example.com"));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@Test
|
|
319
|
+
void testCreateUser() {
|
|
320
|
+
given()
|
|
321
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
322
|
+
.body("""
|
|
323
|
+
{"email": "new@example.com", "name": "New User", "password": "password123"}
|
|
324
|
+
""")
|
|
325
|
+
.when().post("/api/v1/users")
|
|
326
|
+
.then()
|
|
327
|
+
.statusCode(201)
|
|
328
|
+
.header("Location", containsString("/api/v1/users/"));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@Test
|
|
332
|
+
void testCreateUserInvalidEmail() {
|
|
333
|
+
given()
|
|
334
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
335
|
+
.body("""
|
|
336
|
+
{"email": "invalid", "name": "Test", "password": "password123"}
|
|
337
|
+
""")
|
|
338
|
+
.when().post("/api/v1/users")
|
|
339
|
+
.then()
|
|
340
|
+
.statusCode(400);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@QuarkusTest
|
|
345
|
+
@TestProfile(TestProfile.class) // Use test configuration
|
|
346
|
+
class UserServiceTest {
|
|
347
|
+
|
|
348
|
+
@Inject
|
|
349
|
+
UserService userService;
|
|
350
|
+
|
|
351
|
+
@Test
|
|
352
|
+
@Transactional
|
|
353
|
+
void testCreateAndFind() {
|
|
354
|
+
var request = new CreateUserRequest("test@example.com", "Test", "password123");
|
|
355
|
+
var created = userService.create(request);
|
|
356
|
+
|
|
357
|
+
assertThat(created.email()).isEqualTo("test@example.com");
|
|
358
|
+
|
|
359
|
+
var found = userService.findById(created.id());
|
|
360
|
+
assertThat(found).isPresent();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
\`\`\`
|
|
364
|
+
|
|
365
|
+
## ✅ DO
|
|
366
|
+
- Use Panache for simplified data access
|
|
367
|
+
- Use CDI \`@Inject\` for dependency injection
|
|
368
|
+
- Use \`@Transactional\` on service methods
|
|
369
|
+
- Use \`Uni<T>\` for reactive endpoints
|
|
370
|
+
- Use \`@ServerExceptionMapper\` for error handling
|
|
371
|
+
- Profile with \`quarkus dev\` for optimal native image
|
|
372
|
+
|
|
373
|
+
## ❌ DON'T
|
|
374
|
+
- Don't use constructor injection (use field injection with CDI)
|
|
375
|
+
- Don't return null - use Optional or throw exceptions
|
|
376
|
+
- Don't block in reactive code
|
|
377
|
+
- Don't forget \`@Transactional\` for write operations with Panache
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# React Skill
|
|
2
|
+
|
|
3
|
+
## Component Structure
|
|
4
|
+
\`\`\`tsx
|
|
5
|
+
export function UserProfile({ userId }: Props) {
|
|
6
|
+
// 1. Hooks first
|
|
7
|
+
const [user, setUser] = useState<User | null>(null);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
|
|
10
|
+
// 2. Derived values
|
|
11
|
+
const fullName = user ? \`\${user.firstName} \${user.lastName}\` : '';
|
|
12
|
+
|
|
13
|
+
// 3. Effects
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
fetchUser(userId).then(setUser).finally(() => setLoading(false));
|
|
16
|
+
}, [userId]);
|
|
17
|
+
|
|
18
|
+
// 4. Handlers
|
|
19
|
+
const handleLogout = () => { /* ... */ };
|
|
20
|
+
|
|
21
|
+
// 5. Early returns
|
|
22
|
+
if (loading) return <Skeleton />;
|
|
23
|
+
if (!user) return <NotFound />;
|
|
24
|
+
|
|
25
|
+
// 6. Main render
|
|
26
|
+
return <div>...</div>;
|
|
27
|
+
}
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
## Custom Hooks
|
|
31
|
+
\`\`\`tsx
|
|
32
|
+
// ✅ Extract reusable logic into custom hooks
|
|
33
|
+
function useLocalStorage<T>(key: string, initialValue: T) {
|
|
34
|
+
const [value, setValue] = useState<T>(() => {
|
|
35
|
+
const stored = localStorage.getItem(key);
|
|
36
|
+
return stored ? JSON.parse(stored) : initialValue;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
41
|
+
}, [key, value]);
|
|
42
|
+
|
|
43
|
+
return [value, setValue] as const;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ✅ Data fetching hook
|
|
47
|
+
function useUser(userId: string) {
|
|
48
|
+
const [user, setUser] = useState<User | null>(null);
|
|
49
|
+
const [loading, setLoading] = useState(true);
|
|
50
|
+
const [error, setError] = useState<Error | null>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
fetchUser(userId)
|
|
55
|
+
.then(setUser)
|
|
56
|
+
.catch(setError)
|
|
57
|
+
.finally(() => setLoading(false));
|
|
58
|
+
}, [userId]);
|
|
59
|
+
|
|
60
|
+
return { user, loading, error };
|
|
61
|
+
}
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Performance Optimization
|
|
65
|
+
\`\`\`tsx
|
|
66
|
+
// ✅ Memoize expensive computations
|
|
67
|
+
const sortedItems = useMemo(
|
|
68
|
+
() => items.sort((a, b) => b.score - a.score),
|
|
69
|
+
[items]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// ✅ Memoize callbacks passed to children
|
|
73
|
+
const handleClick = useCallback(
|
|
74
|
+
(id: string) => onSelect(id),
|
|
75
|
+
[onSelect]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ✅ Memoize components receiving object/array props
|
|
79
|
+
const MemoizedList = memo(function List({ items }: Props) {
|
|
80
|
+
return items.map(item => <Item key={item.id} {...item} />);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ✅ Skip re-renders with proper comparison
|
|
84
|
+
const MemoizedCard = memo(Card, (prev, next) => {
|
|
85
|
+
return prev.id === next.id && prev.name === next.name;
|
|
86
|
+
});
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
## Error Boundaries
|
|
90
|
+
\`\`\`tsx
|
|
91
|
+
class ErrorBoundary extends Component<Props, State> {
|
|
92
|
+
state = { hasError: false, error: null };
|
|
93
|
+
|
|
94
|
+
static getDerivedStateFromError(error: Error) {
|
|
95
|
+
return { hasError: true, error };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
99
|
+
logErrorToService(error, info.componentStack);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render() {
|
|
103
|
+
if (this.state.hasError) {
|
|
104
|
+
return <ErrorFallback error={this.state.error} />;
|
|
105
|
+
}
|
|
106
|
+
return this.props.children;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Usage
|
|
111
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
112
|
+
<App />
|
|
113
|
+
</ErrorBoundary>
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Form Handling (react-hook-form)
|
|
117
|
+
\`\`\`tsx
|
|
118
|
+
import { useForm } from 'react-hook-form';
|
|
119
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
120
|
+
|
|
121
|
+
const schema = z.object({
|
|
122
|
+
email: z.string().email(),
|
|
123
|
+
password: z.string().min(8),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function LoginForm() {
|
|
127
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
128
|
+
resolver: zodResolver(schema),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const onSubmit = (data: FormData) => {
|
|
132
|
+
// data is typed and validated
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
137
|
+
<input {...register('email')} />
|
|
138
|
+
{errors.email && <span>{errors.email.message}</span>}
|
|
139
|
+
|
|
140
|
+
<input type="password" {...register('password')} />
|
|
141
|
+
{errors.password && <span>{errors.password.message}</span>}
|
|
142
|
+
|
|
143
|
+
<button type="submit">Login</button>
|
|
144
|
+
</form>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Context API
|
|
150
|
+
\`\`\`tsx
|
|
151
|
+
// ✅ Create typed context with default
|
|
152
|
+
interface AuthContextType {
|
|
153
|
+
user: User | null;
|
|
154
|
+
login: (email: string, password: string) => Promise<void>;
|
|
155
|
+
logout: () => void;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const AuthContext = createContext<AuthContextType | null>(null);
|
|
159
|
+
|
|
160
|
+
// ✅ Custom hook for consuming context
|
|
161
|
+
function useAuth() {
|
|
162
|
+
const context = useContext(AuthContext);
|
|
163
|
+
if (!context) {
|
|
164
|
+
throw new Error('useAuth must be used within AuthProvider');
|
|
165
|
+
}
|
|
166
|
+
return context;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ✅ Provider component
|
|
170
|
+
function AuthProvider({ children }: { children: ReactNode }) {
|
|
171
|
+
const [user, setUser] = useState<User | null>(null);
|
|
172
|
+
|
|
173
|
+
const login = async (email: string, password: string) => {
|
|
174
|
+
const user = await authService.login(email, password);
|
|
175
|
+
setUser(user);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const logout = () => setUser(null);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<AuthContext.Provider value={{ user, login, logout }}>
|
|
182
|
+
{children}
|
|
183
|
+
</AuthContext.Provider>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
## Compound Component Pattern
|
|
189
|
+
\`\`\`tsx
|
|
190
|
+
// ✅ Flexible, composable API
|
|
191
|
+
const Tabs = ({ children, defaultTab }) => {
|
|
192
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
193
|
+
return (
|
|
194
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
195
|
+
{children}
|
|
196
|
+
</TabsContext.Provider>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
Tabs.List = ({ children }) => <div role="tablist">{children}</div>;
|
|
201
|
+
Tabs.Tab = ({ id, children }) => { /* ... */ };
|
|
202
|
+
Tabs.Panel = ({ id, children }) => { /* ... */ };
|
|
203
|
+
|
|
204
|
+
// Usage
|
|
205
|
+
<Tabs defaultTab="settings">
|
|
206
|
+
<Tabs.List>
|
|
207
|
+
<Tabs.Tab id="profile">Profile</Tabs.Tab>
|
|
208
|
+
<Tabs.Tab id="settings">Settings</Tabs.Tab>
|
|
209
|
+
</Tabs.List>
|
|
210
|
+
<Tabs.Panel id="profile"><Profile /></Tabs.Panel>
|
|
211
|
+
<Tabs.Panel id="settings"><Settings /></Tabs.Panel>
|
|
212
|
+
</Tabs>
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
## ❌ DON'T
|
|
216
|
+
- Use useEffect for data fetching (use React Query/SWR)
|
|
217
|
+
- Mutate state directly
|
|
218
|
+
- Use index as key for dynamic lists
|
|
219
|
+
- Create components inside components
|
|
220
|
+
- Overuse useMemo/useCallback (measure first)
|
|
221
|
+
- Put too much in Context (causes re-renders)
|
|
222
|
+
|
|
223
|
+
## ✅ DO
|
|
224
|
+
- Use React Query/SWR for server state
|
|
225
|
+
- Use useState for UI state only
|
|
226
|
+
- Memoize expensive computations: useMemo
|
|
227
|
+
- Extract custom hooks for reusable logic
|
|
228
|
+
- Use Error Boundaries for graceful failures
|
|
229
|
+
- Use react-hook-form for complex forms
|
|
230
|
+
- Split Context by update frequency
|