Estat del servidor i estat del client a React
Tornar al blog

Estat del servidor i estat del client: per què les apps de React necessiten dues llibreries

Gairebé totes les apps de React Native tenen dues menes d’estat que no s’assemblen en res: l’estat del servidor i l’estat del client. Distingeix-los i la tria de llibreria gairebé es respon sola; barreja’ls i acabaràs construint a mà allò que una llibreria et donaria de franc.

Aquests dos posts són una pausa breu de la sèrie de Module Federation, perquè la feina federada que ve es recolza justament en aquesta distinció. És més fàcil tenir-la clara en una app senzilla primer.

Dos aparellaments aguanten bé a mesura que una app creix. Amb Redux Toolkit i RTK Query mantens un sol store, però l’estat del servidor i l’estat del client els gestionen dues eines diferents dins seu: RTK Query és una capa d’estat del servidor feta a mida, amb tota la maquinària de cache inclosa, i l’estat del client es queda en slices normals de createSlice. Amb TanStack Query i Zustand ho reparteixes en dues llibreries en lloc d’una, amb la cache del servidor vivint en el client propi de TanStack, fora de qualsevol store, i l’estat del client en un petit store de Zustand. Sigui com sigui, les dues menes reben un tractament diferent i mai les forces a passar per una sola. La drecera que ho desfà és un sol store d’ús general, un thunk per petició, i lògica de cache feta a mà. Va bé en una app petita i es converteix en una segona feina a mesura que creix.

Dues menes d’estat

L’estat del servidor és una còpia local d’una cosa de la qual el servidor és propietari: un perfil de /users/me, una llista de productes, l’estat d’una comanda. La còpia només és mai una instantània, i l’original pot canviar l’instant després que el llegeixis. Aquest sol fet és d’on surt tota la feina. Una còpia de dades remotes necessita cache, perquè no refetchis a cada muntatge; deduplicació, perquè dos components que demanen el mateix recurs disparin una sola petició; refetch en segon pla, perquè la pantalla es refresqui en silenci; retry, per la xarxa que cau; invalidació, perquè una escriptura pugui marcar dades relacionades com a caducades; i recol·lecció d’escombraries, per descartar allò que ja no mira ningú. Res d’això és peculiar de React. És el que qualsevol client deu a dades de les quals no és propietari.

L’estat del client és l’altra meitat: dades de les quals l’app és propietària del tot. El compte seleccionat, un ordre d’ordenació, l’esborrany d’un formulari, si un modal està obert, els tokens d’autenticació que tens després del login (on els guardes de forma segura és un tema a part), el tema i l’idioma de l’usuari. S’ha de tenir, ha de ser reactiu, i de tant en tant ha de sobreviure a un reinici. Aquesta és gairebé tota la llista. No pot caducar, així que no necessita cache; no hi ha res a refetchar, així que no necessita sincronització en segon pla. Les dues no són graus de la mateixa cosa. Tenen feines diferents, i una llibreria feta per una és l’eina equivocada per l’altra.

Què demana cada meitat

L’estat del servidor vol una llibreria que ja tingui la maquinària. Fes-la a mà i cada funcionalitat fa créixer el mateix bastiment: una petició, un flag de càrrega, un camp d’error, i una branca per cadascun dels tres estats en què pot estar un fetch. Una llibreria d’estat del servidor te’ls dóna tots darrere d’un hook. A Redux, aquesta llibreria és 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;

El component llegeix una sola línia, i la cache, la deduplicació, el retry i el refetch-on-focus vénen amb ella:

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

Fora de Redux, TanStack Query fa la mateixa feina amb la mateixa forma: declara la query, deixa que la llibreria porti la cache i el cicle de vida.

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

L’estat del client demana gairebé res en comparació. Una preferència no té cap servidor amb què comparar-se, així que el que sigui que l’app tingui és correcte per definició, i la cache o el refetch estarien resolent un problema que no té. Un petit store és suficient. Amb Zustand:

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

Cap reducer, cap dispatch, cap cicle de vida. A Redux la mateixa cosa és un createSlice normal amb un parell de setters. Sigui com sigui, guarda un valor i deixa que els components hi reaccionin. L’error en totes dues direccions és el mateix: posa estat del client en una eina d’estat del servidor i carregaràs amb maquinària que mai fa servir; fes que un sol store fet a mà faci les dues coses i reconstruiràs a mà la feina de la llibreria.

Quin repartiment, i quan

No hi ha cap guanyador universal entre els dos aparellaments, i desconfiaria de qualsevol post que te’n donés un. Si el teu equip ja viu a Redux, RTK Query és el camí més curt, perquè deixa caure la maquinària d’estat del servidor dins del store que ja fas anar. Si arribes a una app nova sense cap compromís amb Redux, TanStack Query i Zustand són dues eines enfocades que encaixen sense trepitjar-se. I en una app petita, un sol store amb un thunk per petició és un lloc perfectament raonable per començar. Estigues atent al dia que et trobis escrivint lògica de cache a mà: aquest és el senyal per agafar una eina que ja la porta inclosa.

Així que abans que la propera vegada agafis un slice per guardar una resposta d’una API, atura’t primer a la pregunta més petita: necessites maquinària d’estat del servidor, o només un lloc on guardar un valor que uns quants components puguin mirar?

La pregunta només es torna més afilada un cop les funcionalitats deixen de compartir un codebase. Quan cadascuna s’entrega com la seva pròpia app i es carrega en runtime, quina cache mana deixa de ser una comoditat i es converteix en una cosa que has de coordinar. Aquí és on la sèrie de Module Federation reprèn, i és molt més fàcil de raonar un cop domines la versió d’app senzilla.

Abans d’això, el proper post es queda a la meitat del servidor i mira una sola diferència entre aquestes eines: com les cache tags de RTK Query i les query keys de TanStack gestionen la mateixa invalidació, i per què aquesta petita diferència d’API decideix com aguanta cadascuna un cop més d’un equip hi està fent commits.

Warren de Leon
Warren de Leon

Software Engineering Manager. Recentment he liderat l'equip de Mobile Platform a Hargreaves Lansdown. Escric sobre lideratge tècnic, React Native i com construir bons equips.

Veure perfil