LoginSignup
51
30

More than 3 years have passed since last update.

Angular向け状態管理ライブラリAkitaの紹介

Last updated at Posted at 2019-09-05

はじめに

本記事ではAngular向け状態管理ライブラリAkitaの紹介と、導入から使い方までの流れを解説します。
とても使いやすいライブラリだと思うのですが、日本語の情報あまりにも少なく、増えないので自分で書いてしまおうと思いました。
※AkitaはAngular以外でも使うことが出来ます

Akita

akita.png

環境

angular ver8.2.4
akita ver4.10.2

Akitaの特徴、良いところ

Akitaの特徴、ngRxとの違いや、個人的に良いと思ったところについて

  • RxJsで構築されている
  • 関数型ではなくオブジェクト指向の設計原則に従っている
  • ReadのみのQuery・WriteのみのServiceと役割が明確に分かれている
  • ボイラープレートが少ない
  • 現在の状態の値を取得するAPIが用意されている
  • CLI、angular schematicsが用意されている
  • ReduxDevToolsが使える
  • LocalStorageとStoreの同期、状態の履歴(redo、undo)、AngularのReactiveFormとStoreの同期などを容易に実装出来る仕組みがある
  • ロゴの秋田犬が可愛い

インストール

Akita、Akita Cli、akita-ngdevtoolsをインストールします

Akita

> npm install @datorama/akita

Akita Cli

> npm install @datorama/akita-cli -g

package.jsonに以下を追加します。
"template": "angular"で、cliで生成されるファイルがTypeScriptになりAngularの@Serviceアノテーションなどが付きます。

package.json
  "akitaCli": {
    "customFolderName": "true",
    "template": "angular",
    "basePath": "./src/app"
  },

akita-ngdevtools

みんな大好きReduxDevToolが使えるようになります。

> npm i @datorama/akita-ngdevtools --save-dev

app.module.tsimportsAkitaNgDevTools.forRoot()を追加します。
これで、ReduxDevToolsが使えるようになります。

app.module.ts
  imports: [
+   environment.production ? [] : AkitaNgDevtools.forRoot(), // ReduxDevToolsを有効にする。
    BrowserModule,
    BrowserAnimationsModule
  ],

Store

Akita Cliではkey-value構造の状態を持つStoreと、DataBaseのようなコレクション構造の状態を持つEntityStoreのどちらかを選び作ることになります。
ここではStoreを選んだ場合のAkitaの使用方法を、ボタンを押すことで数が増加する良いねボタンの作成を例に説明します。
コンソールでakitaコマンドを実行します
色々聞かれますので、答えていきます。

> akita
? Give me a name, please 😀 counter
? Which store do you need? 😊 Store
? Give me a folder name, please counter
? Choose a directory.. (Use arrow keys)

? Give me a name, please 😀で作成するstoreの名前を入力します。ここではcounterとします。
? Which store do you need? 😊Entity StoreStoreのどちらかを選びます。ここではStoreを選択します。
? Use Http Entity Service ? (from @datorama/akita-ng-entity-service)RestApi作る場合に便利なHttp Entity Serviceを使うかどうかを選択します。ここではNo
? Give me a folder name, please counter'フォルダ名、ここではcounterとします。
Choose a directory..storeを生成する場所を選択します。

以下のようなファイルが生成されます
8c5108d3389add237e4cf5fb0ee222e0.png

  • index.ts:import用のBarrel
  • counter.query.ts(Query 状態の読み込みを行う
  • counter.service.ts(Service 状態の書き込みを行う
  • counter.store.ts(Store

Storeの編集

状態の定義はStoreで行います。favoriteを作り、初期値を0とします

couter.store.ts
export interface CounterState {
+  favorite: number; 
}

export function createInitialState(): CounterState {
  return {
+   favorite: 0
  };
}

Serviceの編集

状態への書き込みはServiceで行います。良いね数増加用のincrement()を作成します。

couter.service.ts
@Injectable({providedIn: 'root'})
export class CounterService {

  constructor(private counterStore: CounterStore) {
  }

+ increment() {
+   this.counterStore.update(store => ({
+     ...store,
+     favorite: store.favorite + 1
+   }));
+ }

- get() {
-   return this.http.get('').pipe(tap(entities => this.counterStore.update(entities)));
- }
}

AkitaCliで作ると get() が存在していますので削除しています。
Angularのチュートリアルの延長から言うと自然だと思いますが、NgRxを使った人だと「ここでhttpClient使って良いのか」と感じるかも知れません。僕は感じました。

Queryには最初から現在の値をObserbableで読み込めるselect()現在の状態を値で読み込めるget()などのAPIが存在します。
基本的に、データをComponentのテンプレートで扱う場合にはObserbableで取り、async pipeを使うことになると思います。
今回もその形になりますのでselect()を使います。
特殊な読み込み、共通化したい読み込みがある場合はメソッドをQueryに追加します。

利用側(Component)

あとはComponentでServiceとQueryをDIし、メソッドを呼ぶだけです。

counter.component.ts
@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
  readonly favorite$: Observable<number>;

  constructor(private counterService: CounterService, private counterQuery: CounterQuery) {
    this.favorite$ = this.counterQuery.select('favorite');
  }

  ngOnInit() {

  }

  increment() {
    this.counterService.increment();
  }
}
counter.component.html
<button mat-raised-button color color="primary" (click)="increment()">いいね</button>
{{ favorite$ | async }}

動いてるところ

counter.gif

EntityStore

CliでEntityStoreを選んだ場合、DataBaseのようなコレクション構造を持ったStoreを簡単に作ることが出来ます。
ここでは商品名と値段を入力し、リストに追加するアプリを例にEntityStoreの紹介を行います。

先程のStoreの例と同じく、Akita Cliを使ってファイルの作成を行います。

> akita
? Give me a name, please 😀 shop
? Which store do you need? 😊 Entity Store
? Give me a folder name, please shop
? Choose a directory.. (Use arrow keys)

以下のようなファイルが生成されます。
48e148cd5dcd1bb0532883cb2eacac7a.png

  • index.ts:import用のBarrel
  • shop.query.ts(Query 状態の読み込みを行う
  • shop.service.ts(Service 状態の書き込みを行う
  • shop.store.ts(Store
  • shop.model.ts(Model DataBaseで言うところのTABLEの構造

Modelの編集

定義をModelで行います。
商品名のためのproductNameと値段のための'price'を追加します。

shop.model.ts
export interface Shop {
  id: ID;
+ productName: string;
+ price: number;
}

Serviceの編集

状態への書き込みはServiceで行います。商品追加用のaddProduct()を作成します。

shop.service.ts
@Injectable({ providedIn: 'root' })
export class ShopService {

  constructor(private shopStore: ShopStore) {
  }

+ addProduct(productName: string, price: number) {
+   this.shopStore.add({
+     id: guid(),
+     productName,
+     price
+   });
+ }
}

利用側(Component)

あとはComponentでServiceとQueryをDIし、メソッドを呼ぶだけです。

shop.component.ts
@Component({
  selector: 'app-shop',
  templateUrl: './shop.component.html',
  styleUrls: ['./shop.component.css']
})
export class ShopComponent implements OnInit {
  readonly allProduct$: Observable<getEntityType<ShopState>[]>;
  formGroup: FormGroup;

  constructor(private shopService: ShopService, private shopQuery: ShopQuery, private formBuilder: FormBuilder) {
    this.allProduct$ = this.shopQuery.selectAll();
    this.formGroup = this.formBuilder.group({
      productName: '',
      price: 0
    });
  }

  ngOnInit() {
  }

  addProduct() {
    this.shopService.addProduct(this.formGroup.get('productName').value, this.formGroup.get('price').value);
  }
}
shop.component.html
<form [formGroup]="formGroup">
  <mat-form-field>
    <input autocomplete="off" matInput placeholder="商品名" formControlName="productName">
  </mat-form-field>
  <mat-form-field>
    <input autocomplete="off" matInput placeholder="値段" formControlName="price">
  </mat-form-field>
</form>
<button mat-raised-button color color="primary" (click)="addProduct()">追加</button>
<br><br>
<table>
  <tr>
    <th>商品名</th>
    <th>値段</th>
  </tr>
  <tr *ngFor="let product of allProduct$ | async">
    <td>{{product.productName}}</td>
    <td>{{product.price}}</td>
  </tr>
</table>

動いてるところ

shop.gif

Angular Forms Manager

AngularのReactiveForm用のStoreを自動で作成し、またフォームの値を自動保存してくれます。
validateの状態などもStoreに保存されるため、他のmodule、Componentでフォームの状態をちょっと使いたい場合などに便利です。

インストール

> npm install @datorama/akita-ng-forms-manager

先程のEntityStoreの例で作ったshop.omponent.tsに組み込んでみます

shop.component.ts
export interface FormsState {
  productForm: {
    productName: string;
    price: number;
  };
}

@Component({
  selector: 'app-shop',
  templateUrl: './shop.component.html',
  styleUrls: ['./shop.component.css']
})
export class ShopComponent implements OnInit, OnDestroy {
  readonly allProduct$: Observable<getEntityType<ShopState>[]>;
  formGroup: FormGroup;

  constructor(private shopService: ShopService, private shopQuery: ShopQuery, private formBuilder: FormBuilder, private formsManager: AkitaNgFormsManager<FormsState>) {
    this.allProduct$ = this.shopQuery.selectAll();
    this.formGroup = this.formBuilder.group({
      productName: '',
      price: 0
    });
  }

  ngOnInit() {
    this.formsManager.upsert('productForm', this.formGroup);
  }

  addProduct() {
    this.shopService.addProduct(this.formGroup.get('productName').value, this.formGroup.get('price').value);
  }

  ngOnDestroy() {
    this.formsManager.unsubscribe();
  }
}

動いてるところ

formManager.gif

Persist State(状態の永続化)

状態をLocalStorageに保存し、永続化させることが出来ます。
再訪問時にはLocalStorageから状態を復元します。

main.ts
+ persistState();

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.log(err));

動いてるところ

persist.gif

F5押しても元に戻る!
ホワイトリスト方式、ブラックリスト方式で、永続化させるStoreを選ぶことも出来ます。

さいごに

ngRxとの比較になりますが、Akitaの最も良いところだと感じていることは、angularの学習曲線が緩やか曲線になることだと考えています。
例えば、angular公式のチュートリアルから学習を始め、チュートリアルが終わり次にngRxを学んで行こうとなった時に一気に覚える事が増えます。
「手続きが多すぎる、覚えないといけないことが多すぎる・・・」
「現在の状態の値を取るAPIがないのか?欲しい場合はどうやって取るのが正解なんだ・・・」
「httpClientはどこに書けば・・・effect入れないといけない・・・?」
色々な悩みも生まれてきます。「うう・・・」と呻きながら学習を続けることになるのではないでしょうか(個人経験より
イメージとして、angularのチュートリアルからngRxを利用するところに高い崖があり、もう1段ジャンプする必要があるように感じます。
その点akitaは、シンプルな作りになっており、angularチュートリアルの延長戦上の緩やかな学習曲線をゆったり登っていけると思います。

公式ドキュメントを参照していただければわかりますが、Akitaには他にも色々便利なAPIがあります。
ロゴの秋田犬が可愛いと思った方、Akitaどうでしょうか

51
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
30