#はじめに
本記事ではAngular向け状態管理ライブラリAkitaの紹介と、導入から使い方までの流れを解説します。
とても使いやすいライブラリだと思うのですが、日本語の情報あまりにも少なく、増えないので自分で書いてしまおうと思いました。
※AkitaはAngular以外でも使うことが出来ます
Akita
- GitHub
https://github.com/datorama/akita - 公式ドキュメント
https://netbasal.gitbook.io/akita/ - Blog
https://engineering.datorama.com/@NetanelBasal
環境
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アノテーションなどが付きます。
"akitaCli": {
"customFolderName": "true",
"template": "angular",
"basePath": "./src/app"
},
akita-ngdevtools
みんな大好きReduxDevToolが使えるようになります。
> npm i @datorama/akita-ngdevtools --save-dev
app.module.ts
のimports
にAkitaNgDevTools.forRoot()
を追加します。
これで、ReduxDevToolsが使えるようになります。
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 Store
とStore
のどちらかを選びます。ここでは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
を生成する場所を選択します。
- index.ts:import用のBarrel
- counter.query.ts(Query 状態の読み込みを行う
- counter.service.ts(Service 状態の書き込みを行う
- counter.store.ts(Store
Storeの編集
状態の定義はStoreで行います。favorite
を作り、初期値を0とします
export interface CounterState {
+ favorite: number;
}
export function createInitialState(): CounterState {
return {
+ favorite: 0
};
}
Serviceの編集
状態への書き込みはServiceで行います。良いね数増加用のincrement()
を作成します。
@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し、メソッドを呼ぶだけです。
@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();
}
}
<button mat-raised-button color color="primary" (click)="increment()">いいね</button>
{{ favorite$ | async }}
動いてるところ
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)
- 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'を追加します。
export interface Shop {
id: ID;
+ productName: string;
+ price: number;
}
Serviceの編集
状態への書き込みはServiceで行います。商品追加用のaddProduct()
を作成します。
@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し、メソッドを呼ぶだけです。
@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);
}
}
<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>
動いてるところ
Angular Forms Manager
AngularのReactiveForm用のStoreを自動で作成し、またフォームの値を自動保存してくれます。
validateの状態などもStoreに保存されるため、他のmodule、Componentでフォームの状態をちょっと使いたい場合などに便利です。
インストール
> npm install @datorama/akita-ng-forms-manager
先程のEntityStoreの例で作ったshop.omponent.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();
}
}
動いてるところ
Persist State(状態の永続化)
状態をLocalStorageに保存し、永続化させることが出来ます。
再訪問時にはLocalStorageから状態を復元します。
+ persistState();
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.log(err));
動いてるところ
F5押しても元に戻る!
ホワイトリスト方式、ブラックリスト方式で、永続化させるStoreを選ぶことも出来ます。
さいごに
ngRx
との比較になりますが、Akita
の最も良いところだと感じていることは、angularの学習曲線が緩やか曲線になることだと考えています。
例えば、angular公式のチュートリアルから学習を始め、チュートリアルが終わり次にngRx
を学んで行こうとなった時に一気に覚える事が増えます。
「手続きが多すぎる、覚えないといけないことが多すぎる・・・」
「現在の状態の値を取るAPIがないのか?欲しい場合はどうやって取るのが正解なんだ・・・」
「httpClientはどこに書けば・・・effect入れないといけない・・・?」
色々な悩みも生まれてきます。「うう・・・」と呻きながら学習を続けることになるのではないでしょうか(個人経験より
イメージとして、angularのチュートリアルからngRxを利用するところに高い崖があり、もう1段ジャンプする必要があるように感じます。
その点akitaは、シンプルな作りになっており、angularチュートリアルの延長戦上の緩やかな学習曲線をゆったり登っていけると思います。
公式ドキュメントを参照していただければわかりますが、Akitaには他にも色々便利なAPIがあります。
ロゴの秋田犬が可愛いと思った方、Akitaどうでしょうか