Hopp til hovedinnhold

Radio-knapper krever overraskende mye kode. Det ble ekstra tydelig da jeg jobbet med radio-komponenten i Gnist ✨, designsystemet hos Møller. Gleden var stor da jeg snublet over et mønster som gjorde hele greia dramatisk enklere. Jeg kan avsløre at løsningen var React Context, men brukt på en måte jeg egentlig har oversett.

Radio-knapper ser enkle ut og er overalt, men koden kan virke overflødig og er sårbar for feil

Det hele startet med følgende kodesnutt:

<RadioGroup label="Hvilken type tjeneste vil du bestille?">
    <RadioButton
        label="EU-kontroll"
        value="eu"
        checked={currentService === "eu"}
        onChange={handleChange}
        name="service"
    />
    <RadioButton
        label="Bytte vindusvisker"
        value="wiper"
        checked={currentService === "wiper"}
        onChange={handleChange}
        name="service"
    />
    <RadioButton
        label="Hjulskift"
        value="wheels"
        checked={currentService === "wheels"}
        onChange={handleChange}
        name="service"
    />
</RadioGroup>

Ganske mye gjentagelse, ikke sant?

Årsaken er at input-felt av typen radio trenger en del verdier:

  • label, for å ha en synlig tekst
  • value, som er den unike verdien for det alternativet
  • checked, som viser at en radio-knapp er valgt
  • onChange, for å kunne oppdatere en tilstand (her kontrollert input), og
  • name, som sier hvilken gruppe de tilhører, så bare én er aktiv av gangen

Vi kan jo se at det er to ting som gjentar seg her som har samme verdier: onChange og name. Hva om vi kunne ha definerert dem på ett sted?

Et renere oppsett

Når jeg utvikler en ny komponent i designsystemet, titter jeg ofte på andre designsystemer (som også er et hot tips, ettersom du bør følge etablerte mønstre). Jeg skal innrømme at jeg ofte henter inspirasjon fra Aksel, NAVs designsystem. De har god dokumentasjon og følger gode praksiser til komponenter — som gir mening og støtter universell utforming.

Der fant jeg følgende representasjon (dog byttet litt med mine egne komponenter):

<RadioGroup
    label="Hvilken type tjeneste vil du bestille?"
    
    // 👇 onChange og name ligger nå i RadioGroup
    onChange={handleChange}
    name="service"
    
    // 👇 value i Group holder rede på nåværende verdi
    value={currentService}
>
    <RadioButton 
        label="EU-kontroll" 
        value="eu" 
    />
    <RadioButton 
        label="Bytte vindusvisker" 
        value="wiper" 
    />
    <RadioButton 
        label="Hjulskift" 
        value="wheels" 
    />
</RadioGroup>

Wow. Vakkert.

Med ett sted å definere name, kan jeg plutselig slippe irriterende bugs som at den ene inputen ikke fungerer. Også nyter jeg jo alltid å ha mindre kode å skrive, og mindre kode å lese — som jo er en bonus når du utvikler en komponent til et komponentbibliotek, som mange skal bruke. Så hvordan få til denne forenklingen?

Vi vet at RadioButton alltid kommer til å brukes sammen med RadioGroup. Hvordan kan vi gi RadioButton props fra sin parent, uten at de er videresendt direkte?

Lokal kontekst

Etter å ha smygtittet inn i Aksels kildekode, oppdaga jeg at forenklingen er oppnådd med en React Context (senere har jeg oppdaga at også material UI har lignende implementasjon).

Likt som at vi kan pakke inn hele appen i en darkmode- kontekst for å tilgjengeliggjøre en verdi, kan vi også pakke inn mindre deler eller enkeltkomponenter. En fun fact er at Facebook har 140 lag med kontekster.

Vi kan bruke React Context ved å ta fellesvariablene vi får fra RadioGroup og mate de inn i en kontekst. Konteksten pakkes rundt children, altså der vi forventer RadioButtons:

export function RadioGroup({
    label,
    children,
    name,
    value,
    onChange,
    ...props
}: RadioGroupProps) {
    return (
        <fieldset {...props}>
            <legend>{label}</legend>
            
            {/* 👇 Context tilgjengeliggjør verdier til children */}
            <RadioGroupContext
            
                {/*
                    👇 Sender verdier satt i RadioGroup
                    ned til RadioButtons via Context
                */}
                value={{
                    name: name,
                    value: value,
                    onChange: onChange,
                }}
            >
                {children}
            </RadioGroupContext>
        </fieldset>
    );
}

Og så konsumerer vi konteksten, så vi får verdiene fra RadioButton. Til dette kan vi lage en hook som gjør det litt lettere å hente verdier:

const RadioGroupContext =
    createContext<RadioGroupContextProps | null>(null);

// 👇 Konsumerer konteksten og returnerer verdiene
export function useRadio() {
    const context = use(RadioGroupContext);
    if (!context) {
        throw new Error(
            "RadioButton must be used within a RadioGroup component",
        );
    }
    return context;
}

Så tar vi i bruk verdiene i RadioButton:

export function RadioButton({ label, value, ...props }: Props) {
    // 👇 Henter verdiene fra context
    const { onChange, name, value: currentValue } = useRadio();
    
    /*
    👇 Impliserer om checked skal bli satt utifra
      verdi fra parent og fra RadioButton
    */
    const isChecked = currentValue === value;
    return (
        <label>
            {label}
            <input
                type="radio"
                name={name}
                onChange={(e) => onChange(e.target.value)}
                value={value}
                checked={isChecked}
                {...props}
            />
        </label>
    );
}

Nå kan vi slippe å eksplisitt sette onChange, name og checkedRadioButton. De blir heller sendt ned fra konteksten (og checked blir implisert basert på nåværende verdi).

Andre bruksområder enn det globale

React Context trenger altså ikke å være kun for de store, globale tingene, men også for små komponenter der vi vet parent og child kommer til å brukes sammen. I dette tilfellet ga det et renere API, færre muligheter for feil og, kanskje viktigst, en komponent som er lettere å bruke.

PS. Dette mønsteret kalles "compound components" (takk for innspill, Aurora Scharff 💡). Compound components innebærer ikke nødvendigvis at React Context er brukt, men er når komponenter er beregnet å bli brukt sammen — som her at RadioGroup holder rede på tilstand for RadioItem. Ofte ser du compound components når komponenter eksporterer en hovedkomponent Card, og du kan bygge opp komponenten med Card.Header, Card.Body og Card.Footer.

Liker du innlegget?

Del gjerne med kollegaer og venner