React-tillståndshantering gjort enkelt: separera server state från client state, följ några regler och upptäck tidiga tecken på ökande komplexitet.

State är all data som kan ändras medan din app körs. Det inkluderar vad du ser (en modal är öppen), vad du redigerar (ett formulärutkast) och data du hämtar (en lista projekt). Problemet är att allt detta kallas state, även om de beter sig väldigt olika.
De flesta röriga appar går sönder på samma sätt: för många typer av state blandas på samma ställe. En komponent sitter kvar med serverdata, UI-flaggor, formulärutkast och härledda värden, och försöker hålla dem synkade med effekter. Snart kan du inte svara på enkla frågor som "var kommer det här värdet ifrån?" eller "vad uppdaterar det?" utan att leta i flera filer.
Genererade React-appar glider in i detta snabbare eftersom det är lätt att acceptera den första fungerande versionen. Du lägger till en ny skärm, kopierar ett mönster, lagar en bugg med ännu en useEffect, och nu har du två sanningskällor. Om generatorn eller teamet byter riktning halvvägs (lokalt state här, global store där), samlar kodbasen på sig mönster istället för att bygga vidare på ett.
Målet är tråkigt: färre slags state och färre ställen att titta på. När det finns ett uppenbart hem för serverdata och ett uppenbart hem för endast UI-state blir buggar mindre och ändringar känns inte lika riskfyllda.
"Håll det tråkigt" betyder att du följer några regler:
Ett konkret exempel: om en användarlista kommer från backend, behandla den som server state och hämta den där den används. Om selectedUserId bara finns för att styra en detaljpanel, håll den som litet UI-state nära den panelen. Att blanda de två är hur komplexiteten börjar.
De flesta React-state-problem börjar med en blandning: att behandla serverdata som UI-state. Separera dem tidigt och state-hantering håller sig lugn, även när din app växer.
Server state hör hemma i backend: användare, beställningar, uppgifter, behörigheter, priser, feature flags. Det kan ändras utan att din app gör något (en annan flik uppdaterar det, en admin redigerar, ett jobb körs, data går ut). Eftersom det är delat och förändringsbart behöver du hämtning, caching, refetching och felhantering.
Client state är vad bara ditt UI bryr sig om just nu: vilken modal som är öppen, vilken flik som är vald, en filterväxel, sorteringsordning, en kollapsad sidopanel, ett utkast till sökfråga. Om du stänger fliken är det okej att tappa det.
Ett snabbt test är: "Kan jag uppdatera sidan och återskapa detta från servern?"
Det finns också härlett state, vilket sparar dig från att skapa extra state från början. Det är ett värde du kan räkna fram från andra värden, så du lagrar det inte. Filtrerade listor, totals, isFormValid och "visa tomt tillstånd" hör oftast hit.
Exempel: du hämtar en lista projekt (server state). Den valda fliken och flaggan för att dialogen "Nytt projekt" är öppen är client state. Den synliga listan efter filtrering är härlett state. Om du lagrar den synliga listan separat kommer den att glida ur synk och du kommer jaga buggar som "varför är den föråldrad?".
Den här separationen hjälper när ett verktyg som Koder.ai genererar skärmar snabbt: håll backenddata i ett hämtlager, håll UI-val nära komponenterna och undvik att lagra beräknade värden.
State blir smärtsamt när en datapost har två ägare. Det snabbaste sättet att hålla det enkelt är att bestämma vem som äger vad och hålla sig till det.
Exempel: du hämtar en lista användare och visar detaljer när en är vald. Ett vanligt misstag är att lagra hela det valda användarobjektet i state. Spara selectedUserId istället. Håll listan i server-cachen. Detaljvyn slår upp användaren via ID, så refetchar uppdaterar UI utan extra synk-kod.
I genererade React-appar är det också lätt att acceptera "hjälpsamt" genererat state som duplicerar serverdata. När du ser kod som gör fetch -> setState -> edit -> refetch, pausa. Det är ofta ett tecken på att du bygger en andra databas i webbläsaren.
Server state är allt som lever på backend: listor, detaljsidor, sökresultat, behörigheter, räknare. Det tråkiga tillvägagångssättet är att välja ett verktyg för det och hålla sig till det. För många React-appar räcker TanStack Query.
Målet är enkelt: komponenter ber om data, visar loading och fel, och bryr sig inte om hur många fetch-anrop som körs i bakgrunden. Detta är viktigt i genererade appar eftersom små inkonsekvenser multipliceras snabbt när nya skärmar läggs till.
Behandla query-nycklar som ett namngivningssystem, inte en eftertanke. Håll dem konsekventa: stabila array-nycklar, inkludera bara inputs som förändrar resultatet (filter, sida, sortering), och föredra några förutsägbara former framför många engångsnycklar. Många team lägger också nyckelbyggnad i små hjälpfunktioner så varje skärm använder samma regler.
För skrivningar, använd mutationer med tydlig success-hantering. En mutation bör svara på två frågor: vad ändrades, och vad ska UI göra härnäst?
Exempel: du skapar en ny uppgift. Vid framgång invalidata task-list-queryn (så den laddas om en gång) eller gör en riktad cache-uppdatering (lägg till den nya uppgiften i den cachade listan). Välj en strategi per funktion och håll den konsekvent.
Om du frestas att lägga refetch-anrop på flera ställen "för säkerhets skull", välj en tråkig standard istället:
Client state är det som webbläsaren äger: en sidopanel öppen-flagga, en vald rad, filtertext, ett utkast innan du sparar. Håll det nära där det används så brukar det förbli hanterbart.
Börja litet: useState i närmaste komponent. När du genererar skärmar (till exempel med Koder.ai) är det frestande att pusha allt till en global store "ifall att". Så blir din store till en plats ingen förstår.
Flytta bara state uppåt när du kan namnge delningsproblemet.
Exempel: en tabell med en detaljpanel kan hålla selectedRowId i tabellkomponenten. Om en verktygsrad i en annan del av sidan också behöver det, lyft det till sidkomponenten. Om en separat rutt (som bulkredigering) behöver det, kan en liten store vara rimlig.
Om du använder en store (Zustand eller liknande), håll den fokuserad på en sak. Spara "vad" (valda ID:n, filter), inte "resultat" (sorterade listor) som du kan härleda.
När en store börjar växa, fråga: är detta fortfarande en funktion? Om svaret är "typ av", dela upp den nu innan nästa funktion förvandlar den till en boll av state du är rädd att röra vid.
Formbuggar kommer ofta från att blanda tre saker: vad användaren skriver, vad servern har sparat och vad UI visar.
För tråkig state-hantering, behandla formuläret som client state tills du skickar. Serverdata är den senaste sparade versionen. Formuläret är ett utkast. Redigera inte serverobjektet på plats. Kopiera värden till utkast-state, låt användaren ändra fritt, och skicka sedan och refetcha (eller uppdatera cachen) vid framgång.
Bestäm tidigt vad som ska bestå när användaren navigerar bort. Det valet förhindrar många överraskningsbuggar. Till exempel bör inline-redigeringsläge och öppna dropdowns vanligtvis återställas, medan ett långt wizards-utkast eller ett oskickat meddelande ofta kan persistera. Persistens över reload endast när användare tydligt förväntar sig det (som i en kassa).
Håll valideringsregler på ett ställe. Om du sprider regler över inputs, submit-handlers och hjälpfunktioner kommer du få mismatchade fel. Föredra ett schema (eller en validate()-funktion) och låt UI bestämma när fel visas (on change, on blur eller on submit).
Exempel: du genererar en Edit Profile-skärm i Koder.ai. Ladda den sparade profilen som server state. Skapa utkast-state för formulärfälten. Visa "osparade ändringar" genom att jämföra utkast vs sparat. Om användaren avbryter, släng utkastet och visa serverversionen. Om de sparar, skicka utkastet och ersätt den sparade versionen med serverns svar.
När en genererad React-app växer är det vanligt att samma data hamnar på tre ställen: komponentstate, en global store och en cache. Åtgärden är sällan ett nytt bibliotek. Det är att välja ett hem för varje state.
Ett rensflöde som fungerar i de flesta appar:
filteredUsers om du kan räkna fram det från users + filter. Föredra selectedUserId framför ett duplicerat selectedUser-objekt.Exempel: en Koder.ai-genererad CRUD-app börjar ofta med en useEffect-fetch plus en global store-kopia av samma lista. När du centraliserar server state kommer listan från en query och "refresh" blir invalidation istället för manuell synk.
För namngivning, håll det konsekvent och tråkigt:
users.list, users.detail(id)ui.isCreateModalOpen, filters.userSearchopenCreateModal(), setUserSearch(value)users.create, users.update, users.deleteMålet är en källa till sanningen per sak, med tydliga gränser mellan server state och client state.
State-problem börjar smått, och en dag ändrar du ett fält och tre delar av UI:t är oense om det "riktiga" värdet.
Det tydligaste varningstecknet är duplicerad data: samma användare eller kundvagn finns i en komponent, i en global store och i en request-cache. Varje kopia uppdateras vid olika tidpunkter, och du lägger till mer kod bara för att hålla dem lika.
Ett annat tecken är synk-kod: effekter som pushar state fram och tillbaka. Mönster som "när query-data ändras, uppdatera store" och "när store ändras, refetcha" kan fungera tills ett hörnfall triggar föråldrade värden eller loopar.
Några snabba röda flaggor:
needsRefresh, didInit, isSaving som ingen tar bort.Exempel: du genererar en dashboard i Koder.ai och lägger till en Edit Profile-modal. Om profildata lagras i en query-cache, kopieras in i en global store och dupliceras i lokalt formulärstate, har du nu tre sanningskällor. När du lägger till bakgrundsrefetching eller optimistiska uppdateringar visar sig mismatchar.
När du ser dessa tecken är det tråkiga steget att välja en ägare per dataobjekt och radera speglingarna.
Att lagra saker "ifall att" är ett av snabbaste sätten att göra state smärtsamt, särskilt i genererade appar.
Att kopiera API-svar till en global store är en vanlig fälla. Om data kommer från servern (listor, detaljer, användarprofil), kopiera inte automatiskt in det i en klientstore. Välj ett hem för serverdata (vanligtvis query-cachen). Använd klientstoren för UI-värden som servern inte känner till.
Att lagra härledda värden är en annan fälla. Räknare, filtrerade listor, totals, canSubmit och isEmpty bör vanligtvis beräknas från inputs. Om prestanda blir verkligt problem, memoize senare, men börja inte med att lagra resultatet.
En enda mega-store för allt (auth, modaler, toasts, filter, utkast, onboarding-flaggar) blir ett dumpningsställe. Dela upp efter feature-gränser. Om state bara används av en skärm, håll det lokalt.
Context är utmärkt för stabila värden (theme, current user id, locale). För snabbrörliga värden kan det orsaka breda rerenders. Använd Context för wiring, och komponentstate (eller en liten store) för frekvent ändrande UI-värden.
Slutligen, undvik inkonsekvent namngivning. Nära-duplicerade query-nycklar och store-fält skapar subtil duplication. Välj en enkel standard och följ den.
När du känner lusten att lägga till "bara en state-variabel till", gör en snabb ownership-check.
För det första, kan du peka på ett ställe där serverhämtning och caching händer (ett query-verktyg, en uppsättning query-nycklar)? Om samma data hämtas i flera komponenter och också kopieras in i en store, betalar du redan ränta.
För det andra, behövs värdet bara inom en skärm (som "är filterpanelen öppen")? Om så är fallet bör det inte vara globalt.
För det tredje, kan du spara ett ID istället för att duplicera ett objekt? Spara selectedUserId och läs användaren från din cache eller lista.
För det fjärde, är det härlett? Om du kan räkna fram det från befintligt state, spara det inte.
Slutligen, gör ett enminuts trace-test. Om en kollega inte kan svara "var kommer detta värde ifrån?" (prop, lokalt state, server-cache, URL, store) på under en minut — fixa ägarskapet innan du lägger till mer state.
Föreställ dig en genererad admin-app (till exempel en som producerats från en prompt i Koder.ai) med tre skärmar: kundlista, kunddetaljsida och ett redigeringsformulär.
State håller sig lugnt när det har uppenbara hem:
List- och detaljsidorna läser server state från en query-cache. När du sparar lagrar du inte kunder igen i en global store. Du skickar mutation, och låter cachen uppdateras eller refresha.
För redigeringsskärmen, håll formulärutkastet lokalt. Initiera det från den hämtade kunden, men behandla det separat när användaren börjar skriva. På så sätt kan detaljvyn refreshas säkert utan att skriva över halvfärdiga ändringar.
Optimistisk UI är där team ofta duplicerar allt. Ofta behöver du inte det.
När användaren trycker Spara, uppdatera endast den cachade kundposten och motsvarande listobjekt, och rulla tillbaka om förfrågan misslyckas. Behåll utkastet i formuläret tills sparandet lyckas. Om det misslyckas, visa ett fel och behåll utkastet så användaren kan försöka igen.
Säg att du lägger till bulkredigering och den också behöver valda rader. Innan du skapar en ny store, fråga: ska detta state överleva navigering och reload?
Om ja, lägg det i URL:en (valda ID:n, filter). Om nej, håll det i sidkomponenten. Om flera avlägsna komponenter verkligen behöver det samtidigt (verktygsrad + tabell + footer), inför en liten delad store för just det client-stateet.
Genererade skärmar kan multipliceras snabbt, och det är bra — tills varje ny skärm tar med sig egna state-beslut.
Skriv ner en kort teamanteckning i repot: vad som räknas som server state, vad som räknas som client state, och vilket verktyg som äger vad. Håll det kort så folk faktiskt följer det.
Lägg till en liten PR-vana: märk varje ny state-bit som server eller client. Om det är server state, fråga "var laddas det, hur cachas det, och vad invalidaterar det?" Om det är client state, fråga "vem äger det, och när återställs det?"
Om du använder Koder.ai (koder.ai), kan Planning Mode hjälpa er att komma överens om tillståndsgränser innan ni genererar nya skärmar. En snapshot och rollback ger ett säkert sätt att experimentera när en tilländringsändring går fel.
Välj en funktion (som redigera profil), applicera reglerna genom hela flödet och låt det bli exempel som alla kopierar.
Börja med att märka varje tillståndsbiten som server, client (UI) eller derived.
isValid).När du har märkt dem, se till att varje objekt har en uppenbar ägare (query-cache, lokal komponentstate, URL eller en liten store).
Använd testet: ”Kan jag uppdatera sidan och återskapa detta från servern?”
Exempel: en projektlista är server state; valt rad-ID är client state.
För att det skapar två sanningskällor.
Om du hämtar users och sedan kopierar dem till useState eller en global store, måste du hålla dem synkade vid:
Standardregel: och skapa bara lokalt state för UI-ändamål eller utkast.
Spara härledda värden endast när du verkligen inte kan räkna ut dem billigt.
Vanligtvis bör du beräkna från befintliga inputs:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingOm prestanda verkligen blir ett problem (mätt), föredra eller bättre datastrukturer innan du introducerar mer lagrat state som kan bli föråldrat.
Standard: använd ett server-state-verktyg (vanligtvis TanStack Query) så komponenter bara kan “be om data” och hantera loading/error-states.
Praktiska grunder:
Håll det lokalt tills du kan namnge ett verkligt delningsbehov.
Promotionsregel:
Detta håller din globala store från att bli ett dumpningsställe för slumpmässiga UI-flaggor.
Spara ID:n och små flaggor, inte fulla serverobjekt.
Exempel:
selectedUserIdselectedUser (kopierat objekt)Rendera sedan detaljer genom att slå upp användaren i den cachade listan/detail-queryn. Det gör att bakgrundsrefetchar och uppdateringar beter sig korrekt utan extra synkroniseringskod.
Behandla formuläret som ett utkast (client state) tills du skickar det.
Ett praktiskt mönster:
Det här undviker att du av misstag redigerar serverdata “in place” och kämpar mot refetchar.
Vanliga varningssignaler:
needsRefresh, didInit, isSaving som bara ackumuleras.Genererade skärmar kan snabbt driva in i blandade mönster. Ett enkelt skydd är att standardisera ägarskap:
Om du använder Koder.ai, använd Planning Mode för att bestämma ägarskap innan du genererar nya skärmar, och använd snapshots/rollback när du experimenterar så att du kan ångra om något går fel.
useMemoUndvik att strö refetch() överallt "för säkerhets skull."
Lösningen är ofta att radera speglingar och välja en ägare per värde.