Article image
Diego Nunes
Diego Nunes05/12/2023 16:16
Compartilhe

NgRx ComponentStore — Gerenciamento de estado no Angular

    Hoje, falaremos um pouco sobre gerenciamento de estado no Angular. Na Pricefy, empresa onde eu trabalho, utilizamos o ComponentStore, do NgRx, para gerenciar os estados das nossas aplicações.

    Nós dividimos o nosso gerenciador em duas partes:

    1. Effects: Responsável por fazer as requisições na API e enviar os dados para o gerenciador de estado.
    2. Store: Local onde ficam as lógicas e transformações das informações vindas do backend em interfaces que serão lidas pelos componentes.

    Agora vamos para um exemplo prático

    Primeiro, criaremos um novo projeto usando o Angular CLI:

    ng new movies-app
    

    Com o projeto criado, vamos instalar duas bibliotecas que nos ajudarão com o servidor fake, o json-server, que será o backend, e o concurrently, que nos permitirá executar o json-server e a nossa aplicação Angular com apenas um comando.

    json-server:

    npm install json-server
    

    concurrently:

    npm install concurrently
    

    Após as instalações, vamos no package.json alterar a chave start para:

    "concurrently \"json-server --watch db.json\" \"ng serve\""
    

    Com essa configuração, quando iniciarmos nossa aplicação com o comando npm start, tanto o servidor quanto nossa aplicação estarão em execução.

    Nosso próximo passo é criar a estrutura do nosso projeto. Para isso, criaremos uma pasta chamada services e, dentro dela, o nosso serviço movies.service.ts, que terá quatro funções para o nosso CRUD:

    private url: string = `http://localhost:3000/movies`;
    
    constructor(private http: HttpClient) { }
    
    insertMovie(movie: Movie): Observable<Movie> {
    return this.http.post<Movie>(this.url, movie);
    }
    
    updateMovie(movie: Movie, id: number): Observable<Movie> {
    return this.http.put<Movie>(`${this.url}/${id}`, movie);
    }
    
    deleteMovie(id: number) {
    return this.http.delete(`${this.url}/${id}`);
    }
    
    getAllMovies(): Observable<Movie[]> {
    return this.http.get<Movie[]>(this.url);
    }
    

    Agora, vamos criar dois componentes que mostrarão os dados do store:

    ng g c componente1
    ng g c componente2
    

    No nosso arquivo app.component.html, colocamos os dois componentes:

    <div class="container">
    <div class="col-6">
      <app-componente1></app-componente1>
    </div>
    <div class="col-6">
      <app-componente2></app-componente2>
    </div>
    </div>
    

    Ah, vale lembrar que coloquei dentro do nosso index.html o style bootstrap, apenas para dar um pouco de estilo aos nossos componentes.

    Bom, com nosso projeto estruturado, vamos ao que interessa. Para usarmos o ComponentStore, teremos de adicioná-lo a nossa aplicação com o comando:

    ng add @ngrx/component-store@latest
    

    Após a instalação, devemos inseri-lo dentro do nosso app.module como provider:

    providers: [ ComponentStore ],
    

    A seguir, criaremos uma pasta chamada state e, dentro dela, um arquivo injectable chamado movies.store.ts, que herdará de ComponentStore. Também criei duas interfaces neste mesmo arquivo.

    export interface Movie {
    id?: number;
    name: string;
    }
    
    export interface MoviesState {
    movies: Movie[];
    }
    
    @Injectable({ providedIn: 'root' })
     export class MoviesStore extends ComponentStore<MoviesState> {
    }
    

    No construtor, enviaremos o nosso array para o construtor do ComponentStore.

    constructor() {
    super( { movies: [] } )
    }
    

    Com o método select, podemos recuperar os dados de nosso estado como no exemplo abaixo:

    readonly movies$: Observable<Movie[]> = this.select(state =>    state.movies);
    

    O método updater é utilizado para atualizar o estado. Ele recebe uma função pura com o estado atual e com o valor que a ser atualizado e retorna o estado alterado. Abaixo temos o exemplo de adicionar, remover e editar um filme.

    readonly addMovie = this.updater((state, movie: Movie) => ({
    movies: [...state.movies, movie]
    }));
    
    readonly editMovie = this.updater((state, movie: Movie) => {
    let actualMovie = state.movies.find(item => item.id === movie.id);
    actualMovie.name = movie.name;
    return state;
    });
    
    readonly removeMovie = this.updater((state, movie: Movie) => {
    let movies = state.movies.filter(item => item.id !== movie.id);
    state.movies = movies;
    return state;
    });
    

    No nosso próximo passo, criaremos um arquivo movies.effects.ts e injetamos nele o nosso serviço e o store.

    constructor(
      private moviesStore: MoviesStore,
      private moviesService: MoviesService
    ) {}
    

    Com o código abaixo, buscaremos os dados na API e enviaremos para o gerenciador de estado.

    readonly getMovies = this.moviesStore.effect((trigger$:   Observable<{}>) => {
    return trigger$.pipe(
      switchMap(() => {
        return this.moviesService.getAllMovies().pipe(
          map((httpResponse) => {
            Object.keys(httpResponse).forEach((item) => {
            this.moviesStore.addMovie(httpResponse[item])
          });
         }),
         catchError((error: any) => {
           console.log(error);
           return EMPTY;
         })
        );
       }));
    });
    

    Com nosso gerenciador de estado configurado, podemos buscar os dados no store e apresentar em nossos dois componentes.

    movies$ = this.moviesStore.movies$;
    
    <tr *ngFor="let movie of (movies$ | async)">
      <th>
        {{ movie.id }}
      </th>
      <th>
        {{ movie.name }}
      </th>
    </tr>
    

    image

    No componente 1, também coloquei funções para adicionar, editar e deletar um filme:

    // Edit movie
    
    this.moviesService.updateMovie(movie, id).subscribe(httpResponse  => {
    this.moviesStore.editMovie(httpResponse);
    this.showFormNewMovie = false;
    this.form.reset();
    
    // New movie
    this.moviesService.insertMovie(movie).subscribe(httpResponse => {
    this.moviesStore.addMovie(httpResponse);
    this.showFormNewMovie = false;
    
    // Delete movie
    this.moviesService.deleteMovie(movie.id).subscribe(() => {
    this.moviesStore.removeMovie(movie);
    });
    

    image

    Pronto, agora ao adicionar, editar ou remover um filme, tanto o componente 1 quanto o componente 2 mostrarão as mesmas informações.

    image

    O código fonte se encontra no GitHub.

    Até a próxima.

    Compartilhe
    Comentários (0)