post header image

Az immutability (megváltoztathatatlanság/statikusság) JS és React esetében

Avagy miért is fontos ez a state kezelésben 😊

Helló mindenkinek, a mostani cikkünk olyan témákat feszeget, amelyet akár alapnak is tekinthetnénk, azonban veterán fejlesztők bicskája is bizonyos esetekben ki tud csorbulni tőle (pl.: keveset aludtak a gyerektől vagy a Baldur’s Gate III-tól vagy Helldivers II-től).

Áttekintjük, hogy az immutability mi is általános érvényben, majd state kezelésben ez hogyan is üti fel a fejét és miket kezdhetünk vele.

A cikk végére, reményeink szerint, igazi mestere leszel a témának.

Mi is az immutability?

Az immutability egy szoftverfejlesztési fogalom, amely arra utal, hogy bizonyos adatok vagy objektumok nem változnak meg a létrehozásuk után. Magyarul az immutability olyan tulajdonság, amelynek során az adatok vagy objektumok egyszer létrejönnek, és utána nem változnak meg.

JS-ben az immutability hogyan is jön elő, hol találkozhatok vele?

Kapásból vannak a JS-nek immutable típusai (a primitívek). Dehát a JS-nek nincsenek is típusai mondhatnánk, azonban ennél nagyobbat nem is tévedhetnénk!

Attól még, hogy dinamikusan tipizált nyelv a JS, még van neki típusai, illetve a változónak sem kell megmondani, hogy mi fog majd belekerülni (di-na-mi-kus).

A typeof operátor is elárulja nekünk, hogy melyik érték milyen típusú. A JS primitívek esetében hozzá akarunk férni valamely property-jéhez akkor történik meg az auto-boxing és wrappolja be őket a típus szerinti wrapper osztályba őket, hogy a rájuk jellemző property-k működjenek rajtuk.

Nézzük is meg!

var str = "asd"

// str.toUpperCase() már működne is köszönhetően az auto-boxing-nak

str[1] = "h"

console.log(str) // "asd"

Hiába próbáljuk az értékét változtatni, mert ez sikertelen, az eredeti string nem változik.

Hogyan történhet meg akkor az adatnak a mutálása?

Kezdjük is az alábbi, object-ként leírt gyerkőcömmel.

const baby = {
    ageInMonth: 12,
    weight: 10,
    name: "Alex"
}

A babát azonban a nagymamája úgy hívja, hogy „Hurci” (teljes félreértés az egész, szót sem érdemel). Leírva mindez:

const grandChild = baby

grandChild.name = "Hurci"

console.log(grandChild)

A feleségem amikor hazajön azt tapasztalja, hogy a gyerek neve megváltozott.

console.log(baby.name) // "Hurci"

ÉÉés kezdődik is részéről az értetlenkedés (még akkor is, ha amúgy ő nagyon okos és szép és társai ^^). Pedig semmi másról nincsen szó, minthogy az érték átadás JS-ben nem primitívek esetében referencia alapján történik és akaratlanul mutáltuk a baby nevét. Már rá sem lehet ismerni 😊.

Ez alapján rá is térhetünk a cikkünk következő állomására, hogy mi a helyzet a React state mutálásának esetében.

Hogyan is tudjuk a mutálódást kiküszöbölni?

  1. JSON parse és stringify method használatával

Ahogyan már fentebb is írtuk, a JS primitívek immutable-ök. Szóval logikus választás lehet az adott objektumot először string-gé tenni, majd azt vissza is parse-olni és az eredményt változóba letárolni.

Esetünkben ez a következőképpen néz ki:

const grandChild = JSON.parse(JSON.stringify(baby))

grandChild.name = "Hurci"

console.log(baby.name)

"Alex"-et kapunk ahogyan az el is várt.

  1. Object.assign használatával

Hogy ne referencia alapján kezelje az object-et ezért a kettes és a hármas esetben is létrehozunk egy új object-et, azt letároljuk változóba és azt módosítjuk.

const grandChild = Object.assign({}, baby)

grandChild.name = "Hurci"

console.log(baby.name)

"Alex"-et kapunk ahogyan az el is várt.

Object.assign másolja az első paraméterébe létrehozott üres object-be a második paramétert és ezáltal létre is jött az elvárt hatás, hogy ez egy új object és csak annak változtattuk meg a name property-jét.

  1. Spread operátor használatával

A kettes pontban leírt hatást el lehet érni a spread operátor használatával is.

const grandChild = {...baby}

grandChild.name = "Hurci"

console.log(baby.name)

"Alex"-et kapunk ismét.

Létrehozunk egy új objectet, abba a baby object tartalmát belemásoljuk.

Figyelmeztetés:

Van egy kis baki abban, ahogyan a kettes és hármas pontban láthattuk az object másolását. Ugyan a jelenlegi példánkhoz tökéletes, de tisztában kell lennünk azzal, hogy ez csupán shallow copy-t képez az adott object-ről, ezáltal deeply nested object-e(ke)t képtelen megfelelően másolni/klónozni, szóval csak egy szint mélységig történik meg a folyamat.

És itt jön a képbe a következő két pont.

  1. A structuredClone használatával

A még mindig kvázi új dolognak számító structuredClone() globális funkció segítségével könnyedén lehetséges ezt a problémát kiküszöbölni, deep clone-t hozva létre a megadott objectből.

Lássuk is működésben:

const grandChild = structuredClone(baby)

grandChild.name = "Hurci"

console.log(baby.name)

"Alex"-et kapunk ismét.

Most már a több mélységben lévő adatok is rendben vannak, nincs már referencia hivatkozás a kettő között, új objektumpéldány jött létre.

  1. Immutability lib használata

Hogyha belefér, hogy a JS csomagunk nagyságát kissé növeljük, a csapatunk tagjai pedig a használatát megtanulják, akkor hasznos segítség lehet valamilyen immutability lib alkalmazása is (pl.).

Ennek használatával az ilyen jellegű problémák meg is szűnnek létezni.

Immutability problémák React state kezelés közben, gyakorlati példán keresztül

Úgy érzem itt a gyakorlati példa segít a legtöbbet, nem pedig, hogy egy regény formájában vázolom fel azt. Az alábbi kóddal prezentálom is a problémát.

import {useState} from "react"

const initialState = [
    {
        id: 1,
        name: "John",
        hobbies: [
            {name: "football", id: 60, active: true},
            {name: "baseball", id: 61, active: true},
            {name: "basketball", id: 62, active: false},
            {name: "tennis", id: 63, active: true}
        ],
        friends: [2, 3, 4],
    },
    {
        id: 2,
        name: "Jane",
        hobbies: [
            {name: "knitting", id: 51, active: true},
            {name: "sewing", id: 52, active: true},
            {name: "reading", id: 53, active: true},
        ],
        friends: [1],
    }
]

const Test = () => {
    const [data, setData] = useState(initialState) 

    const handleCeaseFootball = () => {
        setData((prev) => prev.map((person) => {
            person.hobbies.forEach((hobby) => {
                if (hobby.id === 60) {
                    hobby.active = false
                }
            }) 

            return person
        }))
    } 

    return (
        <div className="flex p-40 w-full justify-between">
            <pre className="bg-blue-200 p-20">{JSON.stringify(data, null, 1)}</pre>
            <div className="flex gap-8 h-full">
                <button
                    className="p-2 bg-amber-100"
                    onClick={() => setData((prev) => [...prev, {
                        id: 3,
                        name: "Andrew",
                        hobbies: [
                            {name: "football", id: 60, active: true},
                            {name: "sword fighting", id: 64, active: true},
                            {name: "basketball", id: 62, active: false},
                            {name: "video gaming", id: 65, active: true},
                        ],
                        friends: [1, 2, 4],
                    }])}>
                    Add Andrew friend
                </button>
                <button
                    className="p-2 bg-amber-100"
                    onClick={handleCeaseFootball}>
                    cease football
                </button>
                <button
                    className="p-2 bg-amber-100"
                    onClick={() => setData(initialState)}>
                    reset state
                </button>
            </div>
        </div>
    )
}

export default Test

Kód átnézése

Láthatjuk, hogy a komponensünk állapotát inicializáljuk az initialState változó tartalmával (egy array of object-tel).

Van egy pre tag amiben az adatot „megjelenítjük”, valamint három gomb.

  1. Az első hozzáad egy hardcode-olt személyt a state-ünkben tároltakhoz.
  2. A második az összes személy foci tevékenységét hamisra állítja.
  3. A harmadik gomb segítségével pedig alaphelyzetbe állíthatjuk, az initialState változó tartalmával, az állapotunkat.

Használata és a következtetés levonása

Először a „megjelenítőben” láthatjuk, hogy két személy van benne, majd az első gomb megnyomását követően már három is van a state-ben. Hogyha megnézzük alaposan akkor észrevesszük, hogy kettő személynek (Andrew és John) a hobbija a foci.

Ezután kattintjuk a második („cease football”) gombot. Láthatjuk is a „megjelenítőben”, hogy ahogyan elvártuk megváltozott azon személyek, akiknek a hobbija foci, a foci hobbijának aktivitása hamisra.

Végül pedig „reset state” gombbal próbáljuk az alapértelmezettre visszaállítani az állapotunkat (state), ám nem az történik, amit elvárunk, mégpedig az alapállapot visszaállítása.

Hogyha megnéznénk az initialState változó tartalmát akkor láthatnánk, hogy az mutálódott és nem úgy működik a kódunk ahogy azt mi szeretnénk.

Kijavítása

Nem árulok el nagy titkot, hogy a probléma, ahol el lett rontva a kód, a handleCeaseFootball funkcióban következett be, egészen pontosan a foci hobbi activitásának false-ra állításánál (hobby.active = false). Itt ezzel a mozdulattal a hobbies array-t mutáltuk is.

Ahogy fentebb említettem több megoldás is létezik, itt a spread operator használatával kerül a megoldás prezentálásra.

const handleCeaseFootball = () => {
    setData((prev) => prev.map((person) => ({
        ...person,
        hobbies: person.hobbies.map((hobby) => {
            if (hobby.id === 60) {
                return {
                    ...hobby,
                    active: false
                }
            } 

            return hobby
        })
    })))
}

Shallow copyt készítünk a személyekről, valamint azok hobbijaikról egyaránt. Ezzel el is értük a helyes kód működését, nem történt adat mutáció.

Végszó

Összefoglalva, a cikk részletesen tárgyalta az immutability fogalmát és annak kiemelt fontosságát a JavaScript és React fejlesztés során. A gyakorlati példák segítségével bemutatásra kerültek a lehetséges problémák és ezek lehetséges megoldásai. Részletesen áttekintettük, hogyan lehet elkerülni a hibalehetőségeket a React állapotkezelésében és hogyan lehet helyesen kezelni azokat.

Reméljük, hogy a cikk által nyújtott információk segítettek jobban megérteni az immutability jelentőségét és a gyakorlati alkalmazását. Bízunk benne, hogy az itt bemutatott példák és tippek segítenek hatékonyabban kezelni a React állapotokat, és növelni a kód stabilitását és olvashatóságát. Köszönjük, hogy elolvastad cikkünket, és reméljük, hogy hasznos volt a témában való elmélyüléshez.

COPYRIGHT © 1999 - 2024 | SKYLINE-COMPUTER KFT.MINDEN JOG FENNTARTVA