Functional ComponentでforceUpdateをエミュレートする

Functional ComponentでforceUpdateをエミュレートする

参考

Class ComponentのforceUpdate

実際にこのAPIを使うケースはほとんどないと思う。setStateを使ってもうまくコンポーネントが更新されないのでこのAPIを使いたい、といった場合はオブジェクトの同じ参照を渡していないかまずはチェックしてほしい。

Reactコアメンバーによれば、レガシーなコードとReactとの統合で使用するらしいが、幸運にもそのようなケースに出くわしたことはない。

export default class App extends React.Component {
  onClickHandler = () => {
    this.forceUpdate();
  };

  render() {
    return (
        <button onClick={this.onClickHandler}>Click me</button>
    );
  }
}

Functional ComponentのforceUpdate

Functional Componentでも、useStateuseReducerを使えばエミュレートすることは可能。ただし非推奨

const useForceUpdate = (): [() => void, number] => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount((prev) => prev + 1);
  return [increment, count];
};

export default function App() {
  const [forceUpdate] = useForceUpdate();
  const onClickHandler = () => {
    forceUpdate();
  };
  return (
      <button onClick={onClickHandler}>Click me</button>
  );
}

Reactの描画プロセス

ではなぜ非推奨のAPIを紹介したのかというと、このforceUpdateがReactの描画プロセスを理解するのに役立つと思ったから。

Reactの描画プロセスは次の流れに沿って行われる:

  • Render: renderメソッドを実行した結果を集め、オブジェクトツリーを形成する
  • Reconciliation: 新しいオブジェクトツリーと前回との差分を検出する
  • Commit: 差分を実際のDOMに反映する

ここで重要なのは仮想DOMの差分だけが実際にDOMに反映されるということ。上記で紹介したサンプルコードでいくらforceUpdateを実行しても、<button>が書き換えられることはない。DOMインスペクタで検証してみてもDOMの更新を意味するflashが表示されていないことが分かる。

ただし、次のように関数の実行によって参照が変わるコンポーネントを使用すると、変更がなさそうに見えても差分は一旦破棄(アンマウント)され実際のDOMは更新される。

import * as React from "react";
import "./styles.css";

const useForceUpdate = (): [() => void, number] => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount((prev) => prev + 1);
  return [increment, count];
};

export default function App() {
  const [forceUpdate] = useForceUpdate();
  const onClickHandler = () => {
    forceUpdate();
  };
  const Child = () => <div>child</div>;
  return (
    <>
      <Child />
      <button onClick={onClickHandler}>Click me</button>
    </>
  );
}

Rails Active Storage で画像ファイルをHTTPSで配信したい

HTTPSページにて、AWS S3に保存された画像ファイルをActive Storageで配信すると、Mixed contentエラーが発生します。

f:id:uraway:20200614222622p:plain

ref:

https://www.mixedcontentexamples.com/Test/NonSecureImage

https://www.mixedcontentexamples.com/

デバッガーツールのSecurityタブからも確認できます。

f:id:uraway:20200614222814p:plain

これはRailsurl_helperメソッドがデフォルトではHTTPを使うため。

config.force_ssl = true

とした上で、config/initializers/force_ssl.rbファイルを作成し

Rails.application.routes.default_url_options[:protocol] = 'https' if Rails.application.config.force_ssl

とすれば良さそうです

ref:

https://medium.com/@stacietaylorcima/rails-active-storage-serve-your-images-over-https-14b916c67a51

CircleCIでマトリックスビルド

CircleCIでマトリックスビルド

現状CircleCIではマトリックスビルドはサポートされていませんが、実現するための方法がいくつかあります。

matrixキーがサポートされました 👏

Configuring CircleCI - CircleCI

node v8、v10、v12、それぞれの実行環境でライブラリのインストール、テスト、ビルドを行い、すべてに成功した場合のみ、ビルドした成果物をNode v10でデプロイしたいとします。また、テストとビルドは互いに依存しないが、インストールに依存するとします。

それぞれの実行環境ごとに、ジョブを定義する

次のように、実行環境(Nodeバージョン)ごとにジョブを定義することができます。

image

version: 2.1

jobs:
  install_deps:
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }}
            - v1-npm-{{ .Branch }}-
            - v1-npm-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }}
      - persist_to_workspace:
          root: ./
          paths:
            - node_modules

  "node-8":
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm test
      - run: npm run build

  "node-10":
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm test
      - run: npm run build
      - persist_to_workspace: # Node v10でビルドした成果物を永続化
          root: ./
          paths:
            - lib/
            - node_modules

  "node-12":
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm test
      - run: npm run build

  deploy:
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run deploy

workflows:
  version: 2
  matrix:
    jobs:
      - install_deps
      - "node-8":
          requires:
            - install_deps
      - "node-10":
          requires:
            - install_deps
      - "node-12":
          requires:
            - install_deps
      - deploy:
          requires:
            - "node-8"
            - "node-10"
            - "node-12"

testbuildが直列に定義されていることが気になるところですが、テストとビルドが依存していないからと言って別ジョブに分けて定義したいという欲がでてくると、node-8-testnode-8-buildnode-10-test…をそれぞれ定義しなければならなくなるため、ちょっと厳しくなります。

image

version: 2.1

jobs:

(略)

  "node-8-test":
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm test

  "node-8-build":
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run build

  "node-10-test":
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm test

  "node-10-build":
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run build
      - persist_to_workspace:
          root: ./
          paths:
            - lib
            - node_modules

  "node-12-test":
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm test

  "node-12-build":
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run build

  deploy:
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run deploy

workflows:
  version: 2
  matrix:
    jobs:
      - install_deps
      - "node-8-test":
          requires:
            - install_deps
      - "node-8-build":
          requires:
            - install_deps
      - "node-10-test":
          requires:
            - install_deps
      - "node-10-build":
          requires:
            - install_deps
      - "node-12-test":
          requires:
            - install_deps
      - "node-12-build":
          requires:
            - install_deps
      - deploy:
          requires:
            - "node-8-test"
            - "node-8-build"
            - "node-10-test"
            - "node-10-build"
            - "node-12-test"
            - "node-12-build"

この場合、パラメータを使ってジョブコンフィグをまとめることができます。

ジョブパラメータで管理する

パラメータを使えば、テスト・ビルドのジョブは使い回すことができるため、設定ファイルがすっきりします。ただし、これでも手動で値を入れる必要があるため、2軸のマトリックス(言語バージョン×OSなど)はうまく管理できる気がしません。

version: 2.1

executor-template: &executor-template
  parameters:
    tag:
      default: "10"
      type: string
    persist_built_files:
      type: boolean
      default: false
  docker:
    - image: circleci/node:<< parameters.tag >>

jobs:
  install_deps:
    <<: *executor-template
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }}
            - v1-npm-{{ .Branch }}-
            - v1-npm-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }}
      - persist_to_workspace:
          root: ./
          paths:
            - node_modules

  test:
    <<: *executor-template
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run test

  build:
    <<: *executor-template
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run build
      - when:
          condition: << parameters.persist_built_files >>
          steps:
            - persist_to_workspace:
                root: ./
                paths:
                  - node_modules
                  - lib

  deploy:
    <<: *executor-template
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run deploy

workflows:
  version: 2
  matrix:
    jobs:
      - install_deps

      - test:
          name: node-8-test
          tag: "8"
          requires:
            - install_deps

      - test:
          name: node-10-test
          tag: "10"
          requires:
            - install_deps

      - test:
          name: node-12-test
          tag: "12"
          requires:
            - install_deps

      - build:
          name: node-8-build
          tag: "8"
          requires:
            - install_deps

      - build:
          name: node-10-build
          tag: "10"
          persist_built_files: true
          requires:
            - install_deps

      - build:
          name: node-12-build
          tag: "12"
          requires:
            - install_deps

      - deploy:
          tag: "10"
          requires:
            - node-8-test
            - node-8-build
            - node-10-test
            - node-10-build
            - node-12-test
            - node-12-build

パイプラインパラメータで管理する

少しトリッキーですが、パイプラインパラメータを使えば、ワークフロー全体にパラメータを渡すことができるため、より少ない記述量で実現できます。この設定ファイルを使用するには、APIトークン(CIRCLE_TOKEN)が必要です。

(パラメータを使った動的なジョブ名は前からできたっけ?)

parameters:
  tag:
    default: '10'
    type: string
  run-main-workflow:
    default: false
    type: boolean
  run-deploy-job:
    default: false
    type: boolean

executors:
  node:
    docker:
      - image: circleci/node:<< pipeline.parameters.tag >>

version: 2.1
jobs:
  install_deps:
    executor: node
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }}
            - v1-npm-{{ .Branch }}-
            - v1-npm-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }}
      - persist_to_workspace:
          root: ./
          paths:
            - node_modules

  test:
    executor: node
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run test

  build:
    executor: node

    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run: npm run build

      - when:
          condition: << pipeline.parameters.run-deploy-job >>
          steps:
            - persist_to_workspace:
                root: ./
                paths:
                  - lib
                  - node_modules

  deploy:
    docker:
      - image: circleci/node:<< pipeline.parameters.tag >>
    steps:
      - when:
          condition: << pipeline.parameters.run-deploy-job >>
          steps:
            - checkout
            - attach_workspace:
                at: ./
            - run: npm run deploy
      - unless:
          condition: << pipeline.parameters.run-deploy-job >>
          steps:
            - run: echo "No deployment on Node v<< pipeline.parameters.tag >>"

  trigger-main-workflows:
    machine:
      image: ubuntu-1604:201903-01
    parameters:
      deploy-node-version:
        default: '10'
        type: string
    steps:
      - run:
          name: Trigger main worflow
          command: |
            VCS_TYPE=$(echo ${CIRCLE_BUILD_URL} | cut -d '/' -f 4)

            for NODE_VERSION in 8 10 12
            do
                PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\"}"
                if [ "$NODE_VERSION" = << parameters.deploy-node-version >> ]
                then
                    PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\", \"run-deploy-job\": true}"
                fi

                curl -u ${CIRCLE_TOKEN}: -X POST --header "Content-Type: application/json" -d "{
                  \"branch\": \"${CIRCLE_BRANCH}\",
                  \"parameters\": ${PIIPELINE_PARAM_MAP}
                }" "https://circleci.com/api/v2/project/${VCS_TYPE}/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pipeline"
            done

workflows:
  version: 2
  matrix:
    unless: << pipeline.parameters.run-main-workflow >>
    jobs:
      - trigger-main-workflows:
          deploy-node-version: "10"

  main:
    when: << pipeline.parameters.run-main-workflow >>
    jobs:
      - build:
          name: build-<< pipeline.parameters.tag >>
      - test:
          name: test-<< pipeline.parameters.tag >>
      - deploy:
          requires:
            - build-<< pipeline.parameters.tag >>
            - test-<< pipeline.parameters.tag >>

trigger-jobsワークフローが、Node v8, v10, v12の実行環境において、それぞれmainワークフローをトリガーします。

Nodeバージョン×DBといったマトリックスも可能です。

全文

(略)
  trigger-main-workflows:
    machine:
      image: ubuntu-1604:201903-01
    parameters:
    steps:
      - run:
          name: Trigger main worflow
          command: |
            VCS_TYPE=$(echo ${CIRCLE_BUILD_URL} | cut -d '/' -f 4)

            for NODE_VERSION in 8 10 12
            do
                for DB in mongo mysql
                do
                    PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\", \"db\":\"$DB\"}"
                    if [ "$NODE_VERSION" = "10" ] && [ "$DB" = "mongo" ]
                    then
                        PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\", \"db\":\"mongo\", \"run-deploy-job\": true}"
                    fi
                    curl -u ${CIRCLE_TOKEN}: -X POST --header "Content-Type: application/json" -d "{
                      \"branch\": \"${CIRCLE_BRANCH}\",
                      \"parameters\": ${PIIPELINE_PARAM_MAP}
                    }" "https://circleci.com/api/v2/project/${VCS_TYPE}/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pipeline"
                done
            done

ただし、APIを叩く必要があるため、これでもまだ他のCIでできるようなマトリックスビルドと比べれば、やぼったさは否めません。また、デプロイジョブをすべてのワークフローが成功した場合のみ実行するしくみ(ファンイン)にすることが非常に難しく、APIを使ってビルド結果をポーリングするか、承認ジョブを使って目視で確認してから手動で起動にするしかなさそうです。

(ファンアウト・ファンインについて)

RFC: Matrix Jobs syntaxを見ると、マトリックスビルドの開発が今まさに行われているようなので、正式サポートされるまではファンアウトだけなら上記のパイプラインパラメータ、ファンインが必要ならジョブパラメータが良さそうかな。

CircleCIからnpm packageを公開するためのOrb

作りました。よろしくお願いします。

Orb Registry: npm-publisher

GitHub Repo: npm-publisher

使用例

使い方

NPMトークンを取得し、環境変数NPM_TOKENとしてセット。package.jsonの情報に従ってパッケージを公開します。

モジュールのダウンロードやビルドはpre-publish-stepsパラメータを使用します。

orbs:
  npm-publisher: uraway/npm-publisher@x.y.z

version: 2.1
workflows:
  build_publish:
    jobs:
      - npm-publisher/publish-from-package-version:
          publish-token-variable: NPM_TOKEN
          push-git-tag: true
          fingerprints: <fingerprints>
          pre-publish-steps:
            - restore_cache:
                keys:
                  - v1-node-cache-{{ .Branch }}-{{ checksum "package-lock.json" }}
                  - v1-node-cache-{{ .Branch }}
                  - v1-node-cache-
            - run: npm install
            - run: npm build
          post-publish-steps:
            - save_cache:
                key: v1-node-cache-{{ .Branch }}-{{ checksum "package-lock.json" }}
                paths:
                  - node_modules
          filters:
            branches:
              only: master

また、push-git-tagオプションを有効にしてフィンガープリントをセットすればタグをコミットします。

配列の積和演算のアルゴリズム

配列の積和演算のアルゴリズム

「特別な配列」が与えられるので積和演算を行って値を返す。「特別な配列」は数値あるいは「特別な配列」をもつ。「特別な配列」内の数値をすべて加算するが、ネストレベルに応じてその和を乗算する。 例えば、

[x, y] → x + y
[x, [y, z]] → x + 2(y + z)
[1, [2, [3, 4]]] → 1 + 2 * (2 + 3 * (3 + 4)) → 47

アルゴリズム実装(1) $O(n)$

再帰関数を別途作るのが楽。見たまんまで、配列ならdepthに1を加えて再帰関数を実行し、数値なら加算する。

function productSum(array) {
    return productSumHelper(array, 1)
}

function productSumHelper(array, depth) {
    let result = 0
    for (const ele of array) {
        if (Array.isArray(ele)) {
            result += productSumHelper(ele, depth + 1)
        } else {
            result += ele
        }
    }
    return result * depth
}

素数n回実行されるので、計算量は$O(n)$

二分探索木(binary search tree)から近似値を求める

二分探索木(binary search tree)から近似値を求める

二分探索木のデータとtargetが与えられる。二分探索木のデータの中からtargetの近似値を求める。

例えば次のような二分探索木のデータとtarget 5が与えられるとき、答えは6である。

        8
        /\
       3  10
      /\   \
     1 6   14
       /\   /
      4  7 13

ref: https://ja.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E6%9C%A8

二分探索木の定義

二分探索木のデータ構造は「左の子孫の値 ≤ 親 ≤ 右の子孫の値」という制約を持つ。与えられた値の近似値を求めるには、一時的にそれまで見つかった近似値を保持しておいて、次の子ノード、孫ノード、…と子ノードが存在しなくなるまで探索を続ける必要があるため、計算量はどのアルゴリズムでも最低O(n)となるはず。

子ノードが存在しないときに保持していた近似値が解となる。

与えられる引数

引数はtreetargetが与えられる。

treeオブジェクトは数値valuetreeオブジェクトlefttreeオブジェクトrightのプロパティを持つ再帰的なオブジェクト。targetは数値。

アルゴリズム実装(1) O(n)

ノードが存在しないとき、treeオブジェクトはnullをとる。近似値を保持しておいて、tree === nullとなるまでループを続ける。二分探索木の定義から、leftプロパティのvalueは必ず現在のノードのvalueより小さく、rightプロパティのvalueは必ず現在のノードのvalueより大きい。

function findClosestValueInBst(tree, target) {
    return traverseNode(tree, target, null);
}

function traverseNode(node, target, closest) {
    while (node !== null) {
        if (closest === null || Math.abs(target - closest) > Math.abs(target - node.value)) {
            closest = node.value
        }
        if (target < node.value) {
            node = node.left
        } else if (target > node.value) {
            node = node.right
        } else {
            break
        }
    }
    
    return closest
}

アルゴリズム実装(2) O(n)

メモリ的には優しくないかもしれないが、再帰関数のほうが理解しやすいかも?

function findClosestValueInBst(tree, target) {
    return traverseNode(tree, target, null);
}

function traverseNode(node, target, closest) {
    if (node === null) {
        return closest
    }
    if (closest === null || Math.abs(target - closest) > Math.abs(target - node.value)) {
        closest = node.value
    }
    if (target < node.value) {
        return traverseNode(node.left, target, closest)
    } else if (target > node.value) {
        return traverseNode(node.right, target, closest)
    }
    
    return closest
}