TECH PLAY

WESEEK, Inc.

WESEEK, Inc. の技術ブログ

75

はじめに こんにちは、システムエンジニアの kota です。 今回は、個人的にまだ触ったことのなかった GraphQL に入門してみたので、ブログとしてまとめてみました。 1. GraphQL とは? GraphQLは、APIのためのクエリ言語であり、データ駆動型のアプリケーションを構築するためのランタイムです。Facebookが開発し、2015年にオープンソースとして公開されました。 これはエンドポイントに対するクエリを行うREST APIとは異なり、クライアント側で必要なデータを具体的に指定できるため、ネットワークの使用効率を最適化し、パフォーマンスを向上させることができます。また、APIのバージョン管理の問題を解消し、APIとフロントエンドの開発をデコープル(分離)することを可能にします。 2. GraphQL が登場するまでの技術 GraphQLが登場する以前、Webアプリケーションのデータ交換の主流はRESTfulなAPIでした。RESTはHTTPプロトコルに厳密に従い、エンドポイントを利用してリソースにアクセスします。 しかしながら、このRESTfulなアプローチには幾つかの問題がありました。 1つ目は「オーバーフェッチング」や「アンダーフェッチング」という問題です。「オーバーフェッチング」はクライアントが必要なデータよりも多くのデータを取得することを指し、無駄なネットワークと計算リソースを消費します。一方、「アンダーフェッチング」はクライアントが必要な全てのデータを一度に取得できず、複数のリクエストを行う必要があることを指します。 2つ目は、リソースごとに独立したエンドポイントが必要となるため、APIが大きくなるにつれてエンドポイントの数が増え、管理が難しくなるというエンドポイントの複雑性です。また、複数のエンドポイントからデータを取得する際には、複数のリクエストが必要となります。 3つ目は、データの形式や構造が変わる度に新しいバージョンのエンドポイントを作成する必要があるため、APIのバージョニングが困難になるというバージョニング問題です。 4つ目は、エンドポイントの設計がクライアントが必要とするデータの構造に大きく依存するため、クライアントとサーバー間の強い結合を意味します。これはフロントエンドとバックエンドの開発を同時に進行させることが難しくなるという問題です。 これらの問題は、GraphQLが登場することで大きく改善されました。具体的には、GraphQLでは1つのエンドポイントで複数のリソースを取得でき、バージョニングの必要性を排除し、クライアントとサーバーのデコープリングを可能にします。 3. GraphQL の登場で何が変わったのか? GraphQLが登場すると、上記のような問題を大きく緩和することができました。それは、GraphQLがクライアントにデータの特定と取得の精度を提供するからです。 具体的には、GraphQLはクライアントに対してどのフィールドを取得するかを指定する能力を与え、これにより必要なデータのみを正確に取得できるようになりました。これにより、ネットワークの使用効率が向上し、不要なデータの取得によるリソースの浪費を避けることができます。 また、GraphQLはクエリ言語としての機能を持つため、複雑なデータの取得やデータの絞り込みも可能となりました。これらの要素は、APIの柔軟性とパフォーマンスを向上させる上で非常に重要な要素です。 4. GraphQL vs REST: それぞれの違いと利点 RESTとGraphQLはどちらもAPIを構築するための技術ですが、以下のような違いがあります。 まず、RESTではエンドポイントごとに固定のデータ構造が返されます。これはシンプルで理解しやすい一方で、前述の「オーバーフェッチング」や「アンダーフェッチング」の問題が生じやすいです。 それに対し、GraphQLはクライアントが必要なデータを自由に指定できるため、不要なデータの取得を避けることができます。これは効率的なネットワーク利用と高いパフォーマンスに繋がります。 さらに、RESTでは複数のエンドポイントを跨いでデータを取得する必要がある場合、複数のリクエストを行う必要があります。一方のGraphQLでは、1つのリクエストで複数のリソースを取得できます。これにより、リクエストの数を大幅に減らすことが可能になりました。 コードの比較 GraphQLとREST APIの違いを理解するために、簡単な例を用いてそれぞれのリクエストとレスポンスを比較してみます。 例えば、あるユーザーの情報とそのユーザーが投稿したブログ記事のタイトルを取得したいとします。それぞれのAPIスタイルでどのように行うか見てみましょう。 REST API REST APIでは、通常は各リソースに対して個別のエンドポイントがあります。まずユーザーの情報を取得し、その後、そのユーザーの投稿を取得するために、別々のリクエストを行うことになります。 1つ目のリクエスト(ユーザー情報の取得): GET /users/<ID> レスポンス: { "id": "<ID>", "name": "Alice", "age": 20 } 2つ目のリクエスト(ユーザーの投稿の取得): GET /users/<ID>/posts レスポンス: [ { "id": "1", "title": "First Post", "content": "Hello, world!" }, { "id": "2", "title": "Second Post", "content": "Another post" } ] GraphQL 一方、GraphQLでは、一つのリクエストで必要なすべての情報を取得することが可能です。また、クライアントは必要なデータの形状と量を正確に指定することができます。以下にその例を示します。 リクエスト: { user(id: "<ID>") { id name age posts { id title content } } } レスポンス: { "data": { "user": { "id": "<ID>", "name": "Alice", "age": 20, "posts": [ { "id": "1", "title": "First Post", "content": "Hello, world!" }, { "id": "2", "title": "Second Post", "content": "Another post" } ] } } } このように、GraphQLは一つのリクエストで複数の関連するリソースを取得することができ、またクライアントは必要なデータを正確に指定できるという利点があります。これに対し、REST APIでは複数のリクエストが必要になることがあり、またサーバー側で定義された形状と量のデータが返されるという特性があります。 5. GraphQL の主要な機能(クエリ、ミューテーション、サブスクリプション) GraphQLの主要な機能はクエリ、ミューテーション、そしてサブスクリプションです。 クエリ(Query) はデータの読み取りを行います。以下はGraphQLクエリの例です: query { user(id: 1) { name email } } クエリは、idが1のユーザーのnameとemailフィールドを取得します。queryキーワードは省略可能ですが、記載しておくことで、リーダブルでわかりやすくなります。userはフィールド名であり、この場合のidは引数を表します。引数を使うことで、特定のユーザーや投稿などを取得できます。 ミューテーション(Mutation) はデータの変更を行います。これにはデータの作成、更新、削除が含まれます。以下はミューテーションの例です: mutation { createUser(input: {name: "John", email: "john@example.com"}) { id name email } } ミューテーションは、createUser関数を呼び出して新しいユーザーを作成します。input引数を使ってユーザーのnameとemailを提供します。この関数は新しく作成されたユーザーのid、name、emailを返します。ミューテーションはデータの作成、更新、削除など、データを変更する操作に使われ、必ずmutationキーワードで始まります。 サブスクリプション(Subscription) はリアルタイムのデータの更新を行います。サーバー側のイベントがトリガーされた時にクライアントに通知されます。以下はサブスクリプションの例です: subscription { userCreated { id name email } } サブスクリプションは、新しいユーザーが作成されたときにリアルタイムでそのid、name、emailをクライアントに通知します。サブスクリプションは、サーバー上で特定のイベント(この場合は新しいユーザーの作成)が発生したときにクライアントに通知するために使用されます。サブスクリプションはsubscriptionキーワードで始まります。 6. GraphQL のスキーマと型システムについて GraphQLは、APIを通じて利用可能なデータの形状と相互作用を明確に定義するための強力な型システムを提供します。これは「スキーマ」として知られています。スキーマは、クライアントがサーバーから取得できるデータと、そのデータがどのように相互作用するかを定義します。 型システム このスキーマは、特定の形状や特性を持つオブジェクトを表す「型」で構成されています。型は、単純なスカラー型(String、Int、Boolean、Float、ID)から、より複雑なオブジェクト型、列挙型、インターフェース型、ユニオン型など、様々な種類があります。これらを組み合わせて、APIが表現するデータの全体像を形作ります。 例えば、ブログシステムのAPIを想像してみてください。このシステムでは、スキーマはおそらく「ユーザー」、「記事」、「コメント」などのオブジェクト型を持つでしょう。それぞれの型はそれぞれ、名前や年齢(ユーザー)、タイトルや内容(記事)、本文(コメント)など、さまざまなフィールドを持つことでしょう。以下に具体的なコードを示します。 type User { id: ID! name: String! age: Int! posts: [Post!]! } type Post { id: ID! title: String! content: String author: User! comments: [Comment!]! } type Comment { id: ID! text: String! post: Post! author: User! } これらの型は、スカラー型や他のオブジェクト型を持つことができます。この例では、 User 型の posts フィールドは Post 型のリストを持つことを示しており、その逆に、 Post 型の author フィールドは User 型を持つことを示しています。 特に、 [Post!]! のような表記について説明します。この表記は Post 型の配列を示していますが、 ! が二つ存在し、それぞれ異なる意味を持っています。 1つ目の ! は Post オブジェクトが必須(null不可)であることを示します。これは、配列が存在する場合、その中に含まれる全ての要素が Post 型のオブジェクトでなければならず、null要素は許容されないことを意味します。 2つ目の ! は、配列自体が必須(null不可)であることを示します。つまり、 posts フィールドは常に Post 型の配列を返すべきで、その配列自体がnullになることは許されません。 従って、 [Post!]! は、配列自体が必ず存在し、その配列の中身は1つ以上の Post 型のオブジェクトで構成され、その中にnullが含まれることはない、という意味を持ちます。これにより、各フィールドの型とそのnull許容性を詳細に定義することができます。 リレーションシップ このようにして、GraphQLのスキーマは、データ間のリレーションシップも表現します。たとえば、「ユーザー」は複数の「記事」を持つことができ、それぞれの「記事」は複数の「コメント」を持つことができます。これらのリレーションシップは、オブジェクト型のフィールドを通じて表現されます。このリレーションシップは、クライアントが必要なデータだけを選択して取得することを可能にします。 スキーマ定義言語(SDL) GraphQLのスキーマは、スキーマ定義言語(SDL)と呼ばれる特殊な構文を使用して記述されます。このSDLはGraphQLのデータ構造を定義するための言語で、上記の User や Post 型のような各種のデータ型を定義することが可能です。 型定義では、型名の後に続く括弧内にフィールドを定義し、各フィールドの後ろにはそのフィールドの型を指定します。フィールド名の後ろに ! をつけると、そのフィールドが必須(null不可)であることを示します。また、型名の前に [ ] をつけると、その型の配列を示します。 また、スキーマには以下の特別な型が存在します: Query 型: クエリのエントリポイントとなります。クライアントがデータを読み取るときに使用します。 Mutation 型: データの変更(作成、更新、削除)を扱います。クライアントがデータを変更するときに使用します。 Subscription 型: リアルタイムの更新を監視します。クライアントがデータのリアルタイムな変更を追跡するときに使用します。 これらの特別な型を使うことで、クライアントはサーバーに対してデータの読み取り、変更、リアルタイムの監視といった様々な操作を要求することが可能になります。 type Query { users: [User!]! user(id: ID!): User posts: [Post!]! } type Mutation { createUser(name: String!, age: Int!): User! createPost(title: String!, content: String, authorId: ID!): Post! } type Subscription { postCreated: Post! } 上記の例では、 Query 型が定義されており、 users 、 user 、 posts といったフィールドを持っています。これらのフィールドはそれぞれ、全ユーザーのリスト、指定したIDのユーザー、全投稿のリストを返します。 一方、 Mutation 型は新しいユーザーや投稿を作成するためのフィールドを定義しており、それぞれ createUser 、 createPost という名前で、必要なパラメータと共に定義されています。これらの操作により、クライアントは新しいユーザーや投稿を作成することができます。 Subscription 型は新しい投稿が作成されたときにその情報をリアルタイムに提供するためのフィールド postCreated を定義しています。これにより、クライアントは新しい投稿の作成をリアルタイムで監視することができます。 これらの型とリレーションシップを利用することで、クライアントは必要なデータを正確にクエリすることができ、サーバーはクエリの妥当性を検証し、正確なデータを返すことができます。これはGraphQLのスキーマと型システムの強力さを示しています。 参考記事 https://graphql.org/ https://hasura.io/learn/ja/graphql/react/intro-to-graphql/ https://zenn.dev/overflow_offers/articles/20220609-graphql-onboarding https://qiita.com/jintz/items/c9105dca1725224d36a8 https://reffect.co.jp/html/graphql/ まとめ 以上が、GraphQLの基本についての解説でした。次回の後編では、より詳細なGraphQLの使い方や応用について解説します。
アバター
はじめに ブラウザの開発者ツールはウェブ開発において必要不可欠なツールです。 開発者ツールを上手に使うことで、ウェブサイトのデバッグやパフォーマンスの最適化、アクセシビリティの向上など多くのことが行えます。 本記事では、ブラウザの開発者ツールの基本的な使い方や様々なテクニックについて解説します。 開発者ツールとは何か 開発者ツールとは、ウェブサイトのデバッグやパフォーマンスの最適化、アクセシビリティの向上など役立つ機能を提供するツールのことです。 主要なブラウザ(Google Chrome, Mozilla Firefox, Microsoft Edgeなど)は開発者ツールを標準搭載しています。 主要ブラウザ共通の開き方 主要なブラウザ共通で「F12」キーを押すか、もしくは「Ctrl + Shift + I」キーを押すことで開発者ツールを開けます。 Google Chrome での開き方 右上の三点ドットから、[その他のツール] > [ディベロッパーツール]を選択すると開発者ツールを開けます。 Mozilla Firefox での開き方 右上のハンバーガーメニューから、[その他のツール] > [ウェブ開発ツール]を選択すると開発者ツールを開けます。 Microsoft Edge での開き方 右上の三点ドットから、[その他のツール] > [開発者ツール]を選択すると開発者ツールを開けます。 Element タブ 要素の表示・編集 Elements タブは、ウェブサイトのHTMLやCSSを表示したり編集する機能を持っています。 このタブではウェブページの構造やスタイルを確認できます。 また右上に表示されている HTML 要素をダブルクリックすることで、その要素自身や要素の中身・クラス・属性などを自由に編集できます。 下の Styles では CSS の style を編集できます。 Elements タブ内の Computed タブでは、選択した要素に適用されている実際の CSS プロパティの値を確認できます。 インスペクトモード 開発者ツール内の一番左上にあるボタンを押すことでインスペクトモードを起動できます。 インスペクトモード起動後、マウスカーソルを移動させて Web ページ上の選択したい要素をクリックすることで、選択された要素が Elements タブに表示され、その要素が HTML でどのようにマークアップされているかを確認できます。 Console タブ JS コード実行 Console タブに JavaScript のコードを入力して Enter キーを押すことで、JavaScript コードが実行され、その実行結果を閲覧できます。 メッセージ表示 Console タブでは Web ページのエラーや警告などのメッセージが表示されます。 エラーが発生した場合、エラーメッセージとともにエラーが発生したスクリプトの行数やファイル名も表示されるため、デバッグに役立ちます。 Sources タブ ソースコードの表示・編集 Sourceタブでは、Web ページの HTML, CSS, JavaScript などのソースコードを表示できます。 ソースコードはフォルダ階層のように表示され、各ファイルを展開することで中身を確認できます。 検索窓を使用してコードを検索することもできます。 また、HTML, CSS, JavaScript などのソースコードを直接編集することができます。 エディタを使用してコードを編集し保存することで、変更内容を即座に反映させられます。 ブレークポイント設定 Source タブでは、ブレークポイントを設定できます。 ブレークポイントは JavaScript の実行を一時停止し、デバッグを行うためのものです。 ソースコード上の任意の行数をクリックすることで、その箇所にブレークポイントを設定できます。 ブレークポイントを設定した後に画面をリロードしファイルを実行させると、ブレークポイントを設定した位置で JavaScript の実行が停止され、その時の変数の中身や画面の状態を確認できます。 変数の中身は、右側の Scope パネルの中だったり、ソースコード上の変数にマウスを重ねることで確認できます。 Network タブ Network タブには、Web ページが発行した全てのネットワークリクエストの詳細情報が表示されます。 リクエストの URL やメソッド、HTTP ステータスコードなどの基本情報はもちろん、リクエストのヘッダーやレスポンスボディなどの詳細情報も確認できます。 タイムラインの使い方 Networkタブには、ネットワークリクエストとそのレスポンスのタイムラインが表示されます。 タイムラインには、Webページの読み込みやリクエストの処理時間、リソースのダウンロードなどの情報が視覚的に表示されます。 各リクエストは水平方向のバーとして表されます。 バーの位置や長さは、リクエストの発行から応答の受信までの時間や処理時間を表しており、色や形状はリクエストの種類やステータスによって変わります。 Application タブ Applicationタブでは、ウェブアプリケーションのローカルストレージ、セッションストレージ、Cookie、Service Workerなど、ブラウザが提供するさまざまなリソースやストレージ情報を管理・操作できます。 Storage 内の項目をクリックすることで、そのストレージに保存されたデータのキーや値などを確認できます。 また、データの追加や削除、編集などの操作も行うことができます。 その他機能 表示デバイス変更 開発者ツール左上のデバイスアイコンをクリックすることで、ウェブページを別デバイスでの表示に切り替えてプレビューできます。 デバイスツールバーには、さまざまなデバイスのプリセットが用意されており、左側のドロップダウンメニューをクリックすると、利用可能なデバイスの一覧が表示されます。 一覧からデバイスを選択すると、ウェブページの表示が選択したデバイスのサイズや解像度に合わせて変更されます。 デバイスを選択するのではなく、縦横のサイズを指定してプレビューすることもできます。 フルサイズスクリーンショット 開発者ツールでは、画面のフルサイズのスクリーンショットを撮影できます。 やり方は先ほどと同様に、開発者ツール左上のデバイスアイコンをクリックしてデバイスツールバーを表示させます。 その後三点ドットから「Capture full size screenshot」を選択するとフルサイズのスクリーンショットを撮影できます。 開発に役立つ拡張機能 RESTer https://chrome.google.com/webstore/detail/rester/eejfoncpjfgmeleakejdcanedmefagga RESTer は HTTP リクエストを実行できる Google Chrome の拡張機能です。 URL、メソッド (GET, POST, PUT, DELETE など)、ヘッダー、ボディなどのパラメータを指定して送信することで、ブラウザ上で HTTP リクエストを送信できます。 RESTer には過去のリクエストの履歴が保存され、後で確認したり一部変更して再度送信することもできます。 さらにエンドポイントの URL などを環境変数として定義することで、異なる環境に応じて簡単に URL を切り替えることができます。 まとめ 開発者ツールを使用することで、ページの要素の確認や編集、JavaScript コードの実行、ネットワークの監視など、様々な面で役立つことが分かりました。 開発者ツールは Web 開発者にとって効率的な開発やデバッグに不可欠な存在です。 ぜひ、これらの機能を覚えて開発者ツールを使いこなせるようになりましょう。
アバター
はじめに こんにちは。システムエンジニアの佐藤です。 WESEEK では Rails を使った Web アプリケーションの開発をするプロジェクトがあります。 この記事では Webpacker から後継である Shakapacker への移行を経験した際に、どのような差があったのか確認した結果を紹介したいと思います。 Webpacker は引退した Readme.md にもあるように、Webpacker は 5 年間に渡って Rails コミュニティにより提供されてきましたが、Rails 7 からは default の JS ビルドツールとしては import-maps が選択されるようになり引退することになりました。 Rails 7 については、これまでのバンドラも引き続き使うことが可能で、 rails new コマンドを実行する際の --javascript オプションには、importmaps (default) に加え、webpack, esbuild, rollup から選択することも可能です。 尚、webpack, esbuild, rollup を選択した場合は jsbundling-rails がインストールされ、この gem が Rails との統合を担うため Webpacker は使われません。 Shakapacker とは Webpacker の引退は開発の停止ではなく、Rails コミュニティによる開発の停止を意味します。 Shakapacker は Webpacker の公式な後継となる gem であり、引き続き開発が行われています。 先に紹介した jsbundling-rails が Rails の Sprockets と連動して動作するのに対し、Shakapacker (Webpacker) は Sprockets の代替になることが出来る点や、Webpack の code splitting, Hot Module Replacement (HMR) 機能が利用できる点が特筆できる違いだと思います。 その他の比較については https://github.com/rails/jsbundling-rails/blob/main/docs/comparison_with_webpacker.md に詳しく紹介されているのでご覧ください。 Webpacker からの移行先として Shakapacker を選択する理由 Shakapacker vs jsbuilding-rails Shakapacker は webpack の wrapper なので webpack と切り離すことが出来ません。 一方で、jsbundling-rails は webpack, esbuild, rollup を選択して使うことが出来ます。 Sprockets の代替として Webpacker を使っていて、特にバンドラを変更する予定がない場合は Shakapacker を選ぶことで少ない工数でライブラリをアップデートをすることが出来ます。 (webpack 5 以降へアップデートする場合は Webpacker は対応していないため Shakapacker へ移行する必要があります) その他の選択肢 DHH 氏による blog Rails 7 will have three great answers to JavaScript in 2021+ では、Rails 7 の default である import-maps を使う場合や、Rails を API モードで利用してフロントエンドと分離するという手段も紹介されています。 API モードは継続して提供され続けるとのことですので、プロジェクトの状況を鑑みて検討をするとよいでしょう。 Webpacker と Shakapacker の違い Webpacker と Shakapacker は名前も違いますが、Webpacker v5 から fork され Shakapacker として v6 がリリースされているので v5 以前を Webpacker、v6 以後を Shakapacker と呼んで差し支えないでしょう。 ( bin/webpacker も bin/shakapacker などへリネームされています) Webpacker から Shakapacker へのマイグレーションガイドにあるように、Shakapacker は、よりスリムな Webpack のラッパーになりました。 webpacker.yml を介して webpack を設定する機会は極力減り、 config/webpack/environment.js で webpack の設定をする機会が増えることになりました。 そのため、Webpacker に比べて Shakapacker では webpack の config をそのまま記述する機会が増えています。 Webpacker から Shakapacker へ移行するときの注意点 細かく挙げると多くの違いがありピックアップ出来ていない点もあります。 また、アプリケーションの設定によっては注意すべき点となる・または注意すべき観点が変わる可能性があるので Shakapacker の CHANGELOG.md は目をとおしてください。 あくまで筆者が変更点として認識した・注意した点が以下です。 node_modules 配下は babel トランスパイル対象にならなくなった Webpacker でおなじみの設定ファイルの構造が変わった ex. config/webpack/environment.js がなくなった NODE_ENV が RAILS_ENV を元に設定されるようになった これまで RAILS_ENV=staging などの production 相当の環境があった場合、NODE_ENV=production に fallback せずに development が設定されるようになった Webpacker では dependencies であった依存ライブラリが peerDependencies に変わった Webpacker の Rails view helper が色々変わった asset_pack_path の出力先が media から static に変わった javascript_pack_tag は 1 回だけしか使用できなくなった javascript_pack_tag が render する javascript タグに defer オプションがデフォルトで付くようになった image_pack_tag が追加された assets ファイルの読み込み先として node_modules を自動で追加する処理がなくなった デフォルトでは app/javascript/packs ディレクトリ配下は直下のファイルのみが bundle される対象となった webpacker.yml の設定が変わった webpacker_precompile 設定が追加された assets:precompile 時に yarn install されなくなった 最後に Webpacker から Shakapacker へ移行する機会がある方の一助になれば幸いです。
アバター
はじめに こんにちは WESEEK で yaml から css まで何でも書く haruhikonyan です。 フォームなどをコンポーネント化したときに同じページにそのコンポーネントを使うと id の重複に困ったりしませんか? そんな時に React が公式で提供している useId という hook を使うと解決するかもしれません。 しかし利用においては注意点があるので具体例とともに紹介したいと思います。 useId とは まず最初に useId とは何かです。 簡単に言えば同じ React を使ったアプリケーションの中で重複の無い id となる文字列を作り出すものです。 import { useId } from 'react'; const id1 = useId() const id2 = useId() console.log(`id1 => ${id1}, id2 => ${id2}`) // id => :r1:, id2 => :r2: こんな感じに : で囲われて数値がインクリメントされた文字列が生成され、重複がないことが保証されています。 くわしくは 公式ドキュメント を読んでいただければよいと思います。 具体的な使い方 具体的な使い方として公式の Usage の例を解説します。 import { useId } from 'react'; function PasswordField() { const passwordHintId = useId(); return ( <> <label> Password: <input type="password" aria-describedby={passwordHintId} /> </label> <p id={passwordHintId}> The password should contain at least 18 characters </p> </> ); } 例に出てきた完成系のコードです 何がうれしいかというと パスワードのインプットではあまり無いと思いますが、汎用化されたコンポーネントを同じページに2つ使う(2種類のパスワードを入力する必要がある)場合に、 useId を使わずに固定の id を使ってしまうと、HTML 上に同じ 2つ以上の id が存在してしまい、HTML のルールを違反してしまうということが起こります。 useId で生成した文字列が selector として使えない!? まずこちらのコンポーネントを見てください。 これはかなり簡略化されていますが、私が実際に最初に業務で書いた useId を用いたコードです。 import { useId } from 'react' import { UncontrolledTooltip } from 'reactstrap' const Tooltip = ({tooltipValue: string, tooltipLabel: string}) => { const id = useId() return ( <> <UncontrolledTooltip target={id}> {tooltipValue} </UncontrolledTooltip> <span id={id}> {tooltipLabel} </span> </> ) } UncontrolledTooltip は children に渡した内容を target に指定した id の要素がホバーされた時に出てくるという reactstrap が提供するコンポーネントです。 このコンポーネントを使うと任意の場所にツールチップを設置することが可能です。(これだけ見ると UncontrolledTooltip をそのまま使えばいいと思うかもしれませんが、あくまで例としての提示です) 一応 useId を使っているので id の値は自分で決めなくても良いし必ず一致するという利点があります。 しかしこのコードはエラーが出ます。 Unhandled Runtime Error SyntaxError: Failed to execute 'querySelectorAll' on 'Document': ':r1:' is not a valid selector. ライブラリのコードを読み進めるとありました。 https://github.com/reactstrap/reactstrap/blob/master/src/utils.js#L293 けっこう深くまで潜ってますが、 target に渡せた id が querySelectorAll に渡されています。 ドキュメント を参照しますとどうやら : は 標準の CSS の構文に従っていない ものであるようなのでエスケープしてあげろと書かれています。 エスケープしたコンポーネントがこちらです。 import { useId } from 'react' import { UncontrolledTooltip } from 'reactstrap' const Tooltip = ({tooltipValue: string, tooltipLabel: string}) => { const id = useId() return ( <> {/* Tooltip が内部で使ってる querySelector には useId で付与される : が使えずエラーが出るため escape する see: https://stackoverflow.com/a/75178117 */} <UncontrolledTooltip target={CSS.escape(id)}> {tooltipValue} </UncontrolledTooltip> <span id={id}> {tooltipLabel} </span> </> ) } このように CSS クラスが用意している escape 関数を使えば適切に : をエスケープしてくれて使えるようになります。 おまけ useId に : を追加した経緯など https://github.com/facebook/react/pull/23360 詳しくは議論を読んでもらえばと思いますが、 コメントでは強気に there are workarounds or alternative solutions. と言っていてけっこう大胆に思いつつも、かくいう私も同意なんでおもしろいなと思いました。 React の世界観というのはいわゆる JQuery や素の js でやりがちな selector をセットしてそれに対して何か作用を加えるということを廃し、宣言的にロジックを書いていくものであるため querySelector のようなものはライブラリ側としては考慮しなくていいというのは良くも悪くも思想がはっきりと伝わってくるなと思いました。 終わりに 複数個所で使うようなコンポーネントを共通化して使い回すのはいいぞ!
アバター
はじめに はじめまして。エンジニアの Ryo です。本記事では、オンライン決済サービス Stripe を用いて海外展開をご検討中のサービス提供者向けに、気をつけておくべき点を紹介します。 ※こちらの記事は、すでに Stripe をサービスに導入しているという前提で作成されております。もし、Stripe を検討中でまだ導入されてない場合は、以下の記事で Stripe 導入のメリットや扱うデータの種類、実際にサービスでどのように利用されているかなどを紹介していますのでご参照ください。 Stripeを使った簡単なサブスク型課金サービスの作り方 Stripe を使った海外展開について Stripe では、135 を超える通貨での支払い処理に対応しています。 弊社開発のサービス GROWI.cloud も、元々は日本円のみでしたが、海外展開としてフィリピンに向けたサービス展開の際に Stripe の力を借り、現地の通貨で商品を提供しています。 そのため、GROWI.cloud のフィリピンユーザーは現地通貨を日本円に換算する及び換算にかかる手数料を払わずしてサービスを利用できます。 気をつけるべきこと 上記のように Stripe を利用すれば幅広い通貨でサービスの商品を提供できます。 しかし、Stripe を利用し異なる通貨を取り入れる際に気をつけておくべきこともあります。 ここからは、実際に GROWI.cloud で第2の通貨を取り入れる際に陥ってしまった課題をもとに気をつけておくべき点を紹介します。 1. 現在契約中の商品の通貨と異なる商品には変更できない Stripe ではトライアルも含め商品の契約が行われるとその契約主体に対しデフォルト通貨が設定されます。 上記のように、一度設定されたデフォルト通貨は変更ができないため現在契約中の通貨と異なる通貨の商品を購入する場合は、新しく契約主体(アカウント)を用意する必要があります。 2. 通貨の小数点以下の有無が設定する金額に影響する Stripe で利用できる通貨では、小数点以下の有無によって設定する金額に際が生じます。 「通貨の小数点の有無」は、日本円で普段決済をしていると馴染みのないワードだと思いますので、 2,000円 で提供している商品をアメリカに 20 USD で展開する場合を例に説明します。 Stripe における USD は「小数点以下の有る通貨」となります。 厳密には、小数点以下2桁目を1の位として扱う通貨となります。 これは USD という通貨の最小単位であるセントに基づくものであり 1 USD=100 セント となるからです。 そのため、 20 USD のつもりで amount に 20 を設定してしまうとその商品に設定される金額は 0.2 USD つまり 20 セント となってしまいます。 なので、 20 USD を設定するためには amount を 2000 に設定する必要があります。 小数点以下が存在する代表的な通貨 USD (米ドル) EUR (ユーロ) PHP (フィリピン・ペソ) 反対に日本円は「小数点以下の無い通貨」に振り分けられます。 小数点以下のない通貨の場合は、 100 を掛けずに整数として金額を指定することができるため 2000 という数値をそのまま日本円の金額として商品に設定できます。 小数点以下が存在しない代表的な通貨 JPY (日本円) KRW (大韓民国ウォン) GROWI.cloud で行っているワークアラウンド GROWI.cloud では日本円とフィリピン・ペソという「小数点以下の無い通貨」「小数点以下の有る通貨」が併存しています。 そのため、金額を表示する画面などでは以下のようなワークアラウンドを採用しています。 const CURRENCY_LIST = { JPY: 'jpy', PHP: 'php', }; // 小数点以下のない通貨のリスト const CURRENCY_WITHOUT_SUBUNIT_LIST = [ CURRENCY_LIST.JPY, // 日本円 ]; /** * 受け取った金額値を通貨の種類ごとに成形して返す * 例:2000 × JPY => ¥2,000 * 例:2000 × PHP => ₱20.00 * @param {number} amount * @param {string} currency * @returns {string} */ const getFormattedPrice = (amount, currency) => { if (amount == null || Number.isNaN(amount)) { return '-'; } // 小数点以下がある通貨の場合 if (!CURRENCY_WITHOUT_SUBUNIT_LIST.includes(currency)) { // 例:2000 × PHP => ₱20.00 return priceFormat(currency).format(amount / 100); } // 例:2000 × JPY => ¥2,000 return priceFormat(currency).format(amount); }; このように、表示金額を小数点以下の有無によって 100 で割る割らないの切り分けを行っています。 おわりに Stripe を利用して海外展開する際に気をつけておくべきことを紹介しました。 「通貨の小数点以下の有無」に関しては、GROWI.cloud で日本円でのみ商品を提供していた私たちも苦しめられました。 逆に今回紹介した点さえ注意しておけば、多くの通貨でサービスを提供することができるようになりますのでぜひ利用してみてください。 参考資料 https://stripe.com/docs/currencies?locale=ja-JP#presentment-currencies https://support.stripe.com/questions/setting-a-customers-default-currency
アバター
こんにちは。 GROWI エンジニアの 宮沢 です。今回は便利な React hooks である SWR 2.0 で追加された useSWRMutation の使い所について簡単に紹介しようと思います。ある程度 SWR を使ったことがある人向けの記事となっております。 使い所 「useSWRMutation」とは、手動でデータのミューテーションを行うことができる React Hooks の1つであり、API の POST、PUT、DELETE などの HTTP リクエストをトリガーにして、データの更新を行うことができます。 これにより、ユーザーは任意のタイミングでデータの取得が可能になり、遅延読み込みなどの UX の向上にも繋がります。例えば、1つのページに多数の画像がある場合、1回のレンダリングで全ての画像を読み込んでしまうと読み込みに時間が掛かってしまう可能性があります。そこで、初回レンダリング時にユーザーが視認可能な部分まで画像の読み込みをして、スクロールしたタイミングなどで必要な部分の読み込みをすることで UX の向上が期待できます。このように、遅延読み込みが必要な場合にも、「useSWRMutation」を使って手動でデータの更新を行うことができます。 公式ドキュメント: https://swr.vercel.app/ja/docs/mutation.ja#useswrmutation 従来の useSWR から useSWRMutation でどのような違いがあるのかサンプルコードを使って紹介していきます。 従来の useSWR page.tsx というサンプルコードを使って見ていきます。 page.tsx コンポーネントがレンダリングされるタイミング で useSWRxPage が呼ばれ、fetcher が走りデータが取得されます。 page.tsx import React, { FC } from 'react'; import useSWR, { SWRResponse } from 'swr'; // swr のカスタムフック const useSWRxPage = (pageId: string): SWRResponse => { const key = pageId; return useSWR( key, () => fetch('page').then(response => response), ); }; type Props = { pageId: string, } export const Page: FC = (props: Props) => { // props から pageId を貰い、カスタムフックの第一引数に入れいている const { data, mutate } = useSWRxPage(props.pageId); return ( <> <div> // ページの本文を表示 { data.body } </div> // ボタンを押して再 fetch <button onClick={() => mutate()} /> </> ); }; swr2.0 で追加された useSWRMutation 次に useSWRMutation を使ってみましょう。上記のサンプルコードを useSWR から useSWRMutation を使うコードに置き換えています。useSWRMutation を使うと page.tsx がレンダリングされた時点では fetcher は走りません。 trigger と呼ばれる hook を呼び出すことことで初めてミューテーションが行われます。 page.tsx import React, { FC } from 'react'; import useSWRMutation, { SWRMutationResponse } from 'swr/mutation'; // swr のカスタムフック const useSWRMUTxPage = (pageId: string): SWRMutationResponse => { const key = pageId; return useSWRMutation( key, () => fetch('page').then(response => response), ); }; type Props = { pageId: string, } export const Page: FC = (props: Props) => { const { trigger, data } = useSWRMUTxPage(props.pageId); return ( <> <div> // trigger が実行されるまで undefined { data.body } </div> <button onClick={() => trigger()} /> </> ); }; 最後に useSWRMutation の登場によって SWR を使ったより UX の高いアプリケーション開発が期待できそう..?というお話でした。
アバター
システムエンジニアの蛸井です。今回は Lerna についてと Lerna v6 の Nx の機能について解説します。 Lerna とは Lerna とは monorepo ツールです。 monorepo とは、1つのリポジトリで複数のサービスを管理することを言います。 monorepo のメリットとして、例えば以下のことが挙げられます。 リポジトリ内の複数のサービス間でコードを共有できる サービスを横断してのテストができる リリースが管理しやすい サービス毎に共通したルールの lint などのツールを使用できる Lerna を使用することにより、複数の node パッケージをまとめて管理できます。 lerna bootstrap コマンドを使用することにより、各サービスの package.json を元にパッケージを一括でインストールし、さらに同じ種類・バージョンのパッケージを共通利用できるようになります。 evocateur さんが開発していた Lerna は version 4.0.0 にて開発が停止しましたが、Nx という monorepo ツールを開発している Nrwl 社が開発を引き継ぎ v5, v6 をリリースしました。 Lerna v5 の変更点 Lerna v5 にて Lerna にキャッシュ機能が備わりました。 lerna.json で useNx フラグを有効にすることで Nx と同等の速度になり、リポジトリの設定にもよりますが以前の Lerna と比較して2~10倍のスピードアップが期待できます。 参考: https://blog.nrwl.io/lerna-used-to-walk-now-it-can-fly-eab7a0fe7700 Lerna v6 の変更点 Lerna v6 では、先ほど紹介した useNx がデフォルトで有効になります。 また lerna add-caching を実行することで、Nx の機能の設定を記載する nx.json を生成できます。 nx.json にはスクリプトの依存関係を指定できます。 // nx.json { ... "targetDefaults": { "build": { "dependsOn": ["^build"] }, "dev": { "dependsOn": ["^build"] } } } 例えば nx.json を上記のように設定すると、build か dev のどちらかのスクリプトを実行した場合に Lerna は依存するすべてのパッケージに対して build を実行します。 また、次の章で解説する cacheableOperations に設定したスクリプトは実行結果がキャッシュされるようになります。 参考: https://blog.nrwl.io/lerna-reborn-whats-new-in-v6-10aec6e9091c タスクの結果をキャッシュする // nx.json { ... "options": { "cacheableOperations": [ "lint", "typecheck", "jest" ] } } 例えば nx.json を上記のように設定すると、タスクランナーの実行結果 ( lerna run lint など) がキャッシュされるようになります。 対象となるスクリプトは cacheableOperations に登録が必要です。 試しに lerna run lint を2度実行してみます。 $ yarn lerna run lint yarn run v1.22.19 $ /app/node_modules/.bin/lerna run lint lerna notice cli v6.5.1 ✔ backend:lint (17s) ✔ frontend:lint (21s) ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > Lerna (powered by Nx) Successfully ran target lint for 2 projects (21s) Done in 24.66s. $ yarn lerna run lint yarn run v1.22.19 $ /app/node_modules/.bin/lerna run lint lerna notice cli v6.5.1 ✔ backend:lint [existing outputs match the cache, left as is] ✔ frontend:lint [existing outputs match the cache, left as is] ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > Lerna (powered by Nx) Successfully ran target lint for 2 projects (43ms) Nx read the output from the cache instead of running the command for 2 out of 2 tasks. Done in 2.18s. 1回目の実行では約25秒かかりました。 2回目の実行ではコマンドの実行の代わりにキャッシュを使用した旨のメッセージ Nx read the output from the cache instead of running the command for 2 out of 2 tasks. が表示され、実行時間は約2秒とすぐ完了するようになりました。 Nx はパッケージごとに前回の実行からファイルの差分があるかどうかを確認し、ファイルに変更が無かった場合にキャッシュを使用します。 また、キャッシュはローカルに1週間保存されます。 nx reset コマンドを実行することで、ローカルに保存したキャッシュを削除できます。 注意点として、 cacheableOperations には、同じ入力に対して常に同じ結果を出力するスクリプトのみを設定するようにしてください。 また、nx.json に適切な設定を行うことにより、コマンド実行により生成されるファイル (例えば dist ディレクトリの生成など) も即時に実行完了できます。 参考: https://lerna.js.org/docs/features/cache-tasks nx cloud でキャッシュを共有する キャッシュは nx cloud を使用することにより複数のマシンに共有できます。 以下を実行するだけでワークスペースを nx cloud に接続できます。 $ npx nx connect-to-nx-cloud ✔ Enable distributed caching to make your CI faster · Yes > NX Generating @nrwl/nx-cloud:init UPDATE nx.json > NX Distributed caching via Nx Cloud has been enabled In addition to the caching, Nx Cloud provides config-free distributed execution, UI for viewing complex runs and GitHub integration. Learn more at https://nx.app Your workspace is currently unclaimed. Run details from unclaimed workspaces can be viewed on cloud.nx.app by anyone with the link. Claim your workspace at the following link to restrict access. https://cloud.nx.app/orgs/workspace-setup?accessToken=ACCESSTOKEN 実行が完了すると nx.json にアクセストークンが追加され、それ以降は nx cloud 上にキャッシュが保存されるようになります。 試しに1度 lerna run lint を実行した後、 nx reset コマンドでローカルのキャッシュを削除します。 $ yarn lerna run lint yarn run v1.22.19 $ /app/node_modules/.bin/lerna run lint lerna notice cli v6.5.1 ✔ backend:lint (22s) ✔ frontend:lint (26s) ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > Lerna (powered by Nx) Successfully ran target lint for 2 projects (26s) View logs and investigate cache misses at https://cloud.nx.app/runs/xxxxxxxxxx Done in 28.45s. $ npx nx reset > NX Resetting the Nx workspace cache and stopping the Nx Daemon. This might take a few minutes. > NX Daemon Server - Stopped > NX Successfully reset the Nx workspace. その後、再度 yarn lerna run lint を実行します。 $ yarn lerna run lint yarn run v1.22.19 $ /app/node_modules/.bin/lerna run lint lerna notice cli v6.5.1 ✔ frontend:lint [remote cache] ✔ backend:lint [remote cache] ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > Lerna (powered by Nx) Successfully ran target lint for 2 projects (2s) Nx read the output from the cache instead of running the command for 2 out of 2 tasks. Nx Cloud made it possible to reuse 2 tasks: https://cloud.nx.app/runs/xxxxxxxxxx Done in 7.31s. nx cloud に保存されたキャッシュが使用され、タスクがすぐに完了することが確認できました。 また、タスク実行時にログに表示される https://cloud.nx.app/runs/:taskCode にアクセスすることでタスクの実行結果を閲覧できます。 参考: https://lerna.js.org/docs/features/share-your-cache まとめ 今回は Lerna に新しく追加された Nx の機能について解説しました。 キャッシュをクラウド上に保存して使用することが、無料でこんなに簡単にできることに驚きました。 非常に便利なのでぜひ使ってみてください。
アバター
こんにちは、システムエンジニアの Kota です。今回は少し前に発表された SWR ver 2.0 に便利で新しいオプションが追加されたので、簡単に解説したいと思います。 今回追加されたオプション optimisticData クライアントキャッシュを即座に更新するためのデータ、または現在のデータを受け取り新しいクライアントキャッシュデータを返す関数 populateCache リモートミューテーションの結果をキャッシュに書き込むかどうか、またはリモートミューテーションの結果と現在のデータを引数として受け取り、ミューテーションの結果を返す関数 rollbackOnError リモートミューテーションがエラーだった場合にキャッシュをロールバックするかどうか、または発生したエラーを引数として受け取りロールバックするかどうかの真偽値を返す関数 公式のドキュメントは こちら です。 公式のブログは こちら です。 解説 公式ブログでは、簡単な Todo の追加が例として記載されています。 await addNewTodo('New Item') addNewTodo は, Todo リストに新しい Todo を追加するための Promise もしくは、非同期関数で、更新後のデータを返します。 これまでは、Todo が追加された場合、 UI への表示は、 mutate を通してサーバにリクエストを送信し、リモートで最新のデータを取得し、返ってきたレスポンス内のデータを使って UI 側を更新するといった流れでした。 const { mutate, data } = useSWR('/api/todos') return <> <ul>{/* データの表示 */}</ul> <button onClick={async () => { await addNewTodo('New Item') mutate() }}> Add </button> </> ※ 画面は後述する公式の example 問題点 もし仮に await addNewTodo('New Item') のリクエストで時間が掛かった場合、レスポンスが返ってくるまでの間、更新前の Todo リストが表示されたままの状態になり UX が低下してしまいます。 新オプション 今回 ver 2.0 で登場した新しいオプション optimisticData でそれを解決できます。 const { mutate, data } = useSWR('/api/todos') return <> <ul>{/* データの表示 */}</ul> <button onClick={() => { mutate(addNewTodo('New Item'), { // クライアントキャッシュを即座に更新する optimisticData: [...data, 'New Item'], }) }}> Add </button> </> 上記のように optimisticData オプションを使うことで、サーバからのレスポンスを待つことなく新しい状態のリストを表示できます。 data を optimisticData の値によって更新し、サーバにリクエストを送信します。リクエストが完了したら SWR はリソースを revalidate しデータが最新であることを保証します。 また、 populateCache オプションを有効にすることで、ミューテーションのレスポンスで useSWR のキャッシュを更新できます。 const { mutate, data } = useSWR('/api/todos') return <> <ul>{/* データの表示 */}</ul> <button onClick={() => { mutate(addNewTodo('New Item'), { optimisticData: [...data, 'New Item'], // ミューテーションのレスポンスで useSWR のキャッシュを更新 populateCache: true, }) }}> Add </button> </> populateCache オプションを有効にした場合、レスポンスのデータが正しい場合、 revalidate は必要ないので、合わせて無効化します。 const { mutate, data } = useSWR('/api/todos') return <> <ul>{/* データの表示 */}</ul> <button onClick={() => { mutate(addNewTodo('New Item'), { optimisticData: [...data, 'New Item'], populateCache: true, // populateCache を有効にする場合は、revalidate は不要 revalidate: false, }) }}> Add </button> </> addNewTodo が例外で失敗した場合は、楽観的な更新によるデータがユーザーに表示されます。正しいデータを保証する為に、 rollbackOnError オプションで、 optimisticData のデータを更新前の data にロールバックします。( default で rollbackOnError=true になっている ) const { mutate, data } = useSWR('/api/todos') return <> <ul>{/* データの表示 */}</ul> <button onClick={() => { mutate(addNewTodo('New Item'), { optimisticData: [...data, 'New Item'], populateCache: true, revalidate: false, rollbackOnError: true, }) }}> Add </button> </> 参考 公式の example でオプションの設定を変えてみて、挙動を確認してみて下さい。 codesandbox まとめ いかがでしたか? 今回は SWR ver 2.0 で登場した新しいオプションについて解説してみました。なお、本日ご紹介したオプションは同じく 2.0 で登場した useSWRMutation でも使えるオプションになりますので、合わせてチェックしてみて下さい。
アバター
こんにちは、 ryosuke です。 今回は、 vscode で devcontainer を使った開発環境について、 「Clone repository in container volume」を使うとうれしい点を取り上げます。 「Clone repository in container volume」とは何か vscode で devcontainer を使った開発環境を構築する場合、下記のような手順を踏むことが多いと思います。 PC の任意のフォルダに repository を clone する 該当フォルダを vscode で開く vsocde 上で Dev Containers: Open Folder in Container... コマンドを選択する devcontainer による開発環境が起動する Clone repository in container volume (以後 cloneInVolume ) の場合、「PC の任意のフォルダに repository を clone」 ではなく「devcontainer の volume に repository を clone」する点が特徴です。 うれしいこと Bind mount による諸問題からの解放 vscode の記事 「 Improve disk performance 」 にもあるように、 Windows や macOS 向け Docker Desktop では「Bind mount したファイルへのアクセスパフォーマンスが悪い」という課題がつきまといます。 改善策として「 依存ライブラリをインストールするディレクトリを volume mount 化 」したり、 Windows では「 clone 先を WSL のファイルシステム上に変更 」したり、 macOS では「 Docker の --volume フラグのチューニング 」や「 bind mount driver を変更 」する方法があります。 しかし、 cloneInVolume を使えば、そもそも bind mount するファイルが1つもなくなるため、この課題とは無縁になります。 上記のような Host OS ごとに異なる改善策をとる必要もありません。 開発環境起動の直リンが作れる このバッジ「 」をクリックすると、 microsoft/vscode-remote-try-java repository の devcontainer 環境が cloneInVolume で立ち上がります。 (事前に vscode のインストールは必要) このような Web リンクをつくることができる ので、新参者でも開発環境を立ち上げるのが一掃簡単になることが期待できます。 制約事項 特殊な repository 構成・運用に対応していない 現在のところ、 repository を特殊な構成・運用にしている場合は、 cloneInVolume が正しく立ち上がらないようです。 実際に試してみて、うまく動かなかったケースは以下の通りです。 複数の repository を clone する必要がある場合 git-lfs を使う repository の場合 submodules を使う repository の場合 Host OS から repository 内の file にアクセスする手段が限定される 開発に必要な全てのファイルが Docker 管理下にあるため、Explorer や Finderを含む Host OS のアプリケーションからファイルを操作する手段がありません。 したがって、 vscode や Extension、devcontainer 内のコマンドだけで開発可能な環境である必要があります。 おわりに ぜひ皆さんも、直リンバッジをクリックして cloneInVolume の良さを体験してみてください。
アバター
こんにちは、システムエンジニアの kouki です。 この記事では VictoriaMetrics のドキュメントに掲載されている Key concepts - VictoriaMetrics をハンズオン形式にまとめてみました。記事を書くに至ったモチベーションは下記の通りです。 VictoriaMetrics に入門してみたいけど、日本語で記述されているチュートリアル的なものがない (2022年12月現在) 自身でデータを登録して、自身でクエリを投げる形式のチュートリアルが Web 上に見当たらなかった Key concepts - VictoriaMetrics は画像と共に解説されていて分かりやすいが、手元で試すには若干の試行錯誤が必要だった ちなみに Key concepts では下記のような画像と共に VictoriaMetrics の挙動が解説されているので、非常に理解の助けになるドキュメントだと思います。 Key concepts を触ってみる 事前準備 今回のチュートリアルを実施するにあたって、下記の環境は構築済みという前提でお話します。 docker コマンドを打てる環境であること ( docker run ... など) curl コマンドがインストール済みであること (任意) jq コマンドがインストール済みであること VictoriaMetrics を docker コマンドで立ち上げる Quick start - VictoriaMetrics を参考に -retentionPeriod=100y オプションを付与 して、手元に VictoriaMetrics を立ち上げます。 docker pull victoriametrics/victoria-metrics:v1.85.3 docker run -it --rm -v `pwd`/victoria-metrics-data:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics:v1.85.3 -retentionPeriod=100y docker image のタグは v1.85.3 を指定しています。( latest だと記事との齟齬が出る可能性があるため) -retentionPeriod というコマンドオプションは VictoriaMetrics のドキュメントに記載 されている通り、データの保持期間を指定するものです。デフォルトでは1ヵ月(それ以降は削除される)ですが、今回のチュートリアルでは1ヵ月以上前のデータを登録する予定のため、それ以上の値として 100y を指定しています。(保持期間に「無限」という指定はできないので、 100y と指定する旨は VictoriaMetrics の README にも記載されています) retention for stored data. Older data is automatically deleted. Default retention is 1 month. See the Retention section for more details. ブラウザで http://localhost:8428/ が閲覧できることを確認します。 VictoriaMetrics にデータを登録する Key concepts · VictoriaMetrics にある「Query data」のセクションのデータを VictoriaMetrics に登録します。 ドキュメントでは下記のようなデータが記載されています。左から「メトリクス名」「データの値」「タイムスタンプ(Unix Time Stamp(ミリ秒)」となっています。 注意点として、コメントのような形で「 # 2022-05-10 10:00:00 」と記載されていますが、UTC で確認すると 2022-05-10 08:00:00 (マイナス2時間) となります。ここ以降のコマンドは公式ドキュメントに記載されている時間からマイナス 2 時間した時刻を基準にしていきます。 foo_bar 1.00 1652169600000 # 2022-05-10 10:00:00 foo_bar 2.00 1652169660000 # 2022-05-10 10:01:00 foo_bar 3.00 1652169720000 # 2022-05-10 10:02:00 foo_bar 5.00 1652169840000 # 2022-05-10 10:04:00, one point missed foo_bar 5.50 1652169960000 # 2022-05-10 10:06:00, one point missed foo_bar 5.50 1652170020000 # 2022-05-10 10:07:00 foo_bar 4.00 1652170080000 # 2022-05-10 10:08:00 foo_bar 3.50 1652170260000 # 2022-05-10 10:11:00, two points missed foo_bar 3.25 1652170320000 # 2022-05-10 10:12:00 foo_bar 3.00 1652170380000 # 2022-05-10 10:13:00 foo_bar 2.00 1652170440000 # 2022-05-10 10:14:00 foo_bar 1.00 1652170500000 # 2022-05-10 10:15:00 foo_bar 4.00 1652170560000 # 2022-05-10 10:16:00 curl コマンドで /api/v1/import エンドポイントに対してデータを登録します。 curl -d '{"metric":{"__name__":"foo_bar"},"values":[1.0,2.0,3.0,5.0,5.5,5.5,4.0,3.5,3.25,3.0,2.0,1.0,4.0],"timestamps":[1652169600000,1652169660000,1652169720000,1652169840000,1652169960000,1652170020000,1652170080000,1652170260000,1652170320000,1652170380000,1652170440000,1652170500000,1652170560000]}' -X POST 'http://localhost:8428/api/v1/import' スペース区切りのテキストから curl コマンドの文字列を出力する簡単な ruby スクリプトを用意して実施しました。このスクリプトは上記の curl コマンドを出力するだけですので、あまり重要ではないので読み飛ばしても問題ありません。 require 'json' # VictoriaMetrics に提示されているサンプルデータ # UTC では 2022-05-10 08:00:00 から始まるので注意 # 変換に利用したサイト refs: https://www.unixtimestamp.com/index.php # データの引用元 refs: https://docs.victoriametrics.com/keyConcepts.html#query-data vm_sample_data = <<~EOS foo_bar 1.00 1652169600000 # 2022-05-10 10:00:00 foo_bar 2.00 1652169660000 # 2022-05-10 10:01:00 foo_bar 3.00 1652169720000 # 2022-05-10 10:02:00 foo_bar 5.00 1652169840000 # 2022-05-10 10:04:00, one point missed foo_bar 5.50 1652169960000 # 2022-05-10 10:06:00, one point missed foo_bar 5.50 1652170020000 # 2022-05-10 10:07:00 foo_bar 4.00 1652170080000 # 2022-05-10 10:08:00 foo_bar 3.50 1652170260000 # 2022-05-10 10:11:00, two points missed foo_bar 3.25 1652170320000 # 2022-05-10 10:12:00 foo_bar 3.00 1652170380000 # 2022-05-10 10:13:00 foo_bar 2.00 1652170440000 # 2022-05-10 10:14:00 foo_bar 1.00 1652170500000 # 2022-05-10 10:15:00 foo_bar 4.00 1652170560000 # 2022-05-10 10:16:00 EOS json_for_victoriametrics = { "metric" => { "__name__" => nil }, "values" => [], "timestamps" => [] } vm_sample_data.lines.each do |line| name, value, timestamp_milisecs = line.split(' ') json_for_victoriametrics['metric']['__name__'] = name if json_for_vm['metric']['__name__'].nil? json_for_victoriametrics['values'].push(value.to_f) json_for_victoriametrics['timestamps'].push(timestamp_milisecs.to_i) end puts "curl -d '#{json_for_vm.to_json}' -X POST 'http://localhost:8428/api/v1/import'" データが登録できたか確認する 先述した curl コマンドによってデータが登録できたか確認しましょう。最初のデータの時刻は「注意点」に記載した通り、マイナス 2 時間した値のため 2022-05-10T08:00:00.000Z を指定します。 curl -s "http://localhost:8428/api/v1/query?query=foo_bar&time=2022-05-10T08:00:00.000Z" | jq { "status": "success", "data": { "resultType": "vector", "result": [ { "metric": { "__name__": "foo_bar" }, "value": [ 1652169600, "1" ] } ] } } Key concepts の Query data をなぞってみる Instant query 早速 Key concepts の Query data を元に触っていきましょう。あらかじめ、 Key concepts の Query data のページを開いておくとスムーズに読めると思います。 まずは Instant query から試していきます。マイナス 2 時間した時刻でクエリを投げてみます。 curl -s "http://localhost:8428/api/v1/query?query=foo_bar&time=2022-05-10T08:03:00.000Z" | jq { "status": "success", "data": { "resultType": "vector", "result": [ { "metric": { "__name__": "foo_bar" }, "value": [ 1652169780, "3" ] } ] } } ここでは 「登録したデータの中に 2022-05-10T08:03:00.00 のデータは無いにも関わらず、 3 という値がレスポンスされた」 がポイントです。(元のデータでいうと # 2022-05-10 10:02:00 と # 2022-05-10 10:04:00 の間) 詳細は 公式の解説 を読んでみてください。 Range query 次は Range query を試してみます。 curl "http://localhost:8428/api/v1/query_range?query=foo_bar&step=1m&start=2022-05-10T07:59:00.000Z&end=2022-05-10T08:17:00.000Z" | jq { "status": "success", "data": { "resultType": "matrix", "result": [ { "metric": { "__name__": "foo_bar" }, "values": [ [ 1652169600, "1" ], [ 1652169660, "2" ], [ 1652169720, "3" ], (省略) ] } ] } } 意図した値が返ってくることが確認できると思います。 ここでも注意点ですが、 Range query に記載されている JSON のレスポンスの中に 7 という数値が含まれていますが、こちらはおそらく 5 が正しいものだと思われます。 解説文にも図と共に下記のように記載されています。 ephemeral data point always repeats the left closest raw sample (see red arrow on the pic above). > エフェメラルデータポイントは常に左側の生サンプルに近いものを繰り返します(上の写真の赤い矢印を参照)。 VM UI でグラフの形を確認してみる VictoriaMetrics には VM UI というクエリを投げて結果を確認できる WebUI があります。(localhost ならば http://localhost:8428/vmui/ ) この画面から上記の画像で示されているようなグラフの形になるか確認してみましょう。ひな形となる URL をこちらで用意しました。 http://localhost:8428/vmui/?g0.expr=foo_bar&g0.range_input=18m&g0.end_input=2022-05-10T08%3A17%3A00&g0.relative_time=none 画面が表示されたら、「Trace query」にチェックを入れて、ドキュメントの画像と条件を揃えるために「Step value」の内容を 60 (秒(1分)) に変えて「EXECUTE QUERY」ボタンを押してクエリを実行してみましょう。 「Range query」の画像のようになっていることが確認できます。 VM UI を自身で触ってみたい方は、画面右上にあるデフォルトでは「LAST 30 MINUTES」となっているボタンをクリックして時刻を指定し、上記の手順 ( Step Value と Trace Query を設定) を踏むことで同様のことができます。 注意点としては利用しているブラウザのタイムゾーンを基準とするため、クエリを投げる際にはタイムゾーンが JST ならばプラス9時間としてください。 先述した curl で指定した Range Query では 2022-05-10T07:59:00.000Z ~ 2022-05-10T08:17:00.000Z なので、JST だと 2022-05-10 16:59:00 ~ 2022-05-10 17:17:00 になります。 最後に ドキュメントの各章に関する解説は大幅にカットしましたが、この記事を通して実践的に VictoriaMetrics を試す環境が構築できたかと思います。 「こういうことをやってみると面白いよ」というネタを列挙して締めとさせていただきます。 「Range query」の step=1m を step=1s に変えて、返ってくるデータの個数を jq でカウントしてみる (約 1000 と公式には記載されていますが、800 くらい) VictoriaMetrics に渡すコマンドラインオプションをドキュメントを見ながら変えてみる retentionPeriod をデフォルトに戻して、この記事のデータを登録してみる (おそらく、データ登録はできないはず。docker のログを合わせて見るとよいかもしれません) この記事を通して VictoriaMetrics への理解を深める一助となれば幸いです。
アバター
はじめに こんにちは WESEEK でわりと何でもやっている haruhikonyan です。 みなさん TypeScript 書いてますか? フロントエンドはもちろん Node でサーバサイドを書いてもよし、さらに型安全! そんな型安全な TypeScript をより強固に使いこなすための User-Defined Type Guards の一つである is 演算子を使った自作型ガードの紹介をします。 型ガードとは まず最初に型ガードとは何かです。 概要は 参考 URL を読んでいただければいいんですが、おそらく一番使うだろうなのは以下のようなものかと思います。 type NullableString = string | null const func = (nullableString: NullableString) => { if (nullableString === null) { // nullableString はこの if 文の中では null 型とトランスパイらは解釈する nullableString.length // 'nullableString' is possibly 'null'.(18047) いわゆるぬるぽ console.log("nullableString is null") } else { // nullableString はこの else 文の中では null 型ではないということなので string 型解釈する nullableString.length // string 確定なので length が参照できる console.log("nullableString is string") } } null や undefined チェックはよく使いますよね。 もちろん null みたいなプリミティブ型だけでなくユーザが定義したオリジナルの型などを判別したいことは往々にしてあるかと思います。 その時に登場するのが is 演算子です。 is とは? 例 説明するよりも見た方が早いと思うので先に実際の例を紹介します。 割と広く使われてるライブラリの一つである Axios に isAxiosError という簡単なものが実装されています。 使い方は 例 にもある通り、何かしらの変数が AxiosError なのかどうかを判断かつ型ガードにより型の絞り込みをやってくれます。 そのまま紹介しようと思ったのですがなんと元のコードは JavaScript だったので比較的読みやすいようこちらで TypeScript に書き直しています。 // https://github.com/axios/axios/blob/56e9ca1a865099f75eb0e897e944883a36bddf48/lib/utils.js#L112 const isObject = (thing: any) => thing !== null && typeof thing === 'object'; // https://github.com/axios/axios/blob/1e58a659ec9b0653f4693508d748caa5a41bb1a2/index.d.cts#L74 class AxiosError<T = unknown, D = any> extends Error { // いろいろプロパティあるが割愛 } // https://github.com/axios/axios/blob/56e9ca1a865099f75eb0e897e944883a36bddf48/lib/helpers/isAxiosError.js#L12-14 // https://github.com/axios/axios/blob/1e58a659ec9b0653f4693508d748caa5a41bb1a2/index.d.cts#L485 // 本体はこれ function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D> { return isObject(payload) && (payload.isAxiosError === true); } isObject これも実質型ガードみたいなものですね is 演算子が使われてないのでトランスパイラ的に型ガードの型絞り込みは行ってくれませんが null ではないかつ typeof で object 型であることを保証してくれます。 isAxiosError 本題はこちらです。まず中身を見てみると return isObject(payload) && (payload.isAxiosError === true); とあり、まず payload が object であること。これはいいですね。 そしてその payload のプロパティに isAxiosError が true という値が入っていることを見ています。 これだけです。 Axios が発行するエラーのオブジェクトは必ず true が入っているということみたいですね。 function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D> 関数の定義はこうなっており、返り値の型に注目してほしいのですが、これは  isAxiosError という関数が true を返すと 引数として与えられた payload は(is) AxiosError<T, D> 型だと TypeScript のトランスパイラに伝えてあげるという意味になります。 説明 上記で説明したことがほぼ全てではありますが、以下 URL に詳しいちゃんとした仕様が書いてあるので迷った時や、もっと深く知りたい際には確認しましょう。 https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates string 配列判定の関数を作ってみた みなさん is 演算子については理解いただけたでしょうか。 ここでは実際に業務で TypeScript を書いており、とある変数が string の配列なのかどうかを正確に知りたくなった時に作成した関数の紹介をします。 作成した関数 export const isStringArray = (value: unknown): value is string[] => { return Array.isArray(value) && value.every((v) => typeof v === 'string') } 上の説明を読んできた方ならもう読めるかと思いますが、説明をすると value として受け取ったものが、 isArray にて配列であることかつ、 配列であることが確定した value を every 関数によってすべての要素が string 型であれば string[] 型であるということをトランスパイラに教えてあげています。 実際の使いどころとしては、 express で Request から query を受け取ると query の型は string | string[] | QueryString.ParsedQs | QueryString.ParsedQs[] | undefined というあらゆる可能性が考慮された型となります。 ただの string が欲しいのであれば typeof でいいのですが、 string[] を確定させようとすると isArray だけでは不十分なので自作の型ガードを作成したという経緯になります。 気をつけないと型の偽りになるという話 is 演算子を使った型判定みなさんも作ってみたくなったことでしょう。しかし 定義に is Hoge と書いて関数が true さえ返してしまえばもうトランスパイラの中ではそれは Hoge 型というようになってしまうので適当な判別を書くと型の偽りになってしまいます。 isAxiosError の判定が決して適当という訳ではありませんが、適当なオブジェクトに isAxiosError というプロパティを持たせてそこに true を入れてしまえば AxiosError 型であるということになってしまいます。関数を定義する側も使う側もこういった危険性があることには留意しておいた方が良いかと思います。 終わりに as や any に逃げずより型安全で堅牢なシステムを作ろう!
アバター
こんにちは、WESEEK でエンジニアをしている 宮沢 です。普段はオープンソースな wiki である GROWI を開発しています。今回はそんな GROWI のビジュアルリグレッションテストで利用している Cypress で発生した XHR エラーの対処方法について解説していきます。 エラー内容 今回紹介するエラー内容は以下の赤丸で囲った部分となっています。スクリーンショットは実際の GROWI のログイン画面です。 実際にエラーが発生した Cypress のコードは以下のようになっています。具体的は、 /login に遷移して、あらかじめ登録されているユーザーネームとパスワードを入力、 ログインボタンを押してログインをするという Cypress の カスタムコマンド です。 使う場合は cy.login(username, password) このような感じで書きます。 Cypress.Commands.add('login', (username, password) => { cy.session(username, () => { cy.visit('/login'); cy.get('.usernameForLogin').type(username); cy.get('.passwordForLogin').type(password); cy.get('.btnSubmitForLogin').click(); }); }); エラー原因 ログインボタンを押下した際に走る /api/v3/login のレスポンスを待たずにログインが必要なページに遷移しようとすることで、ブラウザにユーザーのセッションデータが正常に保存されなかったことが原因と見ています。 試しに、ログインボタンの押下( cy.get('.btnSubmitForLogin').click() ) の後に、 cy.wait(10000) を入れたところ XHR エラーが出ず正常にログイン処理を行うことができました。 以上のことから /api/v3/login のレスポンスを待てば良さそうということが分かったので、そのようなコードに直していきます。 対処方法 Cypress には intercept という API があります。これはアプリケーション内で走る HTTP リクエストに対してスパイやスタブをができる API になっています。 今回はスパイのみでいいのでドキュメントを参考にすると以下のようなコードになります。第一引数にメソッド名、第二引数に URL が入ります。 as を使うとエイリアスを作ることができます。作成したエイリアスを cy.wait に入れると /api/v3/login の完了を待つことができます。 cy.intercept('POST', '/api/v3/login').as('login'); // Do something cy.wait('@login'); 以上のコードをエラーが発生していたカスタムコマンドに入れた結果正常に動作しました。 Cypress.Commands.add('login', (username, password) => { cy.session(username, () => { cy.visit('/login'); cy.get('.usernameForLogin').type(username); cy.get('.passwordForLogin').type(password); cy.intercept('POST', '/_api/v3/login').as('login'); cy.get('.btnSubmitForLogin').click(); cy.wait('@login') }); });
アバター
はじめに こんにちは、WESEEK でエンジニアをしている、藤澤です。 この記事では VS Code でマルチカーソルを使う方法をその具体例とともに解説します。 マルチカーソルを使いこなすことで日々の業務の時間短縮が図れるはずです。 ぜひこの記事を見て、マルチカーソルを使うことを検討してもらえればと思います。 マルチカーソルとは VS Code では複数のカーソルをサポートしていて、 マルチカーソル と呼ばれています。それぞれのカーソルはユーザの入力に対して独立に、並列に動作します。 詳しく説明するよりも見た方が早いので以下の動画を見てください。 document.createElement('video'); https://weseek.co.jp/tech/wp-content/uploads/2022/11/tech-blog-multicursor1.mp4 https://weseek.co.jp/tech/wp-content/uploads/2022/11/●-1-•-Untitled-1-growi-WSL_-Ubuntu-20.04-Visual-Studio-Code-2022-11-24-23-20-28.mp4 以上の動画のように複数のカーソルを並列に動作させ、似たような作業が同時にできます。 VS Code の KeyBindings を設定することでより使いやすくなるので、以下では KeyBindings について説明します。 設定しておくべき KeyBindings マルチカーソルを使用する際にいくつか設定しておくべき KeyBindings があるので紹介します。 KeyBindings の設定方法は こちら を参考にしてください。 ここでは VS Code の KeyBindings 名で紹介します。 AddCursorBelow/Above 現在カーソルがあるところから上下へマルチカーソルのカーソルを追加します cursorWordEndRight/Left Right、 Left とあり、左右 1 単語分移動します。 マルチカーソルで使用する際にはカーソルのみだと単語の文字数によって各カーソルがずれるので使う機会は多いです。 cursorEndRight/Left Right、Left とあり、インデントやスペースを除いて左右行けるところまで行きます。 2回 cursorEndLeft でインデントやスペースを超えて一番左まで行きます。 マルチカーソルでは上の cursorWordEnd と組み合わせて使ったり、使う機会が多いです。 cursorWordEndRightSelect/LeftSelect cursorWordEndRight/Left 同様カーソルが移動し、 さらに移動した範囲を選択します。 コピーや削除と組み合わせて使うことが多いです。 後述の Add Selection To Next Find Match と組み合わせて使うことも多いです。 cursorEndRightSelect/LeftSelect cursorEndRight/Left 同様カーソルが移動し、さらに移動した範囲を選択します。 こちらもコピーや削除と組み合わせて使うことが多いです。 Add Selection To Next Find Match 現在選択中の文字を検索し、次に近い部分に追加でカーソルを置きます。 replace で全体をやるほどでもないときに使うことが多いです。(関数内のみでの replace をしたいとき等) Move Last Selection To Next Find Match でカーソルを置くのを一つスキップできます。 Move Last Selection To Next Find Match 上述の Add Selection To Next Find Match を一つスキップできます。 具体例 具体例をもとに使い方を説明します。 コード自体はテキトーです。 文字列の配列からオブジェクトを作る https://weseek.co.jp/tech/wp-content/uploads/2022/11/●-red-green-blue-yellow-purp-•-Untitled-2-growi-WSL_-Ubuntu-20.04-Visual-Studio-Code-2022-11-25-00-00-32.mp4 そんな場面があるかはわかりませんが配列の各文字列からオブジェクトを作りました。 まず[]はいらないので普通に消します。 次にカンマを選択し、 Add Selection To Next Find Match を使いそれぞれ改行します。その際に全部の行頭の位置を揃えます。 escape でいったんカーソルを外し、1行目頭にカーソルを置き、AddCursorBelow によって各行の頭にカーソルを持ってきます。 あとは一つの行に注目し、オブジェクトを作るような操作をすれば全行でオブジェクトが完成します。この際に各行の差異が出ないようになるべくカーソルキーを単体で使わないことが大事です。 まとめ 今回は一つ具体例を挙げましたが、使い方はもっといろいろあるはずです。 マルチカーソルが手になじむとちょっとしたときに大変便利で時間短縮ができます、是非使ってみてください。 また、拡張機能と組み合わせることで各カーソルで選択中のものの命名ケースを変えたり(camel case -> snake case)、HTML の閉じタグをカーソルごとに追加したり、といろいろな使い方ができるので是非探してみてください。 参考 https://code.visualstudio.com/docs/editor/codebasics#_multiple-selections-multicursor
アバター
はじめに こんにちは、システムエンジニアの蛸井です。 最近は React のパフォーマンスチューニングにハマっており、どのようなコードを書くのが最適なのか気になったため色々と調べてきました。 今回の記事では、パフォーマンスチューニングの中でも React Hooks の useCallback に絞って、適切な使い方・使い時について詳しく解説します。 useCallback とは React Hooks の useCallback について解説します。 公式ドキュメントには以下のように記載されています。 https://ja.reactjs.org/docs/hooks-reference.html#usecallback メモ化されたコールバックを返します。 インラインのコールバックとそれが依存している値の配列を渡してください。useCallback はそのコールバックをメモ化したものを返し、その関数は依存配列の要素のいずれかが変化した場合にのみ変化します。これは、不必要なレンダーを避けるために(例えば shouldComponentUpdate などを使って)参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合に便利です。 メモ化というのは、呼び出しの結果をキャッシュしておき、再度同じ入力が発生したときにキャッシュを再利用することにより再計算を防ぐという手法です。 つまり、適切にメモ化を行うことにより、プログラムの実行速度を向上させられます。 useCallback の構文は以下のようになっており、 useCallback(関数, 依存配列) 実際の書き方はこのようになります。 const memoizedCallback = React.useCallback( () => { doSomething(a, b); }, [a, b] ); 第二引数の [a, b] の部分が依存配列であり、この配列に格納された要素のいずれかが変更されたときに関数が再生成されます。 依存配列の要素に変更がなければ、コンポーネントが再レンダリングされたとしてもこの useCallback は同じ関数を返します。 React.memo とは 次の章で詳しく話しますが、useCallback と深くかかわっている React.memo についても軽く解説します。 React.memo とは、コンポーネントをメモ化する関数です。 https://ja.reactjs.org/docs/react-api.html#reactmemo const MyComponent = React.memo((props) => { /* render using props */ }); コンポーネントのレンダリング結果をキャッシュしておき、コンポーネントに渡された props が変わらなかった場合にそのコンポーネントのレンダリングをスキップし、キャッシュしたレンダリング結果を再利用します。 つまり親コンポーネントが再レンダリングされたときに、React.memo 化した子コンポーネントの props に変化がなければ子コンポーネントの再レンダリングをスキップできるため、パフォーマンスが向上します。 useCallback はどういう時に使うべきか 結論から先に書くと、useCallback は基本的に、先ほど解説した React.memo と併用して使います。 React.memo 化したコンポーネントに関数を渡す際、useCallback でメモ化した関数を渡すことでパフォーマンスが向上します。 公式の useCallback の説明文を再掲します。 https://ja.reactjs.org/docs/hooks-reference.html#usecallback これは、不必要なレンダーを避けるために参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合に便利です。 useCallback を使う一番の目的は 不要なレンダーを避けるため です。 ちなみにこの 参照の同一性を見るよう最適化されたコンポーネント というのが React.memo 化したコンポーネントになります。 React.memo 化したコンポーネントは、呼び出された時に props に変更がないかチェックを行い、変更がなかった時に再レンダリングをスキップします。 ここで重要になってくるのが props に変更がないかのチェックの方法です。 React では、等価性の比較を Object.is 関数により行っています。( === 演算子とだいたい同じ) React.memo の props の比較だけでなく、useEffect や useCallback, useMemo の依存配列の比較などにも Object.js が使用されています。 Object.is で値が同一であるかどうかをチェックしますが、オブジェクトや配列や関数などのオブジェクト型のもの (プリミティブではないもの) を比較する場合、その値そのものを比較するのではなく参照先の情報を比較します。 詳しくは オブジェクト参照とコピー つまり、同じ key, value が入ったオブジェクトや同じ値が入った配列、同じコードで書かれた関数であっても、再レンダリング前と再レンダリング後ではそれらは完全に 別物 として扱われます。 これによりどういった問題が起こるのかというと、React.memo 化した子コンポーネントにオブジェクト型のもの (今回の例では関数) を渡している場合、親コンポーネントが再レンダリングされたときに props が変わったとみなされ、必ず再レンダリングが発生してしまいます。 const ParentComponent = () => { const sampleFunction = () => {} return <ChildComponent sample={sampleFunction} > } const ChildComponent = React.memo((props) => { /* 省略 */ }); このようなケースでは React.memo を使用している意味が全くありません。 ではどうすれば良いのかというと、useCallback を使います。 const ParentComponent = () => { const sampleFunction = React.useCallback(() => {}, []) return <ChildComponent sample={sampleFunction} > } const ChildComponent = React.memo((props) => { /* 省略 */ }); useCallback を使用することにより、依存配列の中身が変わらなかった場合メモ化された関数、つまり再レンダリング前と同一の関数が返るため、この関数を渡している子コンポーネントで props が変化していないとみなされて再レンダリングがスキップされます。 これが、この章で最初に記載した useCallback は基本的に React.memo と併用して使う という理由です。 その他の使い方としては、useEffect や useMemo, useCallback などの依存配列に関数を入れる時です。 useEffect(() => { doSomething() }, [doSomething]) 依存配列に関数を入れる場合、useCallback した関数を入れてあげると不要な実行を防げます。 非推奨ですが、useCallback せずにそもそも依存配列に関数を渡さないという手もあります。(eslint を導入している場合、ルールによっては警告が出たりする) useEffect(() => { doSomething() }, []) useEffect などの実行は、レンダリングと比較してかかるコストが非常に小さいため、ここまでして再実行を防ぐ必要はあまりないと思っています。 いずれにせよ、不要なレンダーを防ぐことが useCallback を使用する一番の目的です。 どんな時でも使って良いのか ここでは、単に関数自体の再生成を防ぐ目的で useCallback を使用してパフォーマンスが向上するかどうかについて解説します。 const MyComponent = () ={ const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], ); return ( /* 省略 */ ) } 上記の例の場合、MyComponent コンポーネントが再レンダリングされるたびに useCallback が呼び出され、(依存配列が変わらなければ) memoizedCallback には毎回同じ関数オブジェクトが格納されます。 ただし、キャッシュした結果を再利用するとはいえ、インライン関数部分はレンダリングのたびに毎回作成されます。 () => { doSomething(a, b); } また、依存配列の中身をチェックする作業だったり、メモ化をするという処理自体にコストがかかるため、上記の例ではそのコストに見合うだけの効果はありません。 そのため結論としては、関数自体の再生成を防ぐ目的で useCallback を使用してもパフォーマンスが向上することはなく、決してどんな時でも useCallback を使用して良いとは言えません。 この話は以下のサイトで詳しく解説されています。 https://prateeksurana.me/blog/when-should-you-memoize-in-react/#usecallback そもそも使い時をしっかり考えて開発すべきなのか 実際に計測したわけではないですが、何も考えずに useCallback を使用した場合、おそらくパフォーマンスは悪化します。 ただしこれによるパフォーマンスの悪化は本当に誤差レベルなため、特に問題があるわけではありません。 どちらかといえば、メモ化用の関数でラップする分コードが若干複雑になってしまうことのほうが問題であると個人的に思ってます。 React はデフォルトで非常に高速で動作するため、メモ化によって再レンダリングを数回防いだ程度ではほぼ何も変わりません。 たいていの場合メモ化による最適化は気にしなくてよく、このような最適化をするよりも他のことに時間を割くべきだといった声もあります。 アプリケーションが重くなってきて初めてメモ化による最適化を考える、といった感じで良いのではないかと考えています。 さいごに 結論としては、不要なレンダーを防ぐことが useCallback を使用する一番の目的であり、useCallback は React.memo 化したコンポーネントに関数を渡す場合にのみ使えば良いことが分かりました。 調べていく中で、そもそもしっかりとしたパフォーマンスチューニングを行う必要はないといった声がちらほらあったのは意外でした。
アバター
はじめに こんにちは、システムエンジニアの @cao19x です。この記事では、Dart sass が提供する @use の基本的な使い方をご紹介します。 まだ@import を使っている方、@useを聞いたことがあるがよくわからない方、これから@useを使ってみたい方 などが対象読者となっています。 [toc] 今までの @import (公式により非推奨) @import は、公式で廃止予定であることが明言されており、「遅くとも2021年10月1日には非推奨」、「遅くとも2022年10月1日にサポート終了」予定となっています。そのため、Sass を新しい環境に適した記法に改修していく必要があります。 主なデメリット @import が廃止されると言うことは、何かしらのデメリットがあると言うことですが、具体的に何がだめだったのでしょうか? これは、 デバックが困難 であることが大きな要因としてあげられます。 以下のコードを例に見てみましょう。 card クラスの 背景色 に $gray-300 を指定しています。 @import で読み込んでいるスタイルシートが 3つありますが、 $gray-300 はどのスタイルシートで定義された変数なのかを知ることはできません。 @import '../styles/bootstrap/piyo'; @import '../styles/bootstrap/hoge'; @import '../styles/bootstrap/fuga'; .card { background: $gray-300; // importされた fileのうち、どこで定義された変数なのかがわからない! } 上記のような デメリットを補完する形で生まれたのが @use になります。 @useの使い方 Sass変数の定義 /styles/_variables.scss $gray-300: darken($light, 5%) !default; variables file に $gray-xxx などの、グレー系カラーの変数を定義しているファイルがあるとします。 variables file で定義された変数を使用 上記で定義されたグレー系カラーを実際に使用するには、以下の3ステップで使用できます。 @use '../styles/_variables.scss; のように「@use + fileへのパス」 とすることで、ファイルの読み込みをする variables.$gray-300 のようにしてスタイルを指定 @use '../styles/variables'; @use '../styles/bootstrap/piyo'; .card { background: variables.$gray-300; // $gray-300 は variables file で定義されていることがわかる // @useを使えば nsの役割を持つので、デバッグしやすい! } ちなみに、namespace は as 修飾子を使うことで変更することができます。 @use '../styles/bootstrap/variables' as var; .card { background: var.$gray-300; } @use におけるベストプラクティス 前提として、@use でできることは主に以下の2つがあります。 ① @use によるスタイルシートへのアクセス ② アクセスしたスタイルシート内で指定されているスタイルの適用 上記を抑えた上で、問題点もあります。 それは、「@use で読み込むファイル内に スタイルを適用するようなコード が含まれている場合そのスタイルが適用されてしまう」という点です。 よくわからないと思うので、以下を例に見てみましょう。 【例】.subnavbar の背景色指定に、nav-bar.scss にある Sass 変数を使いたい アクセスされる側のスタイルシートで、navbar の 背景色を yellow と指定しても、アクセスする側のスタイルシートで、 navbarの背景色をblue と記述すれば、 後に読み込まれる方の指定が上書きされてしまいます。 アクセスされる側 のスタイルシート /box.scss $bgcolor-box: yellow; $textcolor-box: red; .box { background: $bgcolor-box; // このスタイルシートを読み込んだ時点で、このスタイルが適用される。ここでは黄色。 } アクセスする側 のスタイルシート /example.scss @use './box '; .box { // これがアクセスされる側にある .box {} を上書きしてしまう。青色になる。 background: blue; } よって、理想は以下のコードように @use で読み込むファイルの中は、参照のためだけのものに絞り、スタイルを適用するコードは書かないようにすることがベストプラクティスだと言えるでしょう。 /box.scss // 定義のみを記載 $bgcolor-box: yellow; $textcolor-box: red; まとめ ・@import は2022年で廃止される ・@import のデメリットを補完する形で生まれたのが @use ・@use は namespase の役割を持つのでデバッグしやすい ・@use で読み込むファイルの中は、参照のためだけのものに絞り、スタイルを適用するコードは書かないようにする 最後まで読んでいただきありがとうございました!
アバター
こんにちは 田村 です。 STI を使用して複数の type のモデルデータがあり、これを 1 つの API エンドポイントで混ぜて返したいとします。 このとき、 STI の type に応じて serializer を切り替えたくなります。 今回は、その方法を紹介します。 STI については こちら を参照してください。 困ったこと 以下のようなクラス階層を STI で実現しているとします。 SimplePosts は title と body を、 QAPosts は question と answer のフィールドを持ちます。 共通して posted_at のフィールドを持ちます。 /api/v1/posts という API エンドポイントを定義して、下記のような JSON を返したいとします。 { "posts": [ { "id": 3, "type": "QAPost", "posted_at": "2022-11-02T17:00:00.000Z", "question": "質問2", "answer": "質問2の回答です。" }, { "id": 4, "type": "SimplePost", "posted_at": "2022-11-02T15:00:00.000Z", "title": "投稿2", "body": "Consectetur adipiscing elit." }, { "id": 2, "type": "QAPost", "posted_at": "2022-11-01T16:00:00.000Z", "question": "質問1", "answer": "質問1の回答です。" }, { "id": 1, "type": "SimplePost", "posted_at": "2022-10-31T15:00:00.000Z", "title": "投稿1", "body": "Lorem ipsum dolor sit amet." } ] } Posts の type が SimplePost か QAPost かによって、 JSON のキーが異なります。 JSON の出力を ActiveModel::Serializers で行っている場合、 Posts の type に応じて、 Serializer を切り替える必要が出てきます。 解決方法 先にコードを示します。 app/serializers/post_serializer.rb class PostSerializer < ActiveModel::Serializer attributes :id, :type, :posted_at def attributes(requested_attrs = nil, reload = false) @attributes = super(requested_attrs, reload) case object when SimplePost @attributes.merge(SimplePostSerializer.new(object).attributes(nil, reload)) when QAPost @attributes.merge(QAPostSerializer.new(object).attributes(nil, reload)) end end def json_key 'posts' end end app/serializers/simple_post_serializer.rb class SimplePostSerializer < ActiveModel::Serializer attributes :title, :body end app/serializers/qa_post_serializer.rb class QAPostSerializer < ActiveModel::Serializer attributes :question, :answer end app/controllers/api/v1/posts_controller.rb module Api module V1 class PostsController < ApplicationController def index posts = Post.all.order(posted_at: :desc) render json: posts, each_serializer: PostSerializer end end end end PostSerializer で attributes メソッドをオーバーライドします。各データは object で取れるため、 case で class を判定し、 SimplePostSerializer.new(object).attributes(nil, reload) と QAPostSerializer.new(object).attributes(nil, reload) で、 attributes を生成し @attributes にマージしています。 参考: https://github.com/rails-api/active_model_serializers/blob/0fbe0fad0dec9368e9335b6280a46ca13442727e/lib/active_model/serializer.rb#L334-L343 json_key メソッドをオーバーライドしているのは、 json の toplevel object で posts と表示させるためです。 これを定義しないと、下記のように qa_posts と、最初に Serializer で処理した type が key としてセットされてしまいます。 { "qa_posts": [ { "id": 3, "type": "QAPost", "posted_at": "2022-11-02T17:00:00.000Z", "question": "質問2", "answer": "質問2の回答です。" }, ... ] } なお、 jsonapi_include_toplevel_object を false にしている場合は、 json_key メソッドのオーバーライドは不要です。 参考: https://github.com/rails-api/active_model_serializers/blob/0fbe0fad0dec9368e9335b6280a46ca13442727e/lib/active_model/serializer.rb#L384-L392
アバター
はじめまして。エンジニアの Ryo です。本記事では、最近、勢いに乗ってきているオンライン決済サービス Stripe を用いてオンラインサービス提供者向けに、特定の顧客に対して割引を行う方法をご紹介します。 ※こちらの記事は、すでに Stripe をサービスに導入しているという前提で作成されております。もし、Stripe を検討中でまだ導入されてない場合は、以下の記事で Stripe 導入のメリットや扱うデータの種類、実際にサービスでどのように利用されているかなどを紹介していますのでご参照ください。 Stripeを使った簡単なサブスク型課金サービスの作り方 Stripe での割引方法について Stripe ではユーザーのサブスクリプション (定期支払い)に割引を発生させるために 「トライアル」 と 「クーポン」 の2パターンの方法が提供されています。 「トライアル」 は、「○ヶ月間割引。期間終了後、課金が発生」を実現する機能です。ユーザーがトライアル有りで商品を契約するとユーザーは割り引かれた額で請求を受け、設定されたトライアル終了日まで特定の割引額でサービスを利用できます。また、全額割引の設定にすることで無料でトライアルを開始させることもできます。 主にこれからサービスを利用してもらうユーザー向けの割引が「トライアル」となっています。 次に「クーポン」ですが、こちらには割引額を自由に設定でき、設定した期間の間、ユーザーへの請求に割引を発生させることができます。 クーポン適用には主に以下2つの方法があります。 直接 Stripe の管理画面からユーザーのサブスクリプションに対してクーポンを適用させる方法 ユーザーの決済時に合わせクーポンコードを入力させてクーポンを利用させる方法 以降で①の方法でクーポンを適用させる手順を紹介します。 Stripe でのクーポン作成手順 クーポンの適用方法の紹介の前に、まずは Stripe の管理画面でクーポンを作成する方法を紹介します。 まずは、 クーポン一覧ページ にアクセスする 「+ 新規」 ボタンを押す 表示されたクーポン作成フォームの入力項目を埋めて「クーポンを作成」を押す 基本的に作成の流れは上記のとおりです。 次にクーポン作成フォームに関して各項目の説明をします。 クーポン作成フォームの入力項目 名前 クーポン名を入力できます。他のクーポンと見分けがつきやすい名前を付けておくことをおすすめします。 ID 作成するクーポンに付与する ID を入力できます。 この ID は、Stripe が自動で発行してくれるため、基本的に空欄にしておくことが多いです。 タイプ 割引の種類を「パーセント割引」と「定額割引」から選べます。 パーセント割引の場合 割引 率 を入力します。 定額割引の場合 割引 額 を入力します。また、通貨を選択できます。 特定の商品に適用 ON にすることで指定した商品に対してのみクーポンを設定できます。 クーポンが適用できる商品を設定しておくことで、誤った商品への割引のような操作ミスを防ぐことができます。 期間 クーポンの割引期間として、「無期限」「1回のみ」「複数月」の3種類から選べます。 また、「複数月」を選択することで任意の月数を設定できます。 例えば、11月~翌年4月までの「6カ月間」のように特定の期間内で割引を発生させたい場合にここでの設定が有用です。 引き換え回数制限 クーポンの引き換えに対し期間や回数で制限をかけることができます。  顧客に表示されるクーポンコード 作成したクーポンをユーザーが利用するためのコードを設定できます。 管理画面からの操作で個別にクーポンをユーザーに設定するだけであれば不要です。 作成したクーポンの適用手順 Stripe の管理画面からサブスクリプション商品を契約中のユーザーにクーポンを適用させる方法を紹介します。 1. ユーザーのサブスクリプション更新フォームで「クーポンを追加」を選択する 2. 適用したいクーポンを選択する クーポンを選択し「送信」を押すと追加されます。 3. サブスクリプションを更新させてクーポンを適用させる 以上で Stripe の管理画面からクーポンを適用させることができます。 おわりに Stripe の管理画面でクーポンの作成と適用手順を紹介しました。 作成から適用までが簡単に行えるのでぜひこのクーポン機能を利用してみてください。 参考資料 https://stripe.com/docs/billing/subscriptions/coupons
アバター
経緯 css modules ファイル内に、html セレクタを直接記述したらエラーに遭遇したので、原因と解説、解決方法を簡単にまとめました。 やったこと css modules ファイル内で、 <h1> タグ内の文字色を 黄色 にするクラス指定を追記。 import styles from '../styles/xxx.module.scss' export default function Home() { return ( <div className={styles.container}> <h1>Welcome to My app!</h1> // ここの色を黄色に変えたい </div> ) } .container { padding: 0 2rem; } // ↓↓ 以下を追記 ↓↓ h1 { color: yellow; } エラー内容 ./styles/xxx.module.css:131:1 Syntax error: Selector "h1" is not pure (pure selectors must contain at least one local class or id) シンタックスエラー。 セレクタ "h1" は pure ではありません( pure なセレクタは、少なくとも1つのローカルクラスまたは ID を含む必要があります)。 解説 上記でいう pure とは、自前で用意する .top-page-title や #top-page などの classセレクタ や idセレクタ を表しています。 反対に html , body , button , h1 などは impure なセレクタになります。 原因 つまりエラーの原因は xxx.module.scss という css modules ファイル内に、 impure なセレクタを記述していること。 ルールとして、css modules ファイルでは impure なセレクタは使用できません。 解決方法1 css modules ファイルに pure な classセレクタ(id でも可)を用意し、 import したうえでその style を適用してあげる。 これが基本的な使い方になります。 .container { padding: 0 2rem; } // クラスセレクタを用意 .title { color: yellow; } import styles from '../styles/xxx.module.scss' // import export default function Home() { return ( <div className={styles.container}> // styles.title で適用 <h1 className="{styles.title}">Welcome to My app!</h1> </div> ) } 解決方法2 (ネストができる Sass であれば) pure なセレクタ内に記述する。 実は、pure なセレクタの下であれば、 h1等を記述することができます。 .container { padding: 0 2rem; // .container の中に h1 を記述 h1 { color: yellow } } コンパイル後の css css modules の場合、コンパイルされた css は基本的にセレクタの後に hashのような値が付与されます。(下画像の __ngV3c の部分) css modules ファイル名の頭部分 + _pureセレクタ名 + __hashのような値 しかし、classセレクタ、idセレクタでない h1 や p などは、 hashのような値が付与されないようになっています。 まとめ いかがでしたでしょうか。 css modules は css のグローバルスコープ汚染問題を回避するための一つの仕組みです。 つまり、 css modules ファイル内には、基本的に特定の対象にのみクラスを当てるような記述(つまりクラスセレクタ等を用意)をすべきで、グローバルに適用したいスタイルを css modules 内に記述すべきではないんですね。 グローバルに適用したいものはグローバルなファイルを用意して、グローバルな場所で import するようにしましょう。
アバター
経緯 Next.js Conf 2022 でNext.js 13に関する発表、その中には Turbopack なる大変アツい代物がありました。 Next.js 13 Blog から 700倍だと?? これは試してみたいですね。 TL;DR 2022/10/27 現在、v13.0.0 ではカスタムサーバー(Custom Server)向けに Turbopack を使う方法はありません。 バージョンアップを待ちましょう。 試してみる ブログでは優位性について記述があるものの、実際の手順はあまり詳しくは書いてない。。 Try out the Turbopack alpha today in Next.js 13 with next dev --turbo. next dev --turbo で試せるらしい。 が、我々が知りたいのは カスタムサーバー(Custom Server) での実現方法である。つまり、 // setup Next.js this.nextApp = next({ dev }); await this.nextApp.prepare(); みたいなコードに対して turbo: true みたいなのを渡したら動く、みたいなのを期待してる。 dev server with Turbopack 起動方法調査 (v13.0.0 on 10/27) ブログにはこういう Note もあって↓ Note: Turbopack in Next.js currently only supports next dev. View the supported features. We are also working to add support for next build through Turbopack. なるほど? この only supports next dev というのは、開発中しかサポートしてないよという意味なのか、 next dev コマンドからの利用しかサポートしてないよという意味なのか… (2行目の next build との対比だとすると後者のような気もする) ソースを確認してみると https://github.com/vercel/next.js/blob/v13.0.0/packages/next/cli/next-dev.ts#L104-L290 がっつり場合分けで結構な量の挙動の変更が行われている。そしてリポジトリ中にこの箇所以外に turbopack 関連で挙動を変えている部分はなさそう。(そこまで隅々まで調べてないけど) https://github.com/vercel/next.js/blob/v13.0.0/packages/next/server/dev/next-dev-server.ts DevServer にも turbopack 関連のオプション・実装はなさそう。 というわけで、 結論: まだ無理 待ちましょう。
アバター
みなさん、こんにちは kota です。 日頃、業務では Rails を使っているのですが、先日 hash のキーの扱いについて、再認識したことがあったので、簡単に共有したいと思います。 困ったこと model からデータを所得し、変数に入れ、その変数に対し attributes メソッドを使って hash 化した。 hash 化した変数から name の値を取得しようと user[:name] でアクセスするも、nil が返って来て取得できなかった。 [1] pry(main)> user = User.first User Load (13.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]] => #<User id: 1, name: "hoge", created_at: "2022-09-30 08:24:03", updated_at: "2022-09-30 08:24:03"> [2] pry(main)> user = user.attributes => {"id"=>1, "name"=>"hoge", "created_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00, "updated_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00} [2] pry(main)> user[:name] => nil 調査 先述した通り model から取得したデータに対して、attributes メソッドを使った際の返り値は下記の通りでした。 ここで、キーが文字列になっていることに気付きました。 [2] pry(main)> user = user.attributes => {"id"=>1, "name"=>"hoge", "created_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00, "updated_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00} 当たり前ですが、値を取得するには定義されている形式と同じ形式でアクセスしないと取得できないことを改めて認識しました。 [4] pry(main)> user["name"] => "hoge" params の値を取得する際などにシンボルを使うことが多かった為、少々ハマってしまったというお話でした。 追加調査・メソッドまとめ いい機会なので hash についてもう少し調べ、使えそうなメソッドをまとめました。 キーを文字列からシンボルに変換 symbolize_keys Rails が用意しているメソッド。内部的には、ruby の transform_keys(&:to_sym) メソッドを使用している。 参考 https://api.rubyonrails.org/classes/Hash.html#method-i-symbolize_keys https://docs.ruby-lang.org/ja/latest/method/Hash/i/transform_keys.html [1] pry(main)> user.symbolize_keys => {:id=>1, :name=>"hoge", :created_at=>Fri, 30 Sep 2022 17:24:03 JST +09:00, :updated_at=>Fri, 30 Sep 2022 17:24:03 JST +09:00} キーをシンボルから文字列に変換 stringify_keys Rails が用意しているメソッド。内部的には、ruby の transform_keys(&:to_s) メソッドを使用している。 参考: https://api.rubyonrails.org/classes/Hash.html#method-i-stringify_keys [1] pry(main)> user.stringify_keys => {"id"=>1, "name"=>"admin", "created_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00, "updated_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00} hash のネストも変換したい 上記2つのメソッドの注意点として、hash 内にネストがあると、そのネストは変換されません。 [1] pry(main)> user => {"id"=>1, "name"=>"hoge", "created_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00, "updated_at"=>Fri, 30 Sep 2022 17:24:03 JST +09:00, "group"=>{"id"=>1, "group_name"=>"fuga"}} [2] pry(main)> user.symbolize_keys => {:id=>1, :name=>"hoge", :created_at=>Fri, 30 Sep 2022 17:24:03 JST +09:00, :updated_at=>Fri, 30 Sep 2022 17:24:03 JST +09:00, :group=>{"id"=>1, "group_name"=>"fuga"}} <- シンボルに変換されてない ネストも変換したい場合は、 deep_symbolize_keys (deep_stringify_keys) を使うことで、変換が可能です。 [3] pry(main)> user.deep_symbolize_keys => {:id=>1, :name=>"hoge", :created_at=>Fri, 30 Sep 2022 17:24:03 JST +09:00, :updated_at=>Fri, 30 Sep 2022 17:24:03 JST +09:00, :group=>{:id=>1, :group_name=>"fuga"}} ※ ruby のメソッド transform_keys も同様に deep_transform_keys で、ネストの変換も可能 文字列でもシンボルでも値を取得したい with_indifferent_access 先ほどまで使っていた変数 user の class を確認します。 [1] pry(main)> user.class => Hash ここで、 with_indifferent_access というメソッドを使い class を ActiveSupport::HashWithIndifferentAccess に変更してみます。 参考 https://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html [1] pry(main)> user = user.with_indifferent_access ... [2] pry(main)> user.class => ActiveSupport::HashWithIndifferentAccess 文字列とシンボルの両方で値を取得してみます。 [3] pry(main)> user[:name] => "hoge" [4] pry(main)> user["name"] => "hoge" 文字列、シンボルの両方で値を取得することが出来ました。 params もこの ActiveSupport::HashWithIndifferentAccess class を継承していて、文字列、シンボルの両方で値を取得することが出来ます。 最後に いかがだったでしょうか? 今回は、Rails での hash のキーの扱いや関連するメソッドについて、簡単にまとめてみました。 hash に関わる便利なメソッドは多々ありますので、ご自身でも調べてみて下さい。
アバター