Proste zarządzanie stanem na sygnałach w Angularze

Dość często w swoich aplikacjach niepotrzebujemy zaciągać zaawansowanych bibliotek do zarządzania stanem takich jak NgRx czy NGXS.

Niestety wielu niedoświadczonych programistów lub doświadczonych, ale którzy nie mieli nigdy styczności z zaawansowanym stanem (ale czy możemy ich nazwać doświadczonymi? ( ͡° ͜ʖ ͡°)) ulega pokusie instalowania JUŻ na starcie potężnej libki do stanu, bo przecież każdy chce być fajny, nowoczesny no i w internecie tak ładnie o tym piszą!

Moja rada

Zacznij projekt od prostych rozwiązań. Nie wiadomo jak się aplikacja rozwinie w czasie. Czasami projekt jest planowany na lata, a kończy się po pół roku, bo biznesowo spełnia wszystko i zostaje z pięcioma widokami, czasem zaś aplikacja ma być na trzy widoki i dwa miesiące, a projekt trwa 10 lat.

Na to wpływa wiele czynników, może:

  • dane będą na bieżąco aktualizowane z backendu, wtedy taki stan wręcz przeszkadza swoimi narzutami,
  • będzie dużo formularzy i wystarczy trzymać dane w Reactive Forms czy nadchodzących Signal Forms?

Na początku skup się na ogólnej architekturze projektu, wzorcach, jak i dokumentacji Angulara z minimalnym instalowaniem zewnętrznych bibliotek. Naprawdę Angular jest potężny ma wiele w sobie fajnych, ciekawych rozwiązań, jeśli pomyślisz o jakimś rozwiązaniu prawdopodobnie Angular ma to już w sobie!

Kilka lat temu miałem podobnie potrzebowałem zrobić coś na zasadzie przewijania do sekcji. Miałem prosty landing page z menu, przy scrollowaniu myszką do sekcji chciałem, aby menu mi się pozycja w menu zrobiła aktywna.

Pomyślałem, że to jest dość generyczne rozwiązanie może Angular ma coś takiego wbudowanego? Szybki research i pyk klasa ViewportScroller rozwiązała problem!

Od tamtej pory zmieniłem podejście, najpierw dogłębnie szukam czy jest wbudowane rozwiązanie jeszcze teraz w dobie AI gdzie wystarczy wpisać i dostajemy wszystko w minutę.

A teraz do rzeczy!

Prosty stan

Pokażę Ci kompletne rozwiązanie prostego stanu do trzymania danych czy to jakiś token, id, a nawet pokusiłbym się o przetrzymywanie prostych danych np. lista krajów, prefixy numerów telefonów itp.

Zgodne z najlepszymi praktykami Angulara i DI!

Na początku zróbmy token o nazwie STATE który będziemy wstrzykiwać w nasze komponenty oraz interfejsy stanu:

//state.token.ts
export const STATE = new InjectionToken<ApplicationState>('Application State');
//state.interface.ts
export interface ApplicationState {
    data: Signal<DataApplicationState>;
    update: (updateState: Partial<DataApplicationState>) => void;
    reset: () => void;
}

//przykładowy kontrakt przetrzymywanych danych
export interface DataApplicationState {
    appId: number | null;
    accessToken: string | null;
}

Następnie stworzymy fabrykę:

//state.factory.ts
export function applicationState(): ApplicationState {
    const data = signal(stateDefault());

    return {
        data,
        update: (updateState) =>
            data.update((current) => ({ ...current, ...updateState })),
        reset: () => data.set(stateDefault()),
    };
}

const stateDefault = (): DataApplicationState => ({
    appId: null,
    accessToken: null,
});

Na końcu musimy zarejestrować nasz token:

bootstrapApplication(App, {
    providers: [
        {
            provide: STATE,
            useFactory: applicationState,
        },
    ],
});

I to w sumie tyle, jeśli chodzi o implementację.

Prawda, że proste?!

Jak to wykorzystać?

Jeśli chcemy wykorzystać lub zaktualizować stan wystarczy w komponencie wstrzyknąć nasz token jak poniżej:

@Component({
    template: `
    {{ state.data().appId }} 

    <button (click)="update()">Klik</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class App {
    readonly state = inject(STATE);

    update(): void {
        this.state.update({ appId: Math.random() });
    }

    reset(): void {
        this.state.reset();
    }
}

Całość można przetestowąć tutaj.

Weź, implementuj, rozwijaj według swoich potrzeb… i daj znać!