🧘‍♀️

React の後に学ぶ Angular

2021/12/08に公開

前職で React を扱っていましたが、現職では Angular を触って半年くらい経ちました。この記事ではある程度フロントエンド開発に従事していたが、Angular 初学者としてどういうことを感じたのかということを具体的な例を交えて書いていきます。

またこれは Angular Advent Calendar 2021 7 日目の記事です。

前置き

  • React を長く触ってたフロントエンド開発者として Angular の手触りを書いている
  • Angular と React いずれも一長一短あるはずなのでフラットに考えてみる
  • React が優れている、Angular が方が良いというような分断をあおる内容ではない

React に慣れていると Angular でハマりそうなポイント

DOM 構造

Angular コンポーネントの @Component デコレータに指定するメタデータであるセレクタですが、ほとんどは app-list のような命名となるはずです。このセレクタをコンポーネントを利用する側が <app-list>...</app-list> のように使います。

たとえば以下のように app-list, app-listitem といったセレクタを指定したコンポーネントを組み合わせるとします。

<app-list>
  <app-listitem *ngFor="let item of list">
    <p>{{ item.description }}</p>
  </app-listitem>
<app-list>

各コンポーネントの構成にもよりますが、実際の HTML 構造は以下のようになります。

<app-list>
  <div class="list-outer">
    <ul>
      <app-listitem *ngFor="let item of list">
        <li>
          <p>{{ item.description }}</p>
        </li>
      </app-listitem>
    </ul>
  </div>
<app-list>

ul 要素の直の子要素に許可されているのは 0個以上の <li>, <script>, <template> 要素となるのでこれでは好ましくありませんね。

React や CSS-in-JS で慣れているとついコンポーネントをスタイル単位で分けてしまいます。

const List = styled.ul`...`;
const ListItem = styled.li`...`;

function AppList({ list }) {
  return (
    <List>
      {list.map(item => <ListItem>{item.description}</ListItem>)}
    </List>
  );
}

Angular で上記に示したコンポーネント構成を扱いたい場合、アプローチとしてはいくつかありそうです。

  • 属性セレクタ(ul[appList], [appListItem])に変更する
  • app-list の Input で list を受けて項目をリストする
  • 要素を変更し role=listitem といった WAI-ARIA ロールを使用する

Angular が DOM 構造にセレクタを表出しコンポーネントの境界を作っているという点は非常に興味深い点です。ここでは DOM 上の制約が出てきましたが、制約でもあると同時に、コンポーネント間の結合を少なくしているという点では境界がはっきりしていると言えます。

scoped CSS

CSS に関してもコンポーネント間の結合を断っている印象を持ちました。

React はスタイルにが関心がないのでサードパーティのライブラリで補うことが多いと思っていて、以下のように子コンポーネントに、ある子要素を期待するスタイルを書いてしまうことがたまにあります。

const Label = style.label`
  input[disabled] + * {
    opacity: .5;
  }
`
function InputFormLabel({ children }) {
  return (
    <Label>{children}</Label>
  );
}
// InputFormLabel の期待するユースケース
function App() {
  return ()
    <InputFormLabel>
      <input type="radio" name="foo" disabled={true} />
      <span>選択項目</span>
    </InputFormLabel>
  ;
}

ここで実現したいのはこの InputFormLabel の子コンポーネントに input が配置され、その input が選択できない場合(disabled)に、兄弟要素となっている span 要素にスタイルから opacity を与え視覚的に非活性であることを伝えたいという意図です。

良いか悪いかはさておき React サッとコンポーネントを作る際にこういったことは可能ですが、Angular は scoped CSS の制約によりそれがかないません。以下に同様のことを実現しようとした Angular のテンプレートを見てみます。

<!-- app-input-form-label コンポーネント -->
<style>
  label input[disabled] + * { opacity: .5 }
</style>
<label>
  <ng-content></ng-content>
</label>

<!-- 呼び出す側 -->
<app-input-form-label>
  <input type="radio" name="foo" disabled={true} />
  <span>選択項目</span>
</app-input-form-label>

コンポーネント構造をそのまま移植すると上記のようになりますが、app-input-form-label コンポーネントのスタイルから、子要素となる input, span すべてに対してスタイルを当てることはできません。

これは scoped CSS としてかなり優秀だと感じていて、これもコンポーネント間の制約を持たせ、脱法のスタイル注入を許さないぞという強い制約を感じました。

React にない <ng-content select="selector">

最後に Angular の好きなところなのですが、これもまた制約に関わる部分です。

ここまでで、Angular はコンポーネントを設計する際、子に配置するコンポーネントに対して制約をもたせる強みがあると分かってきました。

ここでもう 1 点、子に対する制約を簡易に実現できる例を示します。たとえば「子コンポーネントではこの要素のみを使用させたい」といった制約をもたせたいという要望は容易に現場で登場します。

Angular は以下のようにテンプレートで制御が可能です。

<ng-content select="input[type=radio]"></ng-content>
<ng-content select="span"></ng-content>

このテンプレート記述は順に input[type=radio], span といった子要素を利用者側に強制します。子コンポーネントに何でも許容するということに、コンポーネント設計者は強い制約を持たせることができます。

React なら以下のように制御できるでしょうか。

function Foo({ children }) {
  return React.Children.map(children, (child) => {
    if (
      child.type === "input" &&
      child.props.type === "radio"
    ) {
      return child;
    } else {
      throw new Error(`Don't use ${child.type}`)
    }
  });
}

テンプレート(もしくは JSX)レベルで制御というのは難しく、JavaScript の文脈で制御する必要がありそうです。

まとめ

React の後に学ぶ Angular と題して、JSX と対比させながら Angular コンポーネントにおける制約を中心に列挙してきました。

  • HTML 表出する DOM 構造
  • scoped CSS
  • テンプレートレベルの子要素制御

Angular においては基礎的なレベルかもしれませんが、React 習学者として Angular を学び始めて気付いたことを制約といった観点でまとめまると、HTML でアプリケーションを作るといった点やコンポーネントとして単独の責務を意識して設計可能であることが見えてきます。

引き続き、Angular を学ぶ中で気付きがあればまとめたいと思います。最後までお読みいただきありがとうございました。

Discussion