React statebeheer eenvoudig: scheid server state van client state, volg een paar regels en herken vroeg signalen van toenemende complexiteit.

State is alle data die kan veranderen terwijl je app draait. Dat omvat wat je ziet (een modal is open), wat je aan het bewerken bent (een formulier-draft) en data die je ophaalt (een lijst met projecten). Het probleem is dat al deze dingen allemaal "state" worden genoemd, terwijl ze zich heel verschillend gedragen.
De meeste rommelige apps breken op dezelfde manier: te veel soorten state worden door elkaar gebruikt op dezelfde plek. Een component houdt serverdata, UI-flags, formulier-drafts en afgeleide waarden vast en probeert ze met effects synchroon te houden. Voor je het weet kun je geen eenvoudige vragen beantwoorden zoals "waar komt deze waarde vandaan?" of "wat werkt dit bij?" zonder door meerdere bestanden te zoeken.
Gegenereerde React-apps drijven hier sneller naartoe omdat het makkelijk is om de eerste werkende versie te accepteren. Je voegt een nieuw scherm toe, kopieert een patroon, plakt een bug dicht met nog een useEffect, en nu heb je twee bronnen van waarheid. Als de generator of het team halverwege van richting verandert (lokale state hier, globale store daar), verzamelt de codebase patronen in plaats van voort te bouwen op één aanpak.
Het doel is saai: minder soorten state en minder plekken om te kijken. Als er één duidelijke plek is voor serverdata en één duidelijke plek voor uitsluitend UI-state, worden bugs kleiner en voelen veranderingen minder risicovol.
"Houd het saai" betekent dat je je aan een paar regels houdt:
Een concreet voorbeeld: als een gebruikerslijst van de backend komt, behandel die dan als server state en haal hem op waar hij wordt gebruikt. Als selectedUserId alleen bestaat om een detailpaneel aan te sturen, houd het dan als kleine UI-state dicht bij dat paneel. Het mengen van die twee is hoe complexiteit begint.
De meeste React-stateproblemen beginnen met één verwarring: serverdata behandelen alsof het UI-state is. Scheid ze vroeg en statebeheer blijft rustig, zelfs als je app groeit.
Server state behoort toe aan de backend: users, orders, tasks, permissions, prijzen, feature flags. Het kan veranderen zonder dat je app iets doet (een ander tabblad werkt het bij, een admin wijzigt het, een job draait, data verloopt). Omdat het gedeeld en veranderlijk is, heb je fetching, caching, refetching en error handling nodig.
Client state is wat alleen jouw UI op dit moment interesseert: welke modal open is, welke tab geselecteerd is, een filter-toggle, sorteervolgorde, een ingeklapt zijpaneel, een concept zoekquery. Als je het tabblad sluit, is het prima om het te verliezen.
Een snelle test is: "Kan ik de pagina verversen en dit vanaf de server herbouwen?"
Er is ook afgeleide state, wat je ervan weerhoudt extra state aan te maken. Het is een waarde die je uit andere waarden kunt berekenen, dus je slaat het niet op. Gefilterde lijsten, totalen, isFormValid en "toon empty state" horen hier meestal bij.
Voorbeeld: je haalt een lijst met projecten op (server state). De geselecteerde filter en de "New project"-dialog zijn client state. De zichtbare lijst na filteren is afgeleide state. Als je de zichtbare lijst apart opslaat, raakt die uit sync en ga je achter bugs aan als "waarom is dit verouderd?".
Deze scheiding helpt wanneer een tool zoals Koder.ai schermen snel genereert: houd backend-data in één fetch-laag, houd UI-keuzes dicht bij de componenten en voorkom dat je berekende waarden opslaat.
State wordt pijnlijk wanneer één stukje data twee eigenaren heeft. De snelste manier om het simpel te houden is beslissen wie wat bezit en je daaraan houden.
Voorbeeld: je haalt een lijst met users en toont details wanneer er één geselecteerd is. Een veelgemaakte fout is het volledige geselecteerde gebruikersobject in state opslaan. Sla in plaats daarvan selectedUserId op. Houd de lijst in de servercache. De detail-view kijkt de gebruiker op via het ID, dus refetches werken de UI bij zonder extra sync-code.
In gegenereerde React-apps is het ook makkelijk om "helpful" gegenereerde state te accepteren die serverdata dupliceert. Als je code ziet die doet fetch -> setState -> edit -> refetch, pauzeer even. Dat is vaak een teken dat je een tweede database in de browser bouwt.
Server state is alles wat op de backend leeft: lijsten, detailpagina's, zoekresultaten, permissies, counts. De saaie aanpak is één tool te kiezen en je daaraan te houden. Voor veel React-apps is TanStack Query voldoende.
Het doel is simpel: componenten vragen om data, tonen loading- en error-states, en geven niet om hoeveel fetch-calls er onderliggend gebeuren. Dit is belangrijk in gegenereerde apps omdat kleine inconsistenties snel vermenigvuldigen naarmate er meer schermen bijkomen.
Behandel query keys als een naamgevingssysteem, niet als een bijzaak. Houd ze consistent: stabiele array-keys, include alleen inputs die het resultaat veranderen (filters, pagina, sort), en geef de voorkeur aan een paar voorspelbare vormen boven veel losse uitzonderingen. Veel teams zetten key-building ook in kleine helpers zodat elk scherm dezelfde regels gebruikt.
Voor writes: gebruik mutations met expliciete success-handling. Een mutation moet twee vragen beantwoorden: wat is er veranderd, en wat moet de UI daarna doen?
Voorbeeld: je maakt een nieuwe taak aan. Bij succes kun je ofwel de tasks-list query invalidaten (zodat die één keer opnieuw laadt) of een gerichte cache-update doen (voeg de nieuwe taak toe aan de gecachte lijst). Kies per feature één aanpak en houd je daaraan.
Als je de neiging voelt om op meerdere plekken refetch-calls toe te voegen "voor de zekerheid", kies één saaie zet in plaats daarvan:
Client state is wat de browser bezit: een zijbalk-open-flag, een geselecteerde rij, filtertekst, een concept voordat je opslaat. Houd het dicht bij waar het wordt gebruikt en het blijft meestal beheersbaar.
Begin klein: useState in de dichtstbijzijnde component. Wanneer je schermen genereert (bijvoorbeeld met Koder.ai), is het verleidelijk om alles in een globale store te duwen "voor het geval dat". Zo eindig je met een store die niemand begrijpt.
Verplaats state omhoog alleen wanneer je het delingsprobleem kunt benoemen.
Voorbeeld: een tabel met een details-paneel kan selectedRowId in de tabelcomponent houden. Als een toolbar op een andere plek van de pagina het ook nodig heeft, lift je het naar de pagina-component. Als een aparte route (zoals bulk edit) het nodig heeft, kan een kleine store zinvol zijn.
Als je een store (Zustand of vergelijkbaar) gebruikt, houd het dan gefocust op één taak. Sla op wat (geselecteerde ID's, filters), niet resultaten (gesorteerde lijsten) die je kunt afleiden.
Wanneer een store begint te groeien, vraag jezelf: is dit nog steeds één feature? Als het eerlijke antwoord "beetje" is, split het dan nu, voordat de volgende feature het in een kluwen verandert die je niet durft aan te raken.
Form-bugs komen vaak voort uit het mixen van drie dingen: wat de gebruiker typt, wat de server heeft opgeslagen, en wat de UI laat zien.
Voor saai statebeheer behandel je het formulier als client state totdat je het indient. Serverdata is de laatst opgeslagen versie. Het formulier is een draft. Bewerk het serverobject niet in-place. Kopieer waarden naar draft-state, laat de gebruiker vrij wijzigen en submit, en refetch (of update cache) bij succes.
Bepaal vroeg wat moet blijven wanneer de gebruiker navigeert. Die ene keuze voorkomt veel onverwachte bugs. Inline edit-modus en geopende dropdowns moeten meestal resetten, terwijl een lange wizard-draft of een ongestuurde bericht-draft mogelijk persistent blijft. Persistentie over een reload heen alleen als gebruikers het duidelijk verwachten (zoals bij een checkout-formulier).
Houd validatieregels op één plek. Als je regels verspreid zijn over inputs, submit-handlers en helpers, krijg je mismatched errors. Geef de voorkeur aan één schema (of één validate()-functie) en laat de UI beslissen wanneer fouten worden getoond (on change, on blur of on submit).
Voorbeeld: je genereert een Edit Profile-scherm in Koder.ai. Laad het opgeslagen profiel als server state. Maak draft-state voor de formuliervelden. Toon "unsaved changes" door draft vs saved te vergelijken. Als de gebruiker annuleert, gooi de draft en toon de serverversie. Als ze opslaan, dien de draft in en vervang de opgeslagen versie door de serverresponse.
Naarmate een gegenereerde React-app groeit, komt het vaak voor dat dezelfde data op drie plekken leeft: component state, een globale store en een cache. De oplossing is meestal geen nieuwe library. Het is één thuis kiezen voor elk stuk state.
Een cleanup-flow die in de meeste apps werkt:
filteredUsers als je het kunt berekenen uit users + filter. Geef de voorkeur aan selectedUserId boven een gedupliceerd selectedUser object.Voorbeeld: een Koder.ai gegenereerde CRUD-app begint vaak met een useEffect-fetch plus een globale store-kopie van dezelfde lijst. Nadat je server state centraliseert, komt de lijst uit één query en wordt "refresh" invalidatie in plaats van handmatige synchronisatie.
Voor naamgeving, houd het consistent en saai:
users.list, users.detail(id)ui.isCreateModalOpen, filters.userSearchopenCreateModal(), setUserSearch(value)users.create, users.update, users.deleteHet doel is één bron van waarheid per ding, met duidelijke grenzen tussen server state en client state.
State-problemen beginnen klein, en op een dag verander je een veld en drie delen van de UI oneens over de "echte" waarde.
Het duidelijkste waarschuwingssignaal is gedupliceerde data: dezelfde user of winkelwagen leeft in een component, een globale store en een request cache. Elke kopie werkt op een ander moment bij, en je voegt meer code toe alleen om ze gelijk te houden.
Een ander signaal is sync-code: effects die state heen en weer duwen. Patronen zoals "when query data changes, update the store" en "when the store changes, refetch" kunnen werken totdat een edge-case verouderde waarden of loops triggert.
Een paar snelle rode vlaggen:
needsRefresh, didInit, isSaving die niemand verwijdert.Voorbeeld: je genereert een dashboard in Koder.ai en voegt een Edit Profile-modal toe. Als profieldata in een query cache zit, gekopieerd wordt naar een globale store en gedupliceerd is in lokale form-state, heb je nu drie bronnen van waarheid. Zodra je achtergrond-refetching of optimistic updates toevoegt, komen mismatches naar boven.
Als je deze signalen ziet, is de saaie keuze om één eigenaar per stuk data te kiezen en de spiegels te verwijderen.
Opslaan "voor het geval" is één van de snelste manieren om state pijnlijk te maken, vooral in gegenereerde apps.
API-responses kopiëren naar een globale store is een veelvoorkomende valkuil. Als data van de server komt (lijsten, details, user profile), kopieer het dan niet standaard naar een client store. Kies één thuis voor serverdata (meestal de query cache). Gebruik de client store voor UI-only waarden die de server niet kent.
Het opslaan van afgeleide waarden is een andere valkuil. Counts, gefilterde lijsten, totalen, canSubmit en isEmpty moeten meestal worden berekend uit inputs. Als performance een echt probleem wordt, memoize dan later, maar begin niet met het opslaan van het resultaat.
Een enkele mega-store voor alles (auth, modals, toasts, filters, drafts, onboarding flags) wordt een dumpplaats. Split op feature-grenzen. Als state maar door één scherm wordt gebruikt, houd het lokaal.
Context is geweldig voor stabiele waarden (theme, current user id, locale). Voor snel veranderende waarden kan het brede rerenders veroorzaken. Gebruik Context voor wiring, en component state (of een kleine store) voor vaak veranderende UI-waarden.
Tot slot: vermijd inconsistente naamgeving. Bijna-duplicaat query keys en store-velden creëren subtiele duplicatie. Kies een eenvoudige standaard en volg die.
Begin met elk stuk state te labelen als server, client (UI) of derived.
isValid).Nadat je ze gelabeld hebt, zorg dat elk item één duidelijke eigenaar heeft (query cache, lokale component state, URL of een kleine store).
Gebruik deze eenvoudige test: “Kan ik de pagina verversen en dit opnieuw opbouwen vanaf de server?”
Voorbeeld: een projectlijst is server state; het geselecteerde rij-ID is client state.
Omdat het twee bronnen van waarheid creëert.
Als je users ophaalt en ze vervolgens kopieert naar useState of een globale store, moet je ze tijdens het bijwerken synchroniseren met:
Standaardregel: en maak alleen lokale state voor UI-only zorgen of drafts.
Sla afgeleide waarden alleen op wanneer je ze echt niet goedkoop kunt berekenen.
Meestal bereken je ze uit bestaande inputs:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingAls performance écht een probleem is (gemeten), gebruik eerst of betere datastructuren voordat je extra opgeslagen state introduceert die stale kan worden.
Standaard: gebruik een server-state tool (vaak TanStack Query) zodat componenten gewoon ‘om data kunnen vragen’ en loading/error states tonen.
Praktische basisregels:
Houd het lokaal totdat je een echte reden voor delen kunt noemen.
Promotie-regel:
Dit voorkomt dat je globale store verandert in een dumpplaats voor willekeurige UI-flags.
Sla IDs en kleine flags op, niet volledige serverobjecten.
Voorbeeld:
selectedUserIdselectedUser (gekopieerd object)Lookup en render de user uit de gecachte lijst/detail query. Zo werken achtergrond refetches en updates correct zonder extra synchronisatie-effects.
Behandel het formulier als een draft (client state) totdat je het indient.
Een praktisch patroon:
Dit voorkomt dat je per ongeluk serverdata ‘in place’ bewerkt en vecht met refetches.
Veelvoorkomende rode vlaggen:
needsRefresh, didInit, isSaving die blijven opstapelen.Gegenereerde schermen kunnen snel in gemixte patronen belanden. Een eenvoudige safeguard is eigendom standaardiseren:
Als je Koder.ai gebruikt, gebruik Planning Mode om eigendom te bepalen voordat je nieuwe schermen genereert en vertrouw op snapshots/rollback bij experimenten zodat je makkelijk terug kunt als een patroon fout gaat.
useMemoVermijd het verspreiden van refetch()-aanroepen overal “om het zeker te weten.”
De oplossing is meestal geen nieuwe library—verwijder duplicaten en wijs één eigenaar per waarde toe.