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:
- Effects: Responsável por fazer as requisições na API e enviar os dados para o gerenciador de estado.
- 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>
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);
});
Pronto, agora ao adicionar, editar ou remover um filme, tanto o componente 1 quanto o componente 2 mostrarão as mesmas informações.
O código fonte se encontra no GitHub.
Até a próxima.