Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Generate repeated async hooks from a typed factory instead of copying boilerplate
When several hooks share the same fetch/map/error lifecycle, expose a generic `createXHook(config)` factory and define each concrete hook as a small typed config object.
Bad example
| 1 | // Copy-pasted per feature, each with its own debounce/abort/error bugs to keep in sync. |
| 2 | const useJournalistSearch = () => { |
| 3 | const [rows, setRows] = useState<JournalistRow[]>([]); |
| 4 | const search = useCallback(async (q: string) => { |
| 5 | const res = await fetch(`/api/myfa/authors/search?query=${q}`); |
| 6 | if (!res.ok) { setError('Unable to search for journalists.'); return; } |
| 7 | setRows(mapAuthorSearchResponseToRows(await res.json())); |
| 8 | }, []); |
| 9 | return { rows, search }; |
| 10 | }; |
| 11 | // ...and a near-identical useInstrumentSearch, usePersonSearch, etc. |
Explanation (EN)
Each hand-rolled search hook re-implements the same lifecycle: build the URL, check `res.ok`, parse, map, set error. They drift — one debounces, one aborts stale requests, one forgets error handling — and fixing a bug means editing N copies.
Objašnjenje (HR)
Svaki rucno pisani search hook iznova implementira isti zivotni ciklus: izgradi URL, provjeri `res.ok`, parsiraj, mapiraj, postavi gresku. Razilaze se — jedan debounce-a, jedan prekida zastarjele zahtjeve, jedan zaboravi obradu gresaka — a popravak buga znaci uredivanje N kopija.
Good example
| 1 | // One factory owns the lifecycle, generic over the row type. |
| 2 | export function createSearchHook<Row>(config: { |
| 3 | endpoint: string; |
| 4 | mapResponse: (payload: unknown) => Row[]; |
| 5 | errorMessage: string; |
| 6 | }) { |
| 7 | return () => { /* shared fetch + abort + error + map, returns { rows, search } */ }; |
| 8 | } |
| 9 |
|
| 10 | // Each concrete hook is a typed one-liner. |
| 11 | export const useJournalistSearch = createSearchHook<JournalistRow>({ |
| 12 | endpoint: '/api/myfa/authors/search', |
| 13 | mapResponse: mapAuthorSearchResponseToRows, |
| 14 | errorMessage: 'Unable to search for journalists.', |
| 15 | }); |
Explanation (EN)
The factory centralizes the async lifecycle once; each feature supplies only what differs (endpoint, mapper, message) and is parameterized by its row type. Behavior is identical across searches, bugs are fixed in one place, and adding a new searchable list is a three-line config.
Objašnjenje (HR)
Tvornica centralizira asinkroni zivotni ciklus jednom; svaka znacajka daje samo ono sto se razlikuje (endpoint, maper, poruka) i parametrizirana je svojim tipom retka. Ponasanje je identicno za sve pretrage, bugovi se popravljaju na jednom mjestu, a dodavanje nove pretrazive liste je konfiguracija od tri retka.
Notes (EN)
Pairs well with the boundary-validation rule: the `mapResponse` passed in is exactly the `unknown -> Row[]` guard.
Bilješke (HR)
Dobro se uparuje s pravilom validacije granice: proslijedeni `mapResponse` je upravo `unknown -> Row[]` guard.
Exceptions / Tradeoffs (EN)
Don't pre-build the factory for a single hook — extract it on the second or third near-duplicate, when the shared shape is actually proven.
Iznimke / Tradeoffi (HR)
Ne gradi tvornicu unaprijed za jedan jedini hook — izdvoji je na drugom ili trecem gotovo identicnom slucaju, kad je zajednicki oblik stvarno dokazan.