Server state at client state sa React

Server state at client state: bakit dalawang library ang kailangan ng React apps

Halos bawat React Native app ay may hawak na dalawang uri ng state na magkaiba ang ugali: server state at client state. Kapag napag-iba mo sila, halos sasagot na mismo ang pili mong library. Kapag pinaghalo mo sila, mauuwi ka sa paggawa nang manu-mano ng bagay na ibibigay sana ng library nang libre.

Ang post na ito at ang kasunod nito tungkol sa cache invalidation ay maikling pahinga mula sa Module Federation series, dahil ang paparating na federated na trabaho ay nakasandal mismo sa distinction na ito. Mas madaling maintindihan muna ito sa isang plain app.

May dalawang pairing na matibay habang lumalaki ang app. Sa Redux Toolkit at RTK Query, iisang store lang ang hawak mo, pero ang server at client state ay hinahawakan ng dalawang magkaibang tool sa loob nito: purpose-built na server-state layer ang RTK Query, kasama na ang buong caching machinery, at ang client state ay nananatili sa plain na createSlice slices. Sa TanStack Query at Zustand naman, hinahati mo ito sa dalawang library: nasa sariling client ng TanStack ang server cache, sa labas ng kahit anong store, at ang client state ay nasa maliit na Zustand store. Alinman doon, magkaiba ang handling ng dalawang uri at hindi kailanman pinipilit sa iisang daan. Ang shortcut na sumisira nito ay isang general-purpose store, isang thunk kada request, at cache logic na pinapalago nang manu-mano. Ayos lang ito sa maliit na app at nagiging pangalawang trabaho habang lumalaki.

Dalawang uri ng state

Ang server state ay isang local copy ng bagay na hawak ng server: profile mula sa /users/me, product list, order status. Snapshot lang palagi ang copy, at puwedeng magbago ang original kaagad matapos mong basahin. Doon galing ang lahat ng trabaho. Ang copy ng remote data ay nangangailangan ng caching, para hindi ka mag-refetch kada mount; deduplication, para magpadala lang ng iisang request ang dalawang component na humihingi ng parehong resource; background refetching, para tahimik na mag-refresh ang screen; retry, para sa network na nawawala; invalidation, para ang isang write ay makapag-mark ng related data bilang stale; at garbage collection, para tanggalin ang walang nagbabantay. Wala sa mga ‘yan ang exclusive sa React. ‘Yan ang trabahong kailangang gawin ng kahit anong client para sa data na hindi sa kanya.

Ang client state ang kabilang kalahati: data na buong pag-aari ng app. Ang piniling account, sort order, form draft, kung bukas ba ang modal, ang auth tokens na hawak mo pagkatapos mag-login (kung saan mo sila ligtas na itatago ay sariling paksa), ang theme at wika ng user. Kailangan itong hawakan, kailangang maging reactive ito, at paminsan-minsan kailangang makaligtas sa restart. Halos ‘yan na ang buong listahan. Hindi ito puwedeng mag-stale, kaya walang caching na kailangan; walang ire-refetch, kaya walang background sync na kailangan. Hindi sila magkaibang antas ng iisang bagay. Magkaiba ang trabaho nila, at ang library na ginawa para sa isa ay maling tool para sa isa pa.

Ano ang hinihingi ng bawat kalahati

Kailangan ng server state ang library na may handa nang machinery. Gawin mo ito nang manu-mano at magpapalaki ang bawat feature ng parehong scaffolding: isang request, isang loading flag, isang error field, at isang branch para sa bawat isa sa tatlong state na puwedeng kalagayan ng fetch. Lahat ng ‘yan, ibinibigay ng server-state library sa likod ng iisang hook. Sa Redux, ang library na ‘yan ay RTK Query:

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: builder => ({
    getProfile: builder.query<Profile, void>({
      query: () => '/users/me',
    }),
  }),
});

export const { useGetProfileQuery } = api;

Isang linya lang ang binabasa ng component, at kasama na roon ang caching, deduplication at ang buong loading-at-error lifecycle (opt-in ang retry at refetch-on-focus: ang retry wrapper sa palibot ng base query mo, at refetchOnFocus kasama ang isang setupListeners call):

const { data: profile, isLoading, error } = useGetProfileQuery();

Sa labas ng Redux, ginagawa ng TanStack Query ang parehong trabaho sa parehong hugis: i-declare ang query, hayaang patakbuhin ng library ang cache at ang lifecycle.

export const useProfile = () =>
  useQuery({ queryKey: ['profile'], queryFn: getProfile });

Halos wala namang hinihingi ang client state kumpara riyan. Ang preference ay walang server na ikukumpara, kaya kahit anong hawak ng app ay tama by definition, at walang problemang sinusolusyunan ang caching o refetching dito. Sapat na ang maliit na store. Sa Zustand:

export const useSettings = create<Settings>()(set => ({
  theme: 'system',
  setTheme: theme => set({ theme }),
}));

Walang reducer, walang dispatch, walang lifecycle. Sa Redux, ang parehong bagay ay isang plain na createSlice na may ilang setter. Alinman doon, humahawak ito ng value at hinahayaang mag-react dito ang mga component. Parehas ang mali sa magkabilang direksyon: ilagay mo ang client state sa server-state tool at maiiwan sa’yo ang machinery na hindi nito ginagamit; pagawain mo ang isang hand-rolled na store ng dalawang trabaho at itatayo mo ulit nang manu-mano ang ginagawa ng library.

Aling hati, at kailan

Walang universal na panalo sa dalawang pairing, at hindi ako magtitiwala sa kahit anong post na magbibigay sa ‘yo ng isa.

Nasa Redux ka na. Mas maikli ang daan ng RTK Query, dahil ibinababa nito ang server-state machinery sa store na pinapatakbo mo na.

Bagong app na walang Redux commitment. Ang TanStack Query at Zustand ay dalawang focused na tool na magkasamang gumagana nang hindi nag-aapakan sa isa’t isa.

Maliit na app. Ang isang store na may thunk kada request ay isang lubos na makatwirang simula. Bantayan ang araw na mahuhuli mo ang sarili mong nagsusulat ng cache logic nang manu-mano: ‘yan ang senyas para kumuha ka na ng tool na kasama na ito.

Kaya sa susunod na kukuha ka ng slice para paglagyan ng API response, itanong muna ang mas maliit na tanong: kailangan mo ba ng server-state machinery, o isang lalagyan lang ng value na kayang bantayan ng ilang component?

Mas tumatalim ang tanong sa sandaling tumigil ang mga feature sa pagbabahagi ng isang codebase. Kapag bawat isa ay nagshi-ship bilang sariling app at naglo-load sa runtime, kung kaninong cache ang authoritative ay hindi na convenience kundi bagay na kailangang i-coordinate ng mga team. Doon babalik ang Module Federation series. Bago ‘yon, ang susunod na post ay nananatili sa panig ng server: kung paano hinahawakan ng cache tags ng RTK Query at ng query keys ng TanStack ang parehong invalidation, at kung bakit ang maliit na API difference na ‘yon ang nagpapasya kung gaano katibay ang bawat isa kapag higit sa isang team na ang nagko-commit dito.

Warren de Leon
Warren de Leon

Software Engineering Manager. Pinakahuling pinamunuan ang Mobile Platform team sa Hargreaves Lansdown. Sumusulat tungkol sa engineering leadership, React Native, at pagbuo ng magagandang team.

Tingnan ang profile