8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HubbleAdvent Calendar 2024

Day 12

Hubbleフロントエンドの状態管理アーキテクチャ

Last updated at Posted at 2024-12-11

これはHubble Advent Calendar 2024の9日目の記事です。1

はじめに

株式会社Hubbleでフロントエンドを担当している @nishitaku です。

フロントエンド開発において、状態管理はアプリケーション全体の品質を左右する極めて重要な要素です。適切に設計された状態管理は、コードの保守性や拡張性を大幅に向上させます。

この記事では、Hubbleのフロントエンド開発で採用している状態管理アーキテクチャと、その設計思想について紹介します。

NgRx

HubbleのフロントエンドはAngularをベースに実装しており、状態管理にはNgRxを採用しています。NgRxは強力なライブラリですが、その性能を最大限に活かすためには、設計段階での工夫が欠かせません。

Hubbleでは、単一責任の原則に基づき、状態管理を以下の3種類のStoreに分類しています。

  • Component Store
  • Feature Store
  • Global Store

Component Store

特定のコンポーネントに紐づいた状態を管理するStoreです。このStoreはノードインジェクターに登録され、コンポーネントのライフサイクルと連動します。@ngrx/component-storeを使用して実装されています。

Component StoreComponentは、Container/Presetntationalパターンの関係性を持っています。ビジネスロジックや状態管理はComponent Storeに集約され、ComponentはUIの表示に特化し、原則状態を持ちません。

状態の変化はSignalを利用して追跡します。

サンプルコード
movies.store.ts
interface MovieState {
  movies: Movie[];
}

@Injectable()
class MoviesStore extends ComponentStore<MovieState> {
  constructor() {
    super({movies: []});
  }

  readonly movies = this.selectSignal(
    (state) => state.movies,
  );
}
movies-page.component.ts
@Component({
  template: `
    @for (movie of movies(); track movie.id) {
      {{ movie.title }}
    }
  `
  providers: [MoviesStore],
})
class MoviesPageComponent {
  private readonly componentStore = inject(MoviesStore);

  readonly movies = this.componentStore.movies;
}

Feature Store

特定の機能単位で共有される状態を管理するStoreです。このStoreはRouteの環境インジェクターに登録され、Route配下のコンポーネントのみが利用できます。

状態管理の要素(actions、reducers、selectors)を一つのファイルに集約することで、コードの可読性と保守性を向上させています。

サンプルコード
books.state.ts
export const BooksActions = createActionGroup({
  source: 'Books',
  events: {
    'Set Books': props<{ books: Book[] }>(),
    'Update Book': props<{ book: Book }>(),
    'Remove Book': props<{ book: Book }>(),
  }
});

interface State {
  books: Book[];
}

const initialState: State {
  books: [],
}

export const booksFeature = createFeature({
  name: 'books',
  reducer: createReducer(
    initialState,
    on(BooksActions.setBooks, (state, { books }) => ({
      ...state
      books
    })),
  ),
  extraSelectors: ({ selectBooks }) => {
    const selectFilteredBooks = createSelector(
      selectBooks,
      (books) => books.filter((book) => book.enabled)
    );

    return { selectFilteredBooks };
  },
});
books-routes.ts
{
  path: 'books',
  providers: [
    provideState(booksState)
  ],
}

Global Store

アプリケーション全体で共有されるグローバルな状態を管理するStoreです。ログイン情報やユーザー情報など、全体で使用するデータのみを扱い、ドメイン特化の状態はComponent StoreまたはFeature Storeで管理します。

各種要素(actionseffectsreducersselectors)はファイルを分割し、疎結合を保つ設計にしています。

サンプルコード
auth.action.ts
export const AuthActions = createActionGroup({
  source: 'Auth',
  events: {
    'Login': props<{ user: User }>
    'Logout': emptyProps()
  }
});
auth.reducer.ts
export interface AuthState {
  user: User | null;
}

const initialState: AuthState {
  user: null,
}

export const authReducer = createReducer(
  initialState,
  on(AuthActions.login, (state, { user }) => ({
    ...state
    user
  })),
  on(AuthActions.logout, (state) => ({
    ...state
    user: null
  })),
)
auth.selectors.ts
const selectUser = createSelector(
  (state: AuthState) => state.user,
  user => user
);
auth.effects:ts
@Injectable()
export class AuthEffects {
  logout$ = createEffect(() => this.actions$.pipe(
    ofType('AuthActions.logout'),
    exhaustMap(() => this.authService.logout()),
  );
}
main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ [authFeatureKey]: authReducer }),
  ]
});

最後に

Hubbleに参画する前は、NgRxに対して「使いづらい」「難しい」という印象を持っていました。しかし、近年のアップデートによってAngularとの親和性が向上し、以前と比べて格段に使いやすくなっています。たとえば、@ngrx/signalsなど、Hubbleではまだ採用していない新機能も登場しており、今後の進化にも大いに期待しています。

この記事が、NgRxの魅力や可能性を知るきっかけになれば嬉しいです。ぜひ一度試してみてください!

明日は @ic_lifewoodさんです!

  1. 平日のみの投稿なので、投稿日は12日ですが9日目の記事としています。

8
0
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
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?