Галоўная / Артыкулы / Прагнозаваемая база для React з дапамою TypeScript, Zustand і Typed Services

Прагнозаваемая база для React з дапамою TypeScript, Zustand і Typed Services

Маленькі скелеты на React, TypeScript і Zustand, які выдзеляюць сховышчы даных, типаваныя API-сэрвісы і компоненты, а таксама правілы для структуры, асінхроннага стану і тэставання.

1207 слоў

Першы экран новага проекта ўсьмохнуты, але рашэнні, якія стояць за ўсім гэтым — дзе знаходзится стан, як запрашаюцца даны і насколькі строго адбываецца типаванне — вплываюць на всё, што будзе следаваць. Невялікая прыкладная програма „Hello App“ — ідеальны спосаб разрасіць гэтыя пытанні. У этам пошаговым кераванні ствараецца такая програма з викорыстоўванням React, TypeScript і Zustand; паспяшаецца, за чым адпаведае кожны элемент структуры, і базовыя прыемы ператвараюцца ў правіла, якія можна застосавляць па меры росту кодавой базы.

Чаму разам React, TypeScript і Zustand

Кожны з інструментаў рашае адзін узор проблемы:

  • React забезпечвае декларатыўны інтерфейс корыстніка, развіты экасистема і моцныя інструменты, а композіцыя дапамагае робіць компоненты зрозумелымі.
  • TypeScript выяўляе памылкі ў часа компіляцыі, робіць рефакторынг бяспечным і ператварае параметры компонентаў і структуру хранення данных у самадокументуючыяся контракты.
  • Zustand — это невяленькая дзяржаўная бібліятэкা без большых цераманіяў: храненне дадзенаў адбываецца за дапамогай хвостка, модэль дадзенаў — цэ гэта простыя об’екты і функцыі, а компаненты слухаюць толькі тыя данні, якія чытаюць.
  • Пачніце з гэтых трох і рутера, а бібліятэкі дадзейце толькі тады, калі з’явіцца конкрэтная патрэба.

    Канстракцыя: храненне, служба і компанент

    Прыклад нижэй паказаны як адна стаятка, але на самай працэ ён складаецца з трох файлаў, кожны з якіх виконвае адну задачу. 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 якомна раней і нехай типы дапамагаюць задокументавація і практыкуванню меж модуляў.
    • Арганізавайце код па функцыях, дадзіце залежнасці толькі па патрэбе і оптымізавайце пасля аналізу.
  • Захавайце асінхронныя праблемы за дапамою пераканчэння задач і перагляду свежасці дадзейнаў, калі толькі ситуацыя супернагонкі можа парадразіць корыстувальнікаў.