Estat del servidor i estat del client a React

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

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 biblioteca gairebé es respon sola; barreja’ls i acabaràs construint a mà allò que una biblioteca et donaria de franc.

Aquest post i la seva continuació sobre invalidació de cache 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 biblioteques en lloc d’una, amb el cache del servidor dins del 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 no 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 que pertany al servidor: un perfil de /users/me, una llista de productes, l’estat d’una comanda. La còpia no és mai res més que 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 tornis a demanar les dades 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 recollida d’escombraries, per descartar allò que ja no mira ningú. Res d’això no és peculiar de React. És la feina que qualsevol client ha de fer amb dades que no són seves.

L’estat del client és l’altra meitat: dades que són completament de l’app. 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. L’app l’ha de conservar; l’estat 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 tornar a demanar, així que no necessita sincronització en segon pla. Les dues no són graus de la mateixa cosa. Compleixen funcions diferents, i una biblioteca pensada per a una és l’eina equivocada per a l’altra.

Què demana cada meitat

L’estat del servidor vol una biblioteca que ja tingui la maquinària. Fes-la a mà i cada feature fa créixer la mateixa bastida: 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 biblioteca d’estat del servidor te’ls dona tots darrere d’un hook. A Redux, aquesta biblioteca é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 el cache, la deduplicació i tot el cicle de càrrega i error hi venen inclosos (el retry i el refetch-on-focus s’activen a part: el wrapper retry al voltant de la teva base query, i refetchOnFocus juntament amb una crida a setupListeners):

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 biblioteca porti el cache i el cicle de vida.

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

L’estat del client, en comparació, no demana gairebé res. Una preferència no té cap servidor amb què comparar-se, així que el que l’app tingui és correcte per definició, i el 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 aquest estat no farà servir mai; fes que un sol store fet a mà faci les dues coses i reconstruiràs a mà la feina de la biblioteca.

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.

Ja ets a Redux. RTK Query és el camí més curt, perquè encaixa la maquinària d’estat del servidor dins de l’store que ja fas anar.

Una app nova sense cap compromís amb Redux. TanStack Query i Zustand són dues eines enfocades que encaixen sense trepitjar-se.

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 té incorporada.

Així que la propera vegada que vagis a agafar 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 features deixen de compartir un codebase. Quan cadascuna s’entrega com la seva pròpia app i es carrega en runtime, quin cache mana deixa de ser una comoditat i es converteix en una cosa que els equips han de coordinar. Aquí és on la sèrie de Module Federation reprèn el fil. Abans d’això, el proper post es queda al costat servidor: 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