Håndtere data i en web-applikasjon
Hvordan holde orden på dataflyten og hvorfor betyr det noe?
Når man bygger web-applikasjoner må man ofte håndtere data som mellomlagres på applikasjons-nivå, side-nivå eller komponent-nivå. Det er flere grunner til at dette er viktig å sette opp riktig fra start:
- Konsekvent - Holde data i applikasjonen i synk med grensesnittet
- Skalerbarhet - kunne holde kontroll over dataflyten mellom komponenter
- Ytelse - sørger for optimalisering for et responsive grensesnitt
Definere et interface for sikre bedre kontroll av data
Ved å benytte seg av TypeScript i prosjekter så kan man lettere holde orden og sikre seg at dataene man håndterer og sender rundt i applikasjonen har den strukturen man forventer. Man får også en bedre opplevelse når man utvikler ved at kode-editoren gir deg hjelp underveis med hint om hvordan en struktur ser ut, og om du har brukt det feil.
import { useState } from 'react';
interface User {
name: string;
age: number;
city: string | undefined;
}
const [user, setUser] = useState<User>({
name: '',
age: 29,
city: undefined,
});
For å oppdatere data må man ta med resten av objektet, før man kan selektivt oppdatere noe av dataen:
/**
* Her må funksjonens signatur følge samme datatype i argumentet som det objektet den vil gjøre endring på.
*/
const updateUserName = (newName: string) => {
setUser({...user, name: newName });
};
Samle kontroll over data der det gir mening
Når du har to komponenter som bruker eller er avhengige av den samme data, bør du vurdere å flytte dataene ut av komponentene, opp til den nærmeste overordnede komponenten, og deretter sende data ned , samt eventuelt en funksjon for å endre tilstanden, til komponentene under i hierarkiet.
import { useState } from 'react';
/**
* Overordnet komponent som har kontroll over dataen og
* funksjonen for å endre den.
*/
const ParentComponents = () => {
const [active, setActive] = useState(false);
function handleState() {
setActive((prev) => !prev)
}
return (
<>
<ChildComponent active={active} handleState={handleState} />
<ChildComponent active={active} handleState={handleState} />
</>
);
};
/**
* Underkompoent som tar imot data og en funksjon for å endre data
*/
const ChildComponent = ({ active, handleState }) => {
return (
<div>
{active && <Badge />}
<button onClick={handleState}>
Activate
</button>
</div>
);
};
Reduser kompleksitet i komponenter som bygger grensesnitt
Hvis en skjema-komponent begynner å inkludere mer enn noen få useState-kall, kan administrasjonen av tilstandslogikken raskt bli for kompleks. Ved å bruke en useReducer-hook kan vi samle tilstandslogikken på ett sted og kun kalle funksjonen for å endre tilstanden i UI-komponenten. Dette gjør at vi kan gjøre skjema komponenten enklere, mindre og lettere å teste.
import { useReducer } from 'react';
interface FeedbackFormState {
rating: number;
feedback: string;
name: string;
nameError: boolean;
email: string;
emailError: boolean;
};
export type Action =
| { type: 'SET_RATING'; payload: number; }
| { type: 'SET_FEEDBACK'; payload: string; }
| { type: 'SET_NAME'; payload: string; }
| { type: 'SET_NAME_ERROR'; payload: boolean; }
| { type: 'SET_EMAIL'; payload: string; }
| { type: 'SET_EMAIL_ERROR'; payload: boolean; };
function reducer(state: FeedbackFormState, action: Action): FeedbackFormState {
switch (action.type) {
case 'SET_RATING':
return { ...state, rating: action.payload };
case 'SET_FEEDBACK':
return { ...state, feedback: action.payload };
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_NAME_ERROR':
return { ...state, nameError: action.payload };
case 'SET_EMAIL':
return { ...state, email: action.payload };
case 'SET_EMAIL_ERROR':
return { ...state, emailError: action.payload };
default:
return state;
}
};
export const useFeedbackForm = () => {
const INITIAL_STATE: FeedbackFormState = {
rating: 0,
feedback: '',
name: '',
nameError: false,
email: '',
emailError: false,
};
return useReducer(reducer, INITIAL_STATE);
};
Global tilstandsdata
Ved å bruke Reacts Context API kan vi lagre tilstand på hvilket som helst nivå i appen vår, og komponenter kan hente tilstand uten å sende props nedover flere nivåer.
import { createContext } from 'react';
import { User } from './types';
interface UserContextProps {
user: User | undefined;
};
export const UserContext = createContext<UserContextProps>({} as UserContextProps);
export const UserContextProvider = ({ children }) => {
// Hent brukerdata
// Vis en midlertidig tilstand mens vi venter på data
// Når dataene kommer tilbake, gjør de tilgjengelig for alle kompoenter under seg
return <UserContext.Provider value={{ user }}>{children}</UserContext.Provider>;
};
Flere veier til mål
Nå har vi sett flere eksempler av håndtering av tilstandsdata i React-applikasjoner. Det er alltid en avveiing når det kommer til valg av metode for håndtering av data i ulike scenario. Jeg liker å begynne med den enkleste form og så lokal som mulig, om jeg ikke veit noe skal være tilgjengelig på tvers av appen, som autentisering eller informasjon om en bruker. Så kan man alltids utvide eller refakturere koden senere om det blir nødvendig. Det er også viktig å kjenne til fordelene og ulempene ved en metode. Ikke bruke React Context API-et i alle scenario, og blindt legge for mye logikk og tilstandsdata tilgjengelig der hvor det kanskje ikke hører hjemme. Bygg opp applikasjonen metodisk, systematisk og med tester så blir det enklere for andre utviklere og deg selv og komme tilbake når du/dere må endre på designet seks måneder senere. Om applikasjonen vokser så kan man se etter biblioteker som Zustand eller ReduxToolKit for mer avansert håndtering av data.