$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

ReactのuseEffect/useLayoutEffectやレンダリングの実行順について調べた

useEffect/useLayoutEffect周りで絶賛ハマり中で、また React17におけるuseEffectの破壊的変更を理解する | Zenn の記事が面白かったので、自分の手元で動かしてみて理解を深めてみた。調査したことを自分用のメモとして取っておく。React 17でuseEffect周りで破壊的な変更が入るということなので、Reactのバージョンは17.0.0-rc.1で試した。

気になっていたこと

  • 親子のコンポーネントでuseEffectがどの順で実行されているのか
  • useLayoutEffectが途中にあるとどのような挙動をするか
  • useEffect/useLayoutEffectとレンダリングやDOMへの反映の実行順序

調査メモ

shibayu36/typescript-playground/react-playground あたりで試した。react-playground ディレクトリ以下でyarn installしてyarn startしたら試せる。

以下2つのエンドポイントを実装し、行き来しながらどのようなログが出るか見た。

/ -> /effect-order へアクセスした時は以下のようなログが出る。この時、LayoutEffect useLayoutEffect では同期的に2秒待ちを入れていたので、実際に画面に表示されるのは2秒後となった。Child useEffectは2秒待ちを入れていたが、画面表示には影響せず、その代わり後続のuseEffectは遅延された。

EffectOrder rendering
Effect rendering
LayoutEffect rendering
Child rendering
Home ref null
Child ref <div>​Child Component​</div>​
LayoutEffect ref <div>​…​</div>​
LayoutEffect useLayoutEffect
Effect ref <div>​…​</div>​
EffectOrder ref <div>​…​</div>​
Child useEffect
Effect useEffect
EffectOrder useEffect

/effect-order -> / のときは以下のようなログが出る。こちらもuseLayoutEffectのcleanupに2秒待ちを入れていたのでそれが実行されるまでDOMへの反映も遅延された。

Home rendering
EffectOrder ref null
Effect ref null
LayoutEffect useLayoutEffect cleanup
LayoutEffect ref null
Child ref null
Home ref <header class=​"App-header">​…​</header>​
EffectOrder useEffect cleanup
Effect useEffect cleanup
Child useEffect cleanup

この実行から分かったことは

  • Virtual DOMの構築はたぶん「... rendering」というログのところで実行されていそうなので、useEffect/useLayoutEffect関係なく作られてそう
  • 実際のDOM構築はたぶん「... ref」というログのところで実行されていそうなので、useLayoutEffectはこのタイミングで実行される。実際のDOM構築が全て終わらないと画面表示出来ないので、useLayoutEffectで重い処理をするとユーザーへの描画タイミングが遅れる
  • useEffectはDOM構築が終了した後に実行されるので表示には影響しない。マウント時は子から親の順で実行され、アンマウント時は親から子の順で実行される。途中で重い処理が行われていると後続のuseEffectは遅延する。

フェーズとしては、Virtual DOM構築フェーズ、実際のDOM構築 & useLayoutEffect実行フェーズ、useEffect実行フェーズがある感じか?

まとめ

ReactのuseEffect/useLayoutEffectやレンダリングの実行順について調べたので自分用のメモを残してみた。Reactのドキュメントに書いてあるような内容を再確認しただけにはなったが、自分の手を動かしたためか、もっと実感を持って理解できたような気がする。

参考文献

ALBで特定のpathのときだけCognito認証を通す構成をaws-cdkで作る

管理画面の時だけ認証をかけたいみたいな要件はよくある。今回はその要件をALBで特定のpathの時だけCognito認証を通すという構成で実装してみたのでメモ。構成はaws-cdkを使った。

やりたいこと

  • /admin/以下の場合はCognitoの認証をかけ、許可したユーザーしかアクセスできないようにする
  • それ以外のパスではアプリケーションに認証なしで繋がるようにする

f:id:shiba_yu36:20200918094327p:plain

実装

aws-cdk 1.62.0を使って実装した。なお今回のコードは実際の実装をミニマムに変更しているので、動作確認はちゃんと出来ていない(たぶんそのままだと動かなそう)。雰囲気だけ感じてください。

import { IVpc, Port } from "@aws-cdk/aws-ec2";
import { Ec2Service } from "@aws-cdk/aws-ecs";
import {
  ApplicationLoadBalancer,
  ApplicationProtocol,
  ListenerAction,
  ListenerCondition,
} from "@aws-cdk/aws-elasticloadbalancingv2";
import { AuthenticateCognitoAction } from "@aws-cdk/aws-elasticloadbalancingv2-actions";
import { Construct, Duration } from "@aws-cdk/core";
import {
  UserPool,
  UserPoolClient,
  OAuthScope,
  UserPoolDomain,
  CfnUserPoolClient,
} from "@aws-cdk/aws-cognito";

export interface PathRestrictedAlbProps {
  /**
   * アプリケーション
   */
  appService: Ec2Service;

  /**
   * ALBなどを配置するvpc
   */
  vpc: IVpc;
}
export class PathRestrictedAlb extends Construct {
  constructor(
    parent: Construct,
    name: string,
    props: PathRestrictedAlbProps
  ) {
    super(parent, name);

    const applicationName = "example";

    // パスワードポリシーとか自分の用途に合わせていい感じにする
    const userPool = new UserPool(this, "UserPool", {
      userPoolName: applicationName,
      selfSignUpEnabled: false,
      signInAliases: {
        username: true,
        email: true,
      },
      standardAttributes: {
        email: {
          required: true,
        },
      },
      passwordPolicy: {
        minLength: 20,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: true,
      },
    });
    const userPoolClient = new UserPoolClient(this, "UserPoolClient", {
      userPool: userPool,

      // Required minimal configuration for use with an ELB
      generateSecret: true,
      authFlows: {
        userPassword: true,
        refreshToken: true,
      },
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
        },
        scopes: [OAuthScope.EMAIL],
        callbackUrls: [`https://${applicationName}.com/oauth2/idpresponse`],
      },
    });
    const cfnClient = userPoolClient.node
      .defaultChild as CfnUserPoolClient;
    cfnClient.addPropertyOverride("RefreshTokenValidity", 1);
    cfnClient.addPropertyOverride("SupportedIdentityProviders", ["COGNITO"]);

    const userPoolDomain = new UserPoolDomain(this, "UserPoolDomain", {
      userPool: userPool,
      cognitoDomain: {
        domainPrefix: applicationName,
      },
    });

    const alb = new ApplicationLoadBalancer(this, "Alb", {
      vpc: props.vpc,
      loadBalancerName: applicationName,
      internetFacing: true,
    });
    alb.connections.allowFromAnyIpv4(Port.tcp(443));

    const listener = alb.addListener("Listener", {
      port: 443,
      protocol: ApplicationProtocol.HTTPS,
      open: true,
      certificateArns: [
        // 証明書を設定する
        "...",
      ],
    });

    // デフォルトはECSへ
    const targetGroup = listener.addTargets("Target", {
      targetGroupName: applicationName,
      protocol: ApplicationProtocol.HTTP,
      port: 80,
      targets: [props.appService],
    });

    // conditionsをいい感じに設定することで特定pathだけCognito通すとかができる
    // ここでは/admin/以下の場合はCognito認証を通し、認証をパスした場合のみ
    // ECSへ到達するようにする
    listener.addAction(`Action1`, {
      action: new AuthenticateCognitoAction({
        userPool: userPool,
        userPoolClient: userPoolClient,
        userPoolDomain: userPoolDomain,
        next: ListenerAction.forward([targetGroup]),
      }),
      conditions: ListenerCondition.pathPatterns(["/admin/*"]),
      priority: 1,
    });
  }
}

あとはCognitoのユーザー管理でアクセスできる人を追加したら良い。 f:id:shiba_yu36:20200921131912p:plain

これで、/admin/以下にアクセスしたときに認証のダイアログが出て、先程追加したユーザーしかアクセスできなくなる。 f:id:shiba_yu36:20200921131953p:plain

まとめ

今回はALBで特定のpathのときだけCognito認証を通す構成をaws-cdkで作ってみた。今回は特定パス以下だけ認証を通すようにしたが、例えば管理画面はドメインごと変えたいとなった場合にもデフォルトアクション全てで認証をかけるようにすれば同じような構成で出来ると思う。

Cognitoは他にもMFAを使うなど認証系は簡単に実装できるので、自分で実装したらだるい認証系はSaaSに任せていけると良さそうだった。

参考

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-elasticloadbalancingv2-actions-readme.html

スプレッドシートで保育園の在庫管理をしようとして失敗したけど、claspによるGASの管理方法を学べた

スプレッドシートで保育園の在庫管理をしようとして失敗した...いい方法があれば教えてもらいたい。

失敗したけど学びはあったので、ここにメモしておく。

困っていたこと

  • 毎日保育園で子供二人の服やおむつなどがどのくらいあるか把握するのが難しかった
  • アイテムリストとそれぞれの個数があって、プラスマイナスボタンで増減できるみたいなのがあると便利そうと考えた。またそれを妻と共有もしたい
  • だがiOSアプリで便利そうなアプリが見つからなかった

そのため、GoogleスプレッドシートGoogle Apps Scriptで、簡易的なストック管理が出来るのでは?と考えた。

やってみたら失敗した

実装は出来た。

しかし、スマホアプリからプラスマイナスボタンクリックでスクリプト実行がなされず、詰んでしまった...

claspによるGASの管理方法を学べた

やりたいことは失敗したけど、 @google/clasp を使うと、Google Apps ScriptをTypeScriptで書いて、即座に反映できることが学べたのは良かった。 https://github.com/shibayu36/gas-stock-management でやったことがサンプルケースになりそう。

claspとGASの型定義をインストールしておく。

npm install @google/clasp @types/google-apps-script -D

後は clasp push --watch しておくと、勝手にTypeScriptをコンパイルしてGASにアップロードしてくれる。

$ npx clasp push --watch

詳しくは