RIT Tech Blog

株式会社RITのエンジニアが知見を共有する技術ブログです。

Blitz.js 迷った時のディレクトリ構成

こんにちは。エンジニアの岸本です。

Blitz.jsを使って開発をしている時に、「このファイルはどこに配置しよーかなー」と悩んだことはないでしょうか? 私は多々あります。主にutilsか?それともservicesか?とか。 感覚値で配置場所を決めてたこともあったので、この機会にシンプルにまとめたいと思います。

本記事の対象者

  • Blitz.jsユーザーでファイルの設置場所に悩むことがたまにある人

本記事で解決できること

  • ファイルの設置場所で悩む事による時間の無駄遣い

そして結論

基本的に以下の順序で一考すれば間違いない(と信じてる)

1. まずは基本に忠実にファイル構成を考える(以下は公式から参照)

├── app/
│   ├── core/
│   │   ├── components/
│   │   │   ├── Form.tsx
│   │   │   └── LabeledTextField.tsx
│   │   ├── hooks/
│   │   │   └── useCurrentUser.ts
│   │   └── layouts/
│   │       └── Layout.tsx
│   ├── pages/
│   │   ├── 404.tsx
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── index.test.tsx
│   │   ├── index.tsx
│   │   └── projects/
│   │       ├── [id]/
│   │       │   └── edit.js
│   │       ├── [id].js
│   │       ├── index.js
│   │       └── new.js
│   ├── api/
│   │   └── stripe-webhook.js
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── SignupForm.tsx
│   │   ├── mutations/
│   │   │   ├── login.ts
│   │   │   ├── logout.ts
│   │   │   └── signup.ts
│   │   ├── pages/
│   │   │   ├── login.tsx
│   │   │   └── signup.tsx
│   │   ├── auth-utils.ts
│   │   └── validations.ts
│   ├── users/
│   │   └── queries/
│   │       └── getCurrentUser.ts
│   └── projects/
│       ├── components/
│       │   ├── Project.js
│       │   ├── ProjectForm.js
│       │   └── Projects.js
│       ├── mutations/
│       │   ├── createProject.js
│       │   ├── createProject.test.js
│       │   ├── deleteProject.js
│       │   ├── deleteProject.test.js
│       │   ├── updateProject.js
│       │   └── updateProject.test.js
│       └── queries/
│           ├── getProject.js
│           └── getProjects.js
├── db/
│   ├── index.ts
│   ├── schema.prisma
│   └── seeds.ts
├── integrations/
│   └── sentry.ts
├── public/
│   ├── favicon.ico*
│   └── logo.png
├── test/
│   ├── setup.ts
│   └── utils.tsx
├── README.md
├── babel.config.js
├── blitz.config.js
├── jest.config.js
├── package.json
├── tsconfig.json
├── types.ts
└── yarn.lock

2. 便利かつどちらかと言えば複雑度低めな関数(処理 ex.文字列の加工など)なら/utilsフォルダへ、どちらかと言えば複雑度高めなら/servicesフォルダへ

3. 深くネストしないような構成で考える(相対パスを使ったインポートが面倒 + ファイルが移動したときにそれらを更新するのも大変)

4. それでもしっくり来ないなら、よく一緒に変更するファイルを近くに置いておく!

公式からのサンプルに追記すると、以下のような構成にすれば個人的にはネストも深くせずにシンプルになるのではないでしょうか。

│   ├── users/
│   │   └── queries/
│   │       └── getCurrentUser.ts
│   │   └── services/   <----
│   │       └── serviceExample.ts
│   │   └── utils/   <----
│   │       └── utilsExample.ts

Blitz.jsの公式ページには以下記載があることからも、迷った場合はよく一緒に変更するファイルに記述すれば確実かもしれないですね。

Guiding Principles

  • Files that change together should live together.(一緒に変更されるファイルは一緒に配置しましょう)
  • Minimal requirements, maximum flexibility.(最小の用件を、最大の柔軟性で)

余談

「一緒に変更されるファイルは一緒に配置しましょう」という考え方・原則は「コロケーション」と呼ばれます。 コロケーションを採用することで手を動かすことを優先することが可能になることは、「Done is better than perfect.(完璧であることより、まず終わらせることが重要だ)」で有名なザッカーバーグ氏の思想を反映させたようにも感じますよね。

Gitでディレクトリの大文字小文字の変更が認識されない

こんにちは、RITエンジニアの三浦です。

Gitでディレクトリの大文字小文字の変更を反映するのに苦戦したので、解決方法を記します。

原因

Macファイルシステムディレクトリの大文字小文字をデフォルトで区別しておらず、Gitの大文字小文字変更検知がこれに依存しているのが原因みたいです。 masyus.work

解決策

下流れでディレクトリの大文字小文字変更をnew fileではなく、renameとしてGitに認識させる必要があります。

  1. gitconfigでファイルの大文字小文字を区別するように設定を変更する
  2. 差分がrenameとなっていることを確認する
  3. commitする

具体的に見ていきます。

配下に2つのファイルを持つ小文字sampleディレクトリを大文字Sampleディレクトリに変更します。

sampleディレクトリ内に2つのファイルを作成してcommitしました。 この時点では、Gitにはファイルが正しく認識されています。

$ git ls-files
sample/test1.tsx
sample/test2.tsx

Gitはデフォルトの大文字小文字を区別していない状態です。

$ git config core.ignorecase
true

このとき、ディレクトリsampleをSampleに変更しても差分が認識されません。

$ mv sample/ Sample/
$ git status
On branch master
nothing to commit, working tree clean

理由は、Gitがデフォルトで大文字小文字を検知しない設定になっているためです。

$ git config core.ignorecase
true

Gitが大文字小文字を検知するよう変更します。

$ git config core.ignorecase false
$ git config core.ignorecase
false

差分(Sample/)が検知されるようになりました。

$ git status 
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Sample/

これをインデックスに追加します。 しかしこれでは、ディレクトリを大文字に変更しただけなのに、ファイルの新規作成としてGitに認識されてしまっています。

$ git add .

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   Sample/test1.tsx
        new file:   Sample/test2.tsx
nothing added to commit but untracked files present (use "git add" to track)

インデックスされているファイルを見てみます。

$ git ls-files
Sample/test1.tsx
Sample/test2.tsx
sample/test1.tsx
sample/test2.tsx

このままcommitすると同じファイルが2つずつできてしまうのがわかります。

方法①:renameで認識させたところだけをcommitする

git rm コマンドでインデックス上から不要なファイルを削除します。

$ git rm sample/test1.tsx
rm 'sample/test1.tsx'

$ git rm sample/test2.tsx
rm 'sample/test2.tsx'

インデックス上から削除されたことを確認します。

$ git ls-files           
Sample/test1.tsx
Sample/test2.tsx

このとき、git statusで初めてフォルダの変更がnew fileではなくrenameとして認識されます。

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        renamed:    sample/test1.tsx -> Sample/test1.tsx
        renamed:    sample/test2.tsx -> Sample/test2.tsx

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    Sample/test1.tsx
        deleted:    Sample/test2.tsx

最後にワーキングツリーの変更(deletedの2ファイル)はリセットしておきます。

$ git checkout .
Updated 2 paths from the index

これをcommitすることで、Github上で正しくディレクトリ名がリネームされていることが確認できました!

方法②:一度全く別のディレクトリ名に変更する

ディレクトリ名を一旦別の名前(_sample)に変更します。

$ mv sample/ _sample/
$ git add .
$ git ls-files
_sample/test1.tsx
_sample/test2.tsx
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        renamed:    sample/test1.tsx -> _sample/test1.tsx
        renamed:    sample/test2.tsx -> _sample/test2.tsx

別の名前(_sample)から本来変更したかった名前(Sample)に変更します。

$ mv _sample/ Sample/
$ git add .
$ git ls-files
Sample/test1.tsx
Sample/test2.tsx
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        renamed:    sample/test1.tsx -> Sample/test1.tsx
        renamed:    sample/test2.tsx -> Sample/test2.tsx

これをcommitすれば完了です!

まとめ

今後、ディレクトリの大文字小文字を変更するときは、git ls-filesでGitのインデックス上で正しくrenameとして認識されているかどうか常に確認するよう気をつけます。

また、チームで作業している場合に、別の人がこの変更をpullしたときも同様に大文字小文字が正しく変更されないので、事前に前述の手順でインデックス内のディレクトリの大文字小文字を変更しておく必要があります。

同じような事象で詰まっている方の助けになれば幸いです。

Github Actionsを触る

業務の中で特定のリポジトリへpushした際に任意の処理を行う必要が出てきました。 そういう時に活躍するのが「Github Actions」。 もちろんpushだけに限らず、特定のリポジトリへpushされた時や、毎日決まった時刻に任意の処理(テストしたり、ビルドしたり etc....)を行いたい時にも使えます。 しかもこれらの処理を自前でサーバーを用意しなくてもGitHubを利用する全てのユーザーが使用できるという優れもの。

またGithub ActionsはDevOpsを実現するための手段として主流になってきており、GitHubユーザーにとってはCircle CIやJenkinsを利用しなくてもGithub上でDevOpsを完結できることからも、一度は触って損は無い言えるかもしれません。

Github Actionsで「Hello World

それでは早速Github Actionsを使ってGithubのlog上に「Hello World」を出力していきましょう。 https://docs.github.com/en/actions/quickstartdocs.github.com

類似サービスと比較してみた所感

UI/UX的な観点で類似サービスと比較して時にネガティブな印象を受けたという方の情報をちらほら見たのですが、 個人的には不備をあまり感じなかった印象でした。 また、気になるセキュリティの観点に関しては少しづつ改善がされているようなので、 仕事以外の個人開発などで色々試してみるのも面白いかもしれません。

GitHub Actions のセキュリティ強化 https://docs.github.com/ja/actions/security-guides/security-hardening-for-github-actionsdocs.github.com

利用料金に関しても、MicrosoftによるGithub買収に伴い資金が豊富にあるためか、 公式ドキュメントに以下のような記載があるように、機能をほぼ無料で利用することが可能なため、 類似サービスと比較しても魅力を感じますよね。 ※ 2022/01時点ではパブリックリポジトリとセルフホストランナーでは、どちらも利用は無料。 プライベートリポジトリでは、それぞれのGitHubアカウントは使用している製品に応じて一定量の無料の分とストレージを受け取れる。

アカウントに含まれるストレージや利用時間 (分) を超えてGitHub Actionsを使用したい場合は、追加の使用分が請求されます。

GitHub Actionsの支払いについて https://docs.github.com/ja/billing/managing-billing-for-github-actions/about-billing-for-github-actionsdocs.github.com

類似サービスに比べてもユーザー数では圧倒的な数を誇るGithubなので、 データを元にした独自の機能アップデートの利点をほぼ無料で享受できるのは最高に嬉しいですよね。 ※ 以下記事はレポジトリ情報から適切なリコメンデーションをしてくれる機能に関するアップデート情報です。 Getting started with GitHub Actions just got easier!

github.blog

スポンサー限定公開のリポジトリ、金額のカスタム設定などGitHub Sponsorsに新機能が登場

Github Actionsとは話が外れますが、Github Sponsorsからアップデートがありましたね。

スポンサー限定公開のリポジトリに金額の設定ができるようになり、プラットフォームサービスではスタンダードになっている収益方法(スポンサーに何か特別なものを提供することで、サブスクリプションインセンティブ得る的な)をオープンソースの世界に導入する試みは、オープンソースの思想と反してしまうのでは?と疑問視する人も多いのではないでしょうか。

個人的には、スポンサー限定公開のリポジトリに利便性高めのワークフローなんかを複数パッケージングしておいて、 有料で利用できるようにするスモールサービスが流行りそうだなと推測してたりします。 (スタートアップサービスの資金調達方法として充分にスタンダートになり得る!?)

github.blog

最後に

既に開発をする上で必須ツールと言っても過言ではないGithubから便利が機能がリリースされることは嬉しいです。が、 最初は無料で利用させて類似サービスが淘汰された後に、急に利用料金を値上げ!されても嫌だなと、邪推してしまいながら Githubにお世話になりまくるのでした。

【Next.js × CSS Modules × ReactBootstrap】CSSフレームワークの管理から外れずにカラー変数を定義して、エンジニア、デザイナー間で共通認識を取る

エンジニアの三浦です。

業務で新規サービスを構築する中で、カラーコードの定義方法のベストプラクティスがわからず、手戻りが発生するケースがあったので、自戒も込めて記事に残します。

経緯

基本的にWebアプリは特定の色に制限して配色を決定します。 よって開発着手段階で、エンジニア、デザイナー間でどのカラーコードをどのくらいの割合で使用するのか共通認識をとっておくことで、画面設計通りのWebアプリ開発が可能になります。

弊社では基本的にデザイナーが作った画面設計を元にエンジニアがデザインも込みで実装を進めるのですが、ここで以下の問題が発生しました。

  • ① デザイナーが画面設計内で定義しているカラー変数とエンジニアが使っているカラー変数が異なる
  • ② エンジニアがカラーコードを直接CSSで当てている箇所があり保守が大変になる
  • CSSフレームワークが提供するコンポーネントを使用している箇所で意図しない色が発生する

何が問題なのか

一番の問題はデザイナーが定義している画面設計と異なる色が使われてしまうことです。これによって画面設計段階のUIが保たれず、成果物のUXを損ねかねません。

解決策

①は、CSSフレームワークで表現できるカラー変数おけるデザイナーの認識が異なるためです。カラー変数を実際に使うのはエンジニアなので、認識違いを発見した都度デザイナーに確認をとることで回避できます。

②、③は、エンジニアがカラーコードの直接指定をせず、カラー変数を使用して色をあてることで回避できます。ここでのカラー変数は個別で新しく変数を作成するのではなく、CSSフレームワーク内で使われているカラー変数を上書きします。

ここで③について詳しく見ていきます。

③の問題の再現

(使用した環境は記事末尾に記載しています)

  • Next.jsのプロジェクトをTypeScriptオプションで作成します。
$  yarn create next-app --typescript
  • プロジェクトの名前は仮にverify_css_modules_behavior_appとします。
  • 作成したアプリのディレクトリに移動して立ち上げます。
$ cd verify_css_modules_behavior_app
$ yarn dev
  • 必要なパッケージを追加します。
$ yarn add bootstrap 
$ yarn add react-bootstrap
$ yarn add sass
  • index.tsxを以下に修正します。
// index.tsx
import type { NextPage } from 'next'
import { Button } from 'react-bootstrap'

const Home: NextPage = () => {
  return (
    <div className='d-flex justify-content-center align-items-center min-vh-100'>
      <Button
        variant='primary'
      >
        テスト
      </Button>
    </div>
  )
}

export default Home
  • globals.cssの拡張子をscssファイルに変更して、中身を書き換えます。
// globals.scss
@import '~bootstrap/scss/bootstrap';
  • _app.tsxのインポートを更新します。
// _app.tsx
import '../styles/globals.scss' // 変更
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default MyApp

  • BootstrapのPrimaryのボタンが表示されるのが確認できます。
  • ここで、Bootstrapで定義されているprimaryの変数を更新してみます。
// globals.scss
$primary: red;

@import '~bootstrap/scss/bootstrap';

:root {
  $primary: $primary;
}

  • Primaryが更新されてボタンが赤色に変わりましたが、ボタン内のテキストの色が何故か白から黒に変わっているのがわかります。
  • このようにCSSフレームワークコンポーネントは、理想的な配色がされるようにチューニングされています。

CSSフレームワークの管理から外れるとは?

  • 自前で新しい変数を定義したり、カラーコードを直接指定したりすることです。
  • CSSフレームワークの管理から外れてカラーコードを設定することで、意図しない場所へ影響が出てしまいます。
  • Bootstrapで定義されている変数の一覧は以下なので、これを上書きして使うようにします。 github.com

Bootstrapの変数を上書きする具体例

// scss-docs-start theme-color-variables
$primary:       $blue !default;
$secondary:     $gray-600 !default;
$success:       $green !default;
$info:          $cyan !default;
$warning:       $yellow !default;
$danger:        $red !default;
$light:         $gray-100 !default;
$dark:          $gray-900 !default;
// app.scss
$font-family-sans-serif: 'Noto Sans JP', 'Yu Gothic', '游ゴシック', YuGothic,
  '游ゴシック体', 'ヒラギノ角ゴ Pro W3', 'メイリオ', sans-serif;
$primary: #69709e;
$secondary: #00a0e9;
$purple: $primary;
$danger: #e95384;
$warning: #f6ab00;

@import '~bootstrap/scss/bootstrap';

:root {
  @each $color, $value in $purples {
    --#{$variable-prefix}#{$color}: #{$value};
  }
  @each $color, $value in $grays {
    --#{$variable-prefix}#{$color}: #{$value};
  }
}

まとめ

以上のように、アプリで使用するCSSフレームワークに応じて、都度エンジニア、デザイナー間でカラー変数の認識を合わせて開発を進めることで、手戻りなく画面設計通りのアプリ開発ができるようになると思います。

初めて使うフレームワークだと、このカラー変数の定義方法の調査を後回しにして、カラーコード直指定で開発を進めてしまうケースがありました。100%徹底する必要はないかもしれませんが、将来的な手戻りを防止するためにも気をつけていきたいです。

環境

  • Next.js
  • ReactBootstrap

CORS(Cross-Origin Resource Sharing)とSOP(Same-origin policy)について

こんにちは。エンジニアの岸本です。

個人的な開発をしていて、linkタグで @font-face を使った Web フォントの読み込みをする際に「crossoriginが付与されていませんよ!」警告を頂戴した。何となくで理解していたcrossorigin属性の存在意義を調べるうちにCORS(Cross-Origin Resource Sharing)とSOP(Same-origin policy)について改めて学習する機会になったのでまとめてみることしました。

「crossorigin」とは一体なに?

「CORS(Cross-Origin Resource Sharing)設定要素として定義されており、CORSの振る舞いに関する設定ができる」

「crossorigin」の存在意義はズバリ!

「セキュリティリスクを減らしてくれる凄いやつ」 CORSを利用することでセキュリティリスクを回避できることは解ったが、 実際にどのような機能なのか釈然としなかったのでCORSについておさらいします。

CORS(Cross-Origin Resource Sharing)とは?

別オリジンのリソースへアクセス(HTTPリクエスト)できるようにするために、SOP(Same-origin policy)の機能をカバーする形で誕生した仕組み

簡単な具体例

CORSが生まれた背景

CORSとは異なるオリジンへのアクセスを許可する方法です。それだけを聞いても何がすごいのか解らないですよね。 CORSもSOPも存在しない仮想ケースで考えてみます。 img_2 上記のように、なりすましメールや悪意あるサイトをクリックしてしまうことで スクリプトがダウンロードされ、攻撃対象へリクエストが送信されてしまう可能性があります。 そこで生まれたのがSOP(Same-origin policy)です。 同じオリジンじゃなければ不正とみなすことで上記リクエストによる被害防止につながります。(実際にアクセス自体は可能だがレスポンスの取得が制限される) img_1

しかし別の問題が発生しました。 Ajaxの普及・発展によって異なるオリジン(ほとんどの場合、異なるホスト)のAPIを利用したいという動機が生まれたので SOPだけでは役割を満たすためには不十分になってしまいました。(厳密にはJSONPという裏技レベルの解決策もあったが安全性的に十分ではなかった模様)そこで活躍するのがCORS(Cross-Origin Resource Sharing)です。 HTTPヘッダを用いたアクセス制御のコントロールを行うことで、SOPでは満たせなかった柔軟性をカバーすることが可能になりました。

というのが、ざっくりとしたCORSとSOPの説明になります。 linkタグ等々に付与しているcrossorigin属性に話を戻します。

結論から言うと、以下のケースはSOPが適用されないため、「異なるオリジンへアクセスできるようにCORSを考慮する必要が基本的にはない」ということになります。

developer.mozilla.org

  • imgタグのsrc属性で読み込んだ画像
  • linkタグのhref属性で読み込んだ CSS
  • scriptタグのsrc属性で読み込んだ JavaScript
  • video, audioタグのsrc属性で読み込んだマルチメディアファイル
  • iframe, frameタグのsrc属性での別サイトコンテンツの読み込み
  • @font-face が適用されたフォント ※ X-Frame-Options の設定によっては読み込みがブロックされます

自分のケースではMDNにも記載があるように、「異なるオリジンのフォントを許容するブラウザもありますが、同一オリジンを要求するものもある 」ことを想定してcrossoriginを付与するように警告を頂戴したようです。

おまけ

「scriptタグのsrc属性で読み込んだ JavaScript」にはSOPが適用されないと言っても実際にはcrossorigin属性を付与されているscriptタグやimgタグを見ることはありますよね。個人的には以下のようなケースが想定されるからと理解しています。

「crossorigin属性をつけない限りSOP/CORSの制約は発生しない。つまり、CORSを経由せずに取得された場合は汚染フラグ (taint flag) が立てられ、スクリプトから利用したときに一定の制限が課される(<script> が例外を発生させたときに window.onerror が受け取るエラー情報が制限される等)からcrossorigin属性を付与っている方がベター」

React Bootstrap(v2.1.0)のAccordionで複数のAccordion Collapseを同時に開閉するボタンを作る

こんにちは。RITの関です。

React BootstrapのAccordion.Collapseを同時に開閉するボタンを作るという実装に少し苦労したので、その方法を紹介します。 ※この記事は下記記事のreact-bootstrap 2.~版の実装方法です。react-bootstrap 1.6~版を使われている方は下記を参考にしてください

rit-inc.hatenablog.com

完成イメージ

完成イメージ

まずはシンプルなAccordionを作る

test_table.tsx

import { TestList } from '@/Presentation/Component/Home/TestList';
import { NextPage } from 'next';
import React from 'react';

const TestTable: NextPage = () => {
    return (
        <div>
            <div className='mb-4 d-flex align-items-center justify-content-between'>
                <p className='mb-0 font-weight-bold'>コンタクト一覧</p>
            </div>
            <div>
                {[
                    'accordionTextあああああ',
                    'accordionTextいいいいい',
                    'accordionTextううううう',
                ].map((text, index) => (
                    <div key={index}>
                        <TestList
                            eventKey={index.toString()}
                            accordionText={text}
                        />
                    </div>
                ))}
            </div>
        </div>
    );
};

export default TestTable;


export default TestTable;

test_list.tsx

import React, { FunctionComponent, useState } from 'react';
import { ListGroup, Accordion, Card } from 'react-bootstrap';
import { useAccordionButton } from 'react-bootstrap/AccordionButton';

interface Props {
    eventKey: string;
    accordionText: string;
}

type ToggleProps = {
    children: JSX.Element;
    eventKey: string;
};

export const TestList: FunctionComponent<Props> = props => {
    const [isShow, setIsShow] = useState(true);

    function CustomToggle({ children, eventKey }: ToggleProps) {
        const decoratedOnClick = useAccordionButton(eventKey, () => {
            setIsShow(!isShow);
        });
        return (
            <button type='button' onClick={decoratedOnClick}>
                {children}
            </button>
        );
    }

    return (
        <Accordion activeKey={isShow ? props.eventKey : ''}>
            <Card className='border-0 rounded-0'>
                <CustomToggle eventKey={props.eventKey}>
                    <div className='d-flex justify-content-between align-items-center'>
                        {props.accordionText}
                    </div>
                </CustomToggle>
                <Accordion.Collapse eventKey={props.eventKey}>
                    <ListGroup>
                        <p>リストの中身だよ</p>
                    </ListGroup>
                </Accordion.Collapse>
            </Card>
        </Accordion>
    );
};

React Bootstrapのチュートリアルに書いてあるようなシンプルなアコーディオンを作成しました。

簡易アコーディオン

諸々編集する

次は、諸々更新したtest_tableをまとめて載せます。大きくやったことは下記です。

①openというstateの追加
AccordionOpenという型定義を追加して、openというstateを追加しました。
openの中身は全てを開くか全てを閉じるか判断するisOpenと、ボタンを2回以上連続クリックしても反応してくれるようにtimestampを入れています。
もしtimestampを入れない場合、isOpenの値が変化しないことから、全てを開くボタンが動作してくれなくなります。

②test_listへ渡すpropsにopenを追加
test_listがopenを受け取れるようにして、受け取ったpropsによって全アコーディオンを開閉するようにしています。

③test_list自体にisShowというstateを追加して、Accordion自体をisShowを元に開閉するように変更
Accordion activeKey={isShow ? props.eventKey : ''}と定義することで、isShow: trueのときはactiveKeyとprops.eventKeyが同じ値になるので開く、isShow: falseのときはactiveKeyとprops.eventKeyが異なる値になるので閉じるようになります。
この開閉の仕組自体はreact-bootstrap自体によるものなので、詳細は調べてみてください。

④test_listにuseEffectを追加
test_listにuseEffectを追加して、props.openが変わるごとにisShowの値をprops.openと同じにするようにしています。
これによって、全てを開くボタンを押した時に、openの値がtrueになる→test_listのisShowもtrueになる→アコーディオンが開くという処理ができるようになります!

test_table.tsx

export interface AccordionOpen {
    isOpen: boolean;
    timestamp: number;
}

const TestTable: NextPage = () => {
    const [open, setOpen] = useState<AccordionOpen>({
        isOpen: true,
        timestamp: new Date().getTime(),
    });

    return (
        <div>
            <div className='mb-4 d-flex align-items-center justify-content-between'>
                <p className='mb-0 font-weight-bold'>コンタクト一覧</p>
                <div>
                    <Button
                        variant='outline-primary mr-3'
                        onClick={() =>
                            setOpen({
                                isOpen: true,
                                timestamp: new Date().getTime(),
                            })
                        }
                    >
                        すべてを表示
                    </Button>
                    <Button
                        variant='outline-primary'
                        onClick={() =>
                            setOpen({
                                isOpen: false,
                                timestamp: new Date().getTime(),
                            })
                        }
                    >
                        すべてを閉じる
                    </Button>
                </div>
            </div>
            <div>
                {[
                    'accordionTextあああああ',
                    'accordionTextいいいいい',
                    'accordionTextううううう',
                ].map((text, index) => (
                    <div key={index}>
                        <TestList
                            eventKey={index.toString()}
                            accordionText={text}
                            open={open}
                        />
                    </div>
                ))}
            </div>
        </div>
    );
};

export default TestTable;

test_list.tsx

interface Props {
    eventKey: string;
    accordionText: string;
    open: AccordionOpen;
}

type ToggleProps = {
    children: JSX.Element;
    eventKey: string;
};

export const TestList: FunctionComponent<Props> = props => {
    const [isShow, setIsShow] = useState(true);

    useEffect(() => {
        setIsShow(props.open.isOpen);
    }, [props.open]);

    function CustomToggle({ children, eventKey }: ToggleProps) {
        const decoratedOnClick = useAccordionButton(eventKey, () => {
            setIsShow(!isShow);
        });
        return (
            <button type='button' onClick={decoratedOnClick}>
                {children}
            </button>
        );
    }

    return (
        <Accordion activeKey={isShow ? props.eventKey : ''}>
            <Card className='border-0 rounded-0'>
                <CustomToggle eventKey={props.eventKey}>
                    <div className='d-flex justify-content-between align-items-center'>
                        {props.accordionText}
                    </div>
                </CustomToggle>
                <Accordion.Collapse eventKey={props.eventKey}>
                    <ListGroup>
                        <p>リストの中身だよ</p>
                    </ListGroup>
                </Accordion.Collapse>
            </Card>
        </Accordion>
    );
};

最後に動きのイメージを貼って終わりにします。react-bootstrapを使うときの役に立てると幸いです。ありがとうございました!

完成イメージ

完成イメージ

OpenClosedの原則とは何か

エンジニアの前田です。

概要

OpenClosedの原則について、wikipediaではこう↓あります

ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。 わかりやすく説明すると、機能追加や修正の際に対象の箇所以外の既存のソースコードを変更する必要がないプログラムのことです。 ポリモーフィズムを使用する方法が、OpenClosedの原則に従った実装の典型としてよくあげられています。

悪い例から示します。 全自動で飲み物をしてくれるAutoTakeDrinkクラスがあります。 要件として料理に合わせた飲み物を提供する必要があります。 以下、疑似コードです。

class AutoTakeDrink {
  public takeDrinks(foods) {
    const drinks = foods.map(f => {
        if (instansof food === 'ChineseFood') {
          return new ChinaTea();
         } else if (instansof food === 'ItalianFood') {
           return new Wine();
         } else if (instansod food === 'JapaneseFood') {
           return new JapanTea();
         }
       });
    return drinks;
  }
}

上のやり方だと、料理の種類が増えるたびにAutoTakeDrinkクラスのtakeメソッドのif文の分岐を追加する必要が出てきます。 料理の種類を追加がAutoTakeDrinkクラスにも影響を及ぼしてしまうのでOpenClosedの原則に従った実装ではありませんね。

以下にOpenClosedの原則に従った実装を示します。

// まずは各料理クラスが共通のインターフェイス、Foodに従って実装するようにします。
// Foodインターフェイスに従っているクラスは必ずsuitableDrinkメソッドを実装する必要があります。
interface Food {
  public suitableDrink(): Drink
}

class ItalianFood implements Food {
  suitableDrink() {
    new Wine();
  }
}

class ChineseFood implements Food {
  suitableDrink() {
    new ChinaTea();
  }
}

class JapaneseFood implements Food {
  suitableDrink() {
    new JapanTea();
  }
}

class AutoTakeDrink {
  public takeDrinks(foods: Food[]) {
    const drinks = foods.map(f => {
       return f.suitableDrink();
     });
    return drinks;
  }
}

各料理クラスが飲み物を返すsuitableDrinkメソッドを実装することで、 新しい料理を追加した時にAutoTakeDrinkクラスにif文の分岐を追加する必要がなくなりました。 以上が、ポリモーフィズムを使用しOpenClosedの原則に従った実装をした例です。