【Angular】BehaviorSubjectを使って状態管理しよう!【サンプルコードあり】

ANGULAR

みなさんこんにちは、現役エンジニアのサメハックです

未経験からWebエンジニアに転職し、
正社員として5年働いたのちフリーランスとして独立しました。

Angularの解説シリーズです。

今回はBehaviorSubjectについて学んでいきましょう!

駆け出しエンジニアや未経験の方、
また新入社員を指導する先輩社員にとっても
わかりやすいように解説していきます!

この記事を読むと・・・
  • BehaviorSubjectを使った状態管理が理解出来る

※PCにnpm、nodeがインストールされている前提で記述します。
 yarn等をお使いの方は読み替えてください。

環境がない人はcodesansboxなどを使ってね!

作りたいもの

これが今回の完成形だよ!

実行環境

Angularのバージョンが古いと動かないことがあるよ!
うまく動かなければアップデートしてね!

BehaviorSubjectを使った状態管理をしよう!

BehaviorSubjectとは

BehaviorSubjectとはAngularのRxJSを使った状態管理システムです。

BehaviorSubjectを使うことで、どのコンポーネントからも
共通の保管された情報へアクセスすることが出来ます。

使い方としては

  • BehaviorSubjectに値を保管する
  • 保管した値を更新する
  • 保管した値を購読する
  • 購読を解除する

という処理がセットになります。

BehaviorSubjectを使うことで、
全てのコンポーネントから同じ情報にアクセスできるよ!

BehaviorSubjectに値を保管する構文

import { BehaviorSubject } from 'rxjs';

// BehaviorSubjectの宣言
データ保管用変数 = new BehaviorSubject<型>(初期値);
ここに値が保管されるよ!

保管した値を更新する構文

データ保管用変数.next(更新したい値);
保管した値を更新するにはnext()を使うよ!

保管した値を購読する

// 購読した値を格納する変数
ローカル変数X: any;

// 購読設定停止用
private subscriptions = new Subscription();

ngOnInit(): void {
  this.subscriptions.add(
    // データ保管用変数を購読する
    // ※データ保管用変数が更新される度に呼ばれる
    データ保管用変数.subscribe(
      (response) =>
        // ローカル変数Xに購読した値を代入し、情報をリンクさせる
        (this.ローカル変数X = response)
    )
  );
}
Subscriptionにaddしておくことでまとめて購読の解除ができるよ!

購読を解除する

ngOnDestroy(): void {
  /**ページ遷移等部品が呼ばれなくなったタイミングで購読を停止 */
  this.subscriptions.unsubscribe();
}
ちなみにsubscribeの戻り値はsubscriptionだよ!

保管した値を取得

データ保管用変数.getValue()

実際に作ってみよう!

今回はpage1,page2,page3という3つのコンポーネントから
servieファイルに保管したuser$を参照し、
全てのコンポーネントで同じ値が表示されるアプリを作ります。

今回のコードはGitHubにあげているので、
すぐに動かしたい人は本記事後半からダウンロードしてね!

コンポーネントの作成

ng new my-app
ng generate service service/user
ng generate component page1
ng generate component page2
ng generate component page3

サービスファイルの設定

user.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

// User型
export interface User {
  name: String;
  age: number;
  job: String;
}

@Injectable({
  providedIn: 'root',
})
export class UserService {
  // 初期値用
  data = {
    name: 'サメハック',
    age: 29,
    job: 'エンジニア',
  };

  // ユーザの持つ値を保管する
  user$ = new BehaviorSubject(this.data);

  /**user$を更新 */
  upadte(userInfo: User) {
    console.log('更新前 ', this.user$.getValue());
    // this.user$の保管する値を更新
    this.user$.next(userInfo);
    console.log('更新後 ', this.user$.getValue());
  }

  /**user$のageをインクリメント */
  increment() {
    console.log('インクリメント前 ', this.user$.getValue().age);

    // user$に保管した値ののageプロパティのみを更新
    const userInfo = {
      ...this.user$.getValue(),
      age: this.user$.getValue().age + 1,
    };

    // this.user$の保管する値を更新
    this.user$.next(userInfo);
    console.log('インクリメント後 ', this.user$.getValue().age);
  }

  constructor() {}
}
このサービスファイルで、値の保管と更新を行うよ!

page1.component

<section style="background-color: pink">
  <h2>page1</h2>
  <p>名前: {{ page1User.name }}</p>
  <p>年齢: {{ page1User.age }}</p>
  <p>職業: {{ page1User.job }}</p>
  <button (click)="updateUserInfo()">ユーザ情報更新</button>
</section>
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { UserService, User } from '../service/user.service';

@Component({
  selector: 'app-page1',
  templateUrl: './page1.component.html',
  styleUrls: ['./page1.component.scss'],
})
export class Page1Component implements OnInit {
  // サービスファイルの読み込み
  constructor(private service: UserService) {}

  // 購読設定停止用
  private subscriptions = new Subscription();

  // page1で表示するユーザ情報
  page1User: User;

  ngOnInit(): void {
    this.subscriptions.add(
      // user$を購読する
      // ※user$が更新される度に呼ばれる
      this.service.user$.subscribe(
        (userInfo) =>
          // pageUser1に購読したuser$の値を代入し、情報をリンクさせる
          (this.page1User = userInfo)
      )
    );
  }

  /**更新したいユーザ情報をサービスに渡す */
  updateUserInfo() {
    const newUser: User = {
      name: 'ねこハック',
      age: 24,
      job: 'OL',
    };

    // user$を更新
    this.service.upadte(newUser);
  }

  ngOnDestroy(): void {
    /**ページ遷移時に購読を停止 */
    this.subscriptions.unsubscribe();
  }
}
ユーザの情報を更新するコンポーネントだよ!

page2.component

<section style="background-color: skyblue">
  <h2>page2</h2>
  <p>名前: {{ page2User.name }}</p>
  <p>年齢: {{ page2User.age }}</p>
  <p>職業: {{ page2User.job }}</p>
  <button (click)="clearUserInfo()">ユーザ情報クリア</button>
</section>
import { Component, OnInit } from '@angular/core';
import { UserService, User } from '../service/user.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-page2',
  templateUrl: './page2.component.html',
  styleUrls: ['./page2.component.scss'],
})
export class Page2Component implements OnInit {
  // サービスファイルの読み込み
  constructor(private service: UserService) {}

  // 購読設定停止用
  private subscriptions = new Subscription();

  // page2で表示するユーザ情報
  page2User: User;

  ngOnInit(): void {
    this.subscriptions.add(
      // user$を購読する
      // ※user$が更新される度に呼ばれる
      this.service.user$.subscribe(
        (userInfo) =>
          // pageUser2に購読したuser$の値を代入し、情報をリンクさせる
          (this.page2User = userInfo)
      )
    );
  }

  /**空のユーザ情報をサービスに渡す */
  clearUserInfo() {
    const newUser: User = {
      name: '',
      age: 0,
      job: '',
    };

    // user$を更新
    this.service.upadte(newUser);
  }

  ngOnDestroy(): void {
    /**ページ遷移時に購読を停止 */
    this.subscriptions.unsubscribe();
  }
}
ユーザ情報をクリアするコンポーネントだよ!

page3.component

<section style="background-color: lightgreen">
  <h2>page3</h2>
  <p>名前: {{ page3User.name }}</p>
  <p>年齢: {{ page3User.age }}</p>
  <p>職業: {{ page3User.job }}</p>
  <button (click)="updateUserAge()">年齢を+1</button>
</section>
import { Component, OnInit } from '@angular/core';
import { UserService, User } from '../service/user.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-page3',
  templateUrl: './page3.component.html',
  styleUrls: ['./page3.component.scss'],
})
export class Page3Component implements OnInit {
  // サービスファイルの読み込み
  constructor(private service: UserService) {}

  // 購読設定停止用
  private subscriptions = new Subscription();

  // page3で表示するユーザ情報
  page3User: User;

  ngOnInit(): void {
    this.subscriptions.add(
      // user$を購読する
      // ※user$が更新される度に呼ばれる
      this.service.user$.subscribe(
        (userInfo) =>
          // pageUser3に購読したuser$の値を代入し、情報をリンクさせる
          (this.page3User = userInfo)
      )
    );
  }

  /**ユーザの年齢をインクリメント */
  updateUserAge() {
    // user$を更新
    this.service.increment();
  }

  ngOnDestroy(): void {
    /**ページ遷移時に購読を停止 */
    this.subscriptions.unsubscribe();
  }
}
ユーザの年齢を+1するコンポーネントだよ!

npm start

これでnpm startすると、同じものが完成します。
どのコンポーネントを操作しても全て同じデータが表示されるので
BehaviorSubjectで正常に状態管理されていることがわかります。

GitHubのサンプルコード

今回作ったものはGitHubにあげているので
使いたい人は是非ダウンロードしてみてください。

GitHub - same-hack/Angular-BehaviorSubject: AngularのBehaviorSubjectのサンプルコードです。
AngularのBehaviorSubjectのサンプルコードです。. Contribute to same-hack/Angular-BehaviorSubject development by creating an account on GitHub.

まとめ

  • BehaviorSubject 値を保管する
  • next() 保管した値を更新
  • subscribe 保管した値を購読する
  • Subscription.unsubscribe 購読を解除する

満足いただけたら、1クリックなのでSNSフォローしてもらえると嬉しいです🦈

タイトルとURLをコピーしました