JavaScript i TypeScript sprawiają, że traktowanie referencji do obiektów jako czegoś jednorazowego staje się niebezpiecznie łatwe. Tworzysz obiekt, przekazujesz go do funkcji, przechowujesz w cache'u, a później zastępujesz zmienną nową instancją. Język nie zgłasza błędu. Stara referencja wciąż istnieje gdzieś indziej w programie, wskazując na dane, które nie są już aktualne. To nie jest awaria. To coś gorszego: cicha rozbieżność między dwiema częściami bazy kodu, z których obie uważają, że posiadają prawdę.

Dyscyplina, która temu zapobiega, nazywa się Hard Object References. To nie jest biblioteka ani funkcja kompilatora. To kontrakt, który wymuszasz w swoim kodzie.

Problem nieaktualnego aliasu

Nieaktualny alias (stale alias) pojawia się, gdy jeden moduł przechowuje referencję do obiektu, podczas gdy inny moduł zastępuje ten obiekt nowym. Pierwsza referencja jest wciąż poprawnym kodem, ale nie wskazuje już na aktualne dane.

Wyobraź sobie rekord użytkownika w typowej aplikacji webowej:

const user = {
  name: "Alice",
  address: {
    city: "Seoul",
    country: "KR"
  }
};

Komponent wysyłkowy przechwytuje adres na wczesnym etapie:

const shippingAddress = user.address;

Później przychodzi aktualizacja profilu. Reducer lub handler usługi decyduje się zastąpić cały obiekt:

user.address = { city: "Tokyo", country: "JP" };

W tym momencie user.address wskazuje na Tokio. Ale shippingAddress wciąż wskazuje na stary obiekt w Seulu. Nie zostaje rzucony żaden wyjątek. TypeScript jest zadowolony, ponieważ typy nadal się zgadzają. Interfejs użytkownika (UI) może wyświetlać zaktualizowane miasto na stronie profilu, podczas gdy etykieta wysyłkowa po cichu drukuje stare. Błąd ujawnia się dopiero wtedy, gdy użytkownik skarży się, że jego paczka trafiła do niewłaściwego kraju.

Dzieje się tak, ponieważ JavaScript oddziela tożsamość od wartości. Gdy zastępujesz właściwość obiektu nowym obiektem literalnym, przerywasz łańcuch. Stary obiekt nie zostaje zniszczony; zostaje po prostu osierocony. Każdy, kto wciąż go trzyma, pracuje z duchem.

Co oznaczają Hard Object References

Zasada jest prosta: zastępuj wartości prymitywne, ale nigdy nie zastępuj referencji do obiektów ani tablic. Gdy przychodzą nowe dane, skopiuj je do istniejącego kontenera, zamiast wymieniać cały kontener.

Wymaga to trzech konkretnych nawyków.

Po pierwsze, deklaruj obiekty i tablice za pomocą const. Eliminuje to pokusę ponownego przypisania zmiennej na najwyższym poziomie do nowej instancji. Zmienna powinna pozostać niezmienna przez cały czas trwania zakresu (scope).

Po drugie, nigdy nie zastępuj właściwości przechowującej obiekt lub tablicę nowo utworzoną instancją. Jeśli musisz zaktualizować adres, zmień (mutate) właściwości w jego wnętrzu.

Po trzecie, jeśli musisz wyczyścić lub zresetować stan, opróżnij istniejącą strukturę, zamiast wyrzucać ją i tworzyć nowy pusty obiekt lub tablicę.

Wracając do przykładu z adresem, poprawna aktualizacja wygląda następująco:

user.address.city = "Tokyo";
user.address.country = "JP";

Jeśli przychodzące dane są częściowe lub dynamiczne, użyj Object.assign, aby zapisać je w istniejącym obiekcie docelowym:

Object.assign(user.address, incomingAddressData);

Zmienna shippingAddress, która wskazuje na dokładnie ten sam obiekt w pamięci, natychmiast widzi nowe pola. Istnieje tylko jeden kanoniczny obiekt służący jako żywe źródło prawdy.

Gdzie ma to największe znaczenie

Dyscyplina ta może wydawać się przesadą w przypadku płaskiego obiektu konfiguracyjnego. Staje się jednak niezbędna, gdy Twój stan rozrasta się w graf, w którym wiele podsystemów przechowuje wskaźniki do nakładających się węzłów.

Rozważ edytor tekstu bogatego (rich text editor). Model dokumentu to drzewo węzłów. Model zaznaczenia przechowuje referencje do węzłów początkowego i końcowego. Bufor historii przechowuje referencje do węzłów, które zmieniły się w ostatniej operacji. Warstwa renderowania przechowuje referencje do węzłów, które zostały zmierzone na potrzeby układu (layout). Jeśli menedżer stanu zastąpi węzeł akapitu nowym obiektem, ponieważ zmienił się jego tekst, każdy z tych podsystemów będzie teraz trzymał nieaktualny alias. Zaznaczenie podświetli niewłaściwy obszar. System historii nie będzie mógł poprawnie cofnąć zmian. Renderer ulegnie awarii lub, co gorsza, wyświetli widmowe kursory.

To samo ryzyko pojawia się w profilach użytkowników z zagnieżdżonymi ustawieniami i uprawnieniami, do których odwołują się interfejs użytkownika, warstwa kontroli dostępu i rutyna autozapisu. Pojawia się w silnikach układu, gdzie kontenery nadrzędne przechowują w pamięci podręcznej pomiary węzłów potomnych. Pojawia się w edytorach wizualnych i narzędziach typu canvas, gdzie kontroler czasu wykonywania (runtime controller) śledzi aktywne encje za pomocą referencji. We wszystkich tych dziedzin

Jeśli pracowałeś z Redux lub podobnymi bibliotekami niezmiennych stanów, ten model może brzmieć nielogicznie. W tych systemach zmiana jest sygnalizowana poprzez tworzenie nowego obiektu. Zmiana referencji jest sygnałem. Komponenty porównują prevProps.data === nextProps.data, aby wiedzieć, czy należy wykonać ponowne renderowanie.

Hard Object References wymagają odwrócenia tego założenia. Ponieważ referencja pozostaje stała, równość referencji nie mówi nic o tym, czy dane uległy zmianie. Potrzebujesz innego sposobu na rozsyłanie aktualizacji.

W praktyce oznacza to poleganie na systemach reaktywności, jawnych obserwatorach lub flagach dirty. Mutacja user.address.city może wywołać setter, który powiadamia subskrybentów. Obiekt może emitować zdarzenie zmiany poprzez szynę zdarzeń (event bus). Pętla gry lub narzędzie typu canvas może ustawić globalną flagę dirty i ponownie przeskanować graf na końcu klatki. Referencja jest stabilna, więc musisz uczynić przepływ danych widocznym za pomocą innych mechanizmów.

Ta zmiana architektury sprawia, że podejście to najlepiej sprawdza się w złożonych stanach frontendowych, dużych stanach lokalnych komponentów, edytorach wizualnych, narzędziach typu canvas i kontrolerach czasu rzeczywistego. Systemy te już teraz polegają na granularnych aktualizacjach, bezpośrednich mutacjach lub imperatywnych API. Wymuszanie niezmienności (immutability) na wierzchu często powoduje nadmierną presję alokacji i rotację referencji, nie przynosząc proporcjonalnej jasności. Gdy liczy się każda klatka, alokowanie nowego grafu obiektów tylko po to, by przesunąć suwak, jest marnotrawstwem. Utrzymanie stałej referencji i mutowanie wnętrza odpowiada rzeczywistej mechanice problemu.

Jak to utrwalić

Jedną z pomijanych zalet tej zasady jest