Прагнозаваемая база для React з дапамою TypeScript, Zustand і Typed Services
Маленькі скелеты на React, TypeScript і Zustand, які выдзеляюць сховышчы даных, типаваныя API-сэрвісы і компоненты, а таксама правілы для структуры, асінхроннага стану і тэставання.
Першы экран новага проекта ўсьмохнуты, але рашэнні, якія стояць за ўсім гэтым — дзе знаходзится стан, як запрашаюцца даны і насколькі строго адбываецца типаванне — вплываюць на всё, што будзе следаваць. Невялікая прыкладная програма „Hello App“ — ідеальны спосаб разрасіць гэтыя пытанні. У этам пошаговым кераванні ствараецца такая програма з викорыстоўванням React, TypeScript і Zustand; паспяшаецца, за чым адпаведае кожны элемент структуры, і базовыя прыемы ператвараюцца ў правіла, якія можна застосавляць па меры росту кодавой базы.
Чаму разам React, TypeScript і Zustand
Кожны з інструментаў рашае адзін узор проблемы:
- React забезпечвае декларатыўны інтерфейс корыстніка, развіты экасистема і моцныя інструменты, а композіцыя дапамагае робіць компоненты зрозумелымі.
- TypeScript выяўляе памылкі ў часа компіляцыі, робіць рефакторынг бяспечным і ператварае параметры компонентаў і структуру хранення данных у самадокументуючыяся контракты.
Пачніце з гэтых трох і рутера, а бібліятэкі дадзейце толькі тады, калі з’явіцца конкрэтная патрэба.
Канстракцыя: храненне, служба і компанент
Прыклад нижэй паказаны як адна стаятка, але на самай працэ ён складаецца з трох файлаў, кожны з якіх виконвае адну задачу. store/counterStore.ts выклікае типаванае храненне Zustand, якое зберагае count, флажок loading, сінхронную дзеянне increment і асінхронную дзеянне loadInitial. services/counterApi.ts абгружае HTTP-запыт за дапамогай типаванай функцыі, якая кідае адзінку у разе некоректнай адпаведзі. App.tsx чытае адзінлівыя значэнні чераз селектары і запускае пачатковае абгружэнне ў эфекте.
Зверніце увагу на тры рэчы. Магазін ніколі не выклікае fetch безпосередна; ён перадае задачу службе, таму шар сеті можна зменіць або імітавацыяваць незалежна. Блок finally гарантуе, што параметр loading будзе скасаваны нават тады, калі запит не выйшоў. А компонент падпісваецца на кожнае поле за дапамогою свайго селектара, таму ён перерысовваецца толькі тады, калі змянюецца значэнне, якое ён фактычна выкарыстоўвае.
// store/counterStore.ts
import { create } from "zustand"
type CounterState = {
count: number
loading: boolean
increment: () => void
loadInitial: () => Promise<void>
}
export const useCounter = create<CounterState>((set, get) => ({
count: 0,
loading: false,
increment: () => set({ count: get().count + 1 }),
loadInitial: async () => {
set({ loading: true })
try {
const value = await fetchInitialCount()
set({ count: value })
} finally {
set({ loading: false })
}
},
}))
// services/counterApi.ts
export type CounterResponse = { value: number }
export async function fetchInitialCount(): Promise<number> {
const res = await fetch("/api/counter")
if (!res.ok) throw new Error("Failed to load")
const data = (await res.json()) as CounterResponse
return data.value
}
// App.tsx
import React, { useEffect } from "react"
import { useCounter } from "./store/counterStore"
export default function App() {
const count = useCounter(s => s.count)
const loading = useCounter(s => s.loading)
const increment = useCounter(s => s.increment)
const loadInitial = useCounter(s => s.loadInitial)
useEffect(() => {
void loadInitial()
}, [loadInitial])
return (
<main>
<h1>Hello App</h1>
<p>{loading ? "Loading..." : `Count: ${count}`}</p>
<button onClick={increment} disabled={loading}>
Increment
</button>
</main>
)
}
Є калькі, які варта выправіць пры перадачы гэтага коду ў рэальны проект. Як окольныя файлы, хранальнік патрабуе явнага імпорту функцыі fetchInitialCount з модуля службы. Функцыя increment чытае текущую значэння за дапамою get(); функцыональная форма set((s) => ({ count: s.count + 1 })) выражае тую ж мету і ўжо є болей распашчытанай практыкай. Нарэшце, у разе невыпаннага запиту процес завантажэння проста завершваецца і відбуваецца новая падача аберанцы, што залічвае неконтрольваную аберанцю, каліколькі эфект выкарыстоўвае void для адхілення ад прагнення; дадзены ў хранальніку поле error і ловленне аберанцы там дазволяе інтерфейсу паказаць корыстнай інформаціі.
Структура, якая застаецца зрозумелай праз рост
Код групавайце па функцыях, а не па типах файлаў. Папка для кожной функцыі, якая мае ў сабе елементы зберагання, типы і інтерфейс, лёгча кераваць, чым верхніяяршковыя каталогі components, utils і services, якія патрабуюць змены ў кожнай правоке. Аднародныя інструменты та система дизайnu маюць своія окремыя модулі. Чырвонейшую порэванацію ляйаўтаў можна пазнакоміцца ў выборе структуры папак у React.
TypeScript як шар кантракта
Спрытваць тыпы як частку публічнага API кожнага модуля. Экспортуваць тыпы, якія патрабуюцься корыстувачамі, а внутрэшнія залічыць прыватнымі. З самага пачатку увёліце параметр strict (які включае noImplicitAny) а таксама строгі настройкі JSX; дадзець строгосць пазней значна складнее. Ожывайце функцыональныя тыпы, такія як Pick, Omit і ReturnType, каб 파생аваныя тыпы заставаліся сінхроннымі, і прызначайце тыпы для селектараў.
Іспытанне Zustand без працягоў
Раздзеліце стан на маленькі сховышчы па домэнах, напрыклад authStore і todosStore; кожны з яных ствараецца за дапамою create. Храніце селектары ў вузкім дыяпазоне: выбіранне аднаго поля запобегае павторным перрэндарам, калі зменяюцца некалязаныя поля, тады як выбіранне аднародзе створанага об’екта ў кожным вызове можа спрычыніць дадатковыя перрэндары, якщо толькі вы не викорыстаеце дапаможнік для паверхневага пораўняння. Zustand не накладае жадных редюсэраў, таму апдэйты застаюцца короткімі і прыемнымі, як толькі вы ствараеце новыя значэнні замест таго, каб мутаваць стан.
Шаблоны компанентаў і форм
Раздзеляйце контэйнеры ад компанентаў для прадстаўлення. Контэйнеры вярбуюцца з хранілішчаў і адпавядаюць за логіку; компаненты для прадстаўлення прымаюць заданыя параметры, застаюцца чыстымі і лёгка прабавляюцца. Для простых форм дастатнія керованыя элементы вводу; для складных форм лёгкая бібліятэка, такая як react-hook-form у поўнай зусімнасці з схемай, якая падтрымае TypeScript, дапамагае падтрымваць адпаведнасць верыфікацыі і типаў. Выкорыстоўвайце React.memo, useMemo і useCallback толькі там, дзе аналіз працы паказвае ўжытк.
Асінхронная праца і пабочныя эфекты
Усі вызванні API трэба размешчаць у сервісе з типаваннем, і нехай сторыны вызываюць гэтыя сервісы, зберагчы толькі тое стан, які патрэбен інтэрфейсу. Следзіце за статусам запыткаў, процэсам заваношчання, падазрэннямі і успехам у сторыне, каб інтэрфейс завжды адбіваў тое, што на самай працэ. Анулюйце застарелыя, дугія запыткі за дапамою AbortController, і зберагчыце ID запытка або маркер свежасці у сторыне, каб старэйшая адпаведнасць не магла перазапісаць новэйшую.
Тэставанне і якасць коду
Елементныя тэсты для основнай бізнес-логікі і селектараў сторыны є дашчавыя і даюць рэзультаты быстра. Спалучыце ESLint з правіламі, якія падтрымваюць TypeScript, і Prettier, і запускайце іх аўтаматычна ў хуку pre-commit. Ствараюце даны для тэстаў і Storybook за дапамою фабрык прыладоў з типаваннем, каб програма зламалася ў часе компіляцыі, калі змянюецца модэль.
Развіцце без перапісву
Дадзіце можлівасці зверху: кожная новая функцыя значыць новую папку, месчык для зберагачвання дадзеных і шлях для ўпрыема, тады калі локалізацыя і тэматыка знаходзяцца у своіх сабе модулях. Калі змянюецца API або модэль дадзеных, нехай кампіляр паказае ўсе нарушанні угоды. Введзіце кэшаванне, нормалізацыю і оптымістычныя апдэйты толькі тады, калі для гэтага є рэальныя патрэбнасці; якщо стан сервера начынае домінаваць над месчыкам для зберагачвання дадзеных, спецыяльная бібліятэка для запрашоўкі дадзеных часта ёсць кращым выборам, чым Zustand.
Ключовыя выводы
- Зберагачвайце месчыкі для дадзеных, сервісы і компаненты ў адзельных, спецыялізаваных модулях, нават у самай маленькай аплікацыі.
- Чытайце стан чераз спецыяльныя селектары, а таксама явна загружайце модэль і вылучайце стан аднойчын.
- Увёліце строгія правіла TypeScript якомна раней і нехай типы дапамагаюць задокументавація і практыкуванню меж модуляў.
- Арганізавайце код па функцыях, дадзіце залежнасці толькі па патрэбе і оптымізавайце пасля аналізу.