20211127のReactに関する記事は9件です。

styled-componentsで条件付きmixinをする方法

はじめに styled-componentsで条件付きレンダリングをする場合、以下のように、項目ごとに場合分けをする例をよく見ます 基本はこれで問題ないのですが、場合分けの種類が増えて来ると、管理が面倒になります。 この場合、場合分けの条件ごとにclassのようにスタイリングをまとめたいと思うのが自然だと思います。 そこで、複数の項目をclassの有無のように一気に設定した場合についてわからなかったので、調べました。 const Wrapper = styled.div.attrs((props) => ({ ...props, }))` background: ${(props) => (props.hoge ? '#fff' : '#000')}; ` やりたいこと 以下のようにpropsの条件一つでスタイリングが大きく変わる場合を想定します。 上の場合と下の場合で、個別にcssを管理することを目標とします。 まずはデフォルトの状態を準備 まずは、props.typeに何も指定していない、アニメーションのない状態を実装します。 import React, { VFC } from "react"; import styled from "styled-components"; export type ButtonProps = { className?: string; type?: "bgleft" | "bgright"; }; const Button: VFC<ButtonProps> = ({ className, type }: ButtonProps) => { return ( <WrapperButton className={className} type={type}> <span>ボタン</span> </WrapperButton> ); }; const WrapperButton = styled.a.attrs((props) => ({ ...props }))` /* default */ position: relative; overflow: hidden; text-decoration: none; display: inline-block; border: 1px solid #555; padding: 10px 30px; text-align: center; outline: none; transition: ease 0.2s; span { position: relative; z-index: 3; color: #333; } :hover { span { color: #fff; } } `; export default Button; MixInの定義 次に左右から動作するアニメーションをMixInで定義します。 最初に、styled-componenstからcssをインポートします。 import React, { VFC } from "react"; import styled, { css } from "styled-components"; //ここ 次にMixInを定義します。 それぞれ、左右から動作する部分のcssです。 const MixInBgLeft = css` &:before { content: ""; position: absolute; top: 0; left: 0; z-index: 2; background-color: #333; width: 100%; height: 100%; transition: transform 0.6s cubic-bezier(0.8, 0, 0.2, 1) 0s; transform: scale(0, 1); transform-origin: right top; } &:hover:before { transform-origin: left top; transform: scale(1, 1); } `; const MixInBgRight = css` &:before { content: ""; position: absolute; top: 0; left: 0; z-index: 2; background-color: #333; width: 100%; height: 100%; transition: transform 0.6s cubic-bezier(0.8, 0, 0.2, 1) 0s; transform: scale(0, 1); transform-origin: left top; } &:hover:before { transform-origin: right top; transform: scale(1, 1); } `; 条件付きでレンダリング 最後にstyledの中にMixInを条件付きで追加します。 const WrapperButton = styled.a.attrs((props) => ({ ...props }))` //....省略 /* typeにbgleftを選択した場合 */ ${(props) => props.type === "bgleft" && MixInBgLeft} /* typeにbgrightを選択した場合 */ ${(props) => props.type === "bgright" && MixInBgRight} `; このbuttonを使う側は以下のようにして使います。 import Button from '***' <Button type="bgleft" /> // or <Button type="bgright" /> 最後に 今回の例だと、最初に紹介した、項目ごとに場合分けする場合でもよかったかも知れませんが、props.typeが増える場合を考えると、このような方ほでの実装も悪くはないかも知れません。 もっと良い方法があればぜひ共有してください。 最後に全部まとめたコードを貼ります。 import React, { VFC } from "react"; import styled, { css } from "styled-components"; export type ButtonProps = { className?: string; type?: "bgleft" | "bgright"; }; const Button: VFC<ButtonProps> = ({ className, type }: ButtonProps) => { return ( <WrapperButton className={className} type={type}> <span>ボタン</span> </WrapperButton> ); }; const MixInBgLeft = css` &:before { content: ""; position: absolute; top: 0; left: 0; z-index: 2; background-color: #333; width: 100%; height: 100%; transition: transform 0.6s cubic-bezier(0.8, 0, 0.2, 1) 0s; transform: scale(0, 1); transform-origin: right top; } &:hover:before { transform-origin: left top; transform: scale(1, 1); } `; const MixInBgRight = css` &:before { content: ""; position: absolute; top: 0; left: 0; z-index: 2; background-color: #333; width: 100%; height: 100%; transition: transform 0.6s cubic-bezier(0.8, 0, 0.2, 1) 0s; transform: scale(0, 1); transform-origin: left top; } &:hover:before { transform-origin: right top; transform: scale(1, 1); } `; const WrapperButton = styled.a.attrs((props) => ({ ...props }))` /* default */ position: relative; overflow: hidden; text-decoration: none; display: inline-block; border: 1px solid #555; padding: 10px 30px; text-align: center; outline: none; transition: ease 0.2s; span { position: relative; z-index: 3; color: #333; } :hover { span { color: #fff; } } /* typeにbgleftを選択した場合 */ ${(props) => props.type === "bgleft" && MixInBgLeft} /* typeにbgrightを選択した場合 */ ${(props) => props.type === "bgright" && MixInBgRight} `; export default Button;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】【Vue】 など流行りのフレームワークを使うもののためのJS基礎

流行りのフレームワークを使っていると忘れがち(意識しなくてよい)JSの基礎を学び直してみた。 関数とメソッドの違い 結論:オブジェクトに入っているのかどうか 関数→複数の箇所から共通で呼び出せるプログラムの塊 メソッド→オブジェクトがプロパティとして持っている関数 例 ・関数 const sample = () => { console.log("hello") } sample() ・メソッド const person = { name:"菅田将暉", age:28, sample:()=>{ console.log("結婚おめでとう!") } } person.sample() クラス・インスタンス ・クラス 設計図をイメージ クラス名は基本的に大文字から始める 例 class Car{ } ・インスタンス  オブジェクトを生成するための設計図(class)が用意できたので、  その設計図から実際にオブジェクトを生成する為には、「new クラス名()」とする。  クラスから生成したオブジェクトはインスタンスと呼ぶ。 例 class Car{ } // Carクラスのインスタンスを定数carに代入 const car = new Car(); ・コンストラクタ  クラスにはコンストラクタと呼ばれる機能が用意されている。  コンストラクタはインスタンスを生成するときに実行したい処理や設定を追加するための機能。  クラスの中括弧 { } 内に 「constructor() { }」と記述する。 例 class Car{ // クラスの中に追加 constructor() { } } コンストラクタの処理  コンストラクタの中に処理を記述することで、ここに書いた処理は  インスタンスが生成された直後に実行される。  そして、大切なのは、インスタンスごとに毎回実行されるという事。 例 class Car{ constructor() { console.log("新車が発売されます!"); } } const car1 = new Car(); const car2 = new Car(); 出力結果 // インスタンスごとに実行される 新車が発売されます! 新車が発売されます! プロパティと値を追加  コンストラクタの中で、生成したインスタンスに関する情報を追加するには、  コンストラクタの中で「this.プロパティ = 値」とする事で、  生成されたインスタンスにプロパティと値を追加する事が出来る。 例 class Car{ constructor() { this.プロパティ名 = 値; } }  コンストラクタの中で追加した値は、  「インスタンス.プロパティ」とする事でクラスの外で使用出来る。 例 class Car{ constructor() { this.name = "トラック"; } } const car = new Car(); // 「名前: 〇〇」となるように出力 console.log(`名前: ${car.name}`); 出力結果 名前: トラック 引数ごとに値を変える  コンストラクタでは、関数と同じように、引数を受け取ることが可能。  「constructor」の後の括弧「( )」内に引数名を記述することで、  その引数をコンストラクタの処理内で使用出来る。  コンストラクタに引数として値を渡すには、  「new クラス名( )」の括弧「( )」内に値を追加する。 例 class Car{ constructor(name, color) { this.name = name; this.color = color; } } // 引数に「"トラック"」と「黒」を渡す const car = new Car("トラック", "黒"); // 「名前: 〇〇」となるように出力 console.log(`名前: ${car.name}`); // 「色: 〇〇」となるように出力 console.log(`色: ${car.color}`); 出力結果 名前: トラック 色: 黒 メソッド  メソッドとはそのインスタンスの「動作」のようなもの。  「名前」や「年齢」などの情報はプロパティで追加したのに対して、  メソッドは「挨拶をする」「値を計算する」などの処理のまとまりを表す。 メソッドの定義  メソッドはクラスの中で定義します。  「メソッド名() { }」とすることでメソッドは定義出来る。  メソッドは関数と似たようなもので、中括弧「{ }」の中にそのメソッドで行いたい処理を記述する。 例 class Car{ メソッド名() { // 行いたい処理 } } メソッドの呼び出し方  「インスタンス.メソッド名()」とする事でそのメソッドを呼び出し、  処理を実行することが出来る。 例 class Car{ constructor(name, color) { this.name = name; this.color = color; } // actionメソッドを追加 action() { console.log("走る"); } } const car = new Car("トラック", "黒"); // carに対してactionメソッドを呼び出す car.action(); 出力結果 走る メソッド内で値を使う  メソッド内でインスタンスの値を使用するには、「this」という特殊な値を用いて、  「this.プロパティ名」とします。 例 class Car{ constructor(name, color) { this.name = name; this.color = color; } action() { console.log(`${this.name}は、走る`); } } const car = new Car("トラック", "黒"); // carに対してactionメソッドを呼び出す car.action(); 出力結果 トラックは、走る メソッド内でのメソッド呼び出し  メソッド内で他のメソッドを呼び出すことも可能。  メソッド内で「this.メソッド名()」とすることで、同じクラスの他のメソッドを使うことが出来る。 例 class Car{ constructor(name, color) { this.name = name; this.color = color; } // actionメソッド action() { console.log(`${this.name}は、走る`); } // infoメソッド info() { // actionメソッドを呼び出す this.action(); console.log(`この車は${this.name}です`); console.log(`${this.color}色です`); } } const car = new Car("トラック", "黒"); car.action(); 出力結果 トラックは、走る この車はトラックです 黒色です 実際の使用方法 実際クラスを作ってインスタンス化して使うことはあまりしないと思います。 元から用意されて物を使うのが一般的かなと思います。 //インスタンス化(初期化) let today = new Date() //引数を渡してインスタンス化 let newYear = new Date(2021,1,1) //インスタンスに元から入っているメソッドを使う today.getDate() newYear.getFullYear()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【デプロイ】Next.jsをGCP Cloud Run上にデプロイを行う

はじめに Next.js をGCP Cloud Run 上にデプロイする方法をまとめます ① GCPをインストール $ brew install google-cloud-sdk $ which gcloud /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud source "/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.bash.inc" source "/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/completion.bash.inc" ※zshの場合は、path.bash.inc → path.zsh.inc、completion.bash.inc → completion.zsh.inc に変更する ② 認証情報などを設定する $ gcloud init 説明に従って設定していく ③ Dockerを作成する (例) FROM node:16.6.2 WORKDIR /src ENV PORT 8080 ENV HOST 0.0.0.0 COPY . . RUN npm install --only=production RUN npm install --save-dev typescript @types/react @types/node RUN npm run build CMD npm start ④ gcloudでDockerが使えるよう認証 $ gcloud auth configure-docker ⑤ docker build を行う gcr.io : ホストする場所を指定 ※1 [project id] : プロジェクトid ※2 [app] : プロジェクト名 (自由に命名) tag : tag (自由に命名) docker build -t gcr.io/{project id}/{app}:{tag名} . ※2 プロジェクトIDは以下から見れる ⑥ GCP Container Registry に docker push する $ docker push gcr.io/{project id}/{app}:{tag名} ⑦ cloud run上で動かす 画像の「Cloud Run」をクリック CREATE SERVICEをクリック SELECT をクリック 先程 GCP Container Registry に Pushした Docker Imageを選択。(ここでいうと「be627de1a0」) そのままNEXTをクリック(他の設定はdocument等読んで、自分好みの設定にしてください) 赤い枠に囲ってある部分をクリックしていく (注意: アクセス制限をかけたい場合はここは慎重に) デプロイ完了を待って、URLをクリックすると  デプロイが完了している
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Testing Libraryで要素を取得する方法

はじめに ReactTestingLibrary(RTL)を利用する際に、要素を取得する方法をまとめました。本記事では単純にレンダリングのテストを行っているだけに留まりますが、RTLを使ってテストを始める取っ掛かりとして参考になればと思います。 対象読者 Reactで開発してて、これからテストを導入したい人 サンプルリポジトリはこちらです(参考程度に) ① テストファイルの中でコンポーネントをレンダリングする。 RTLは、Render結果であるDOM構造をテストするので実際のユーザー行動に近いテストをし易いのが特徴です。 コンポーネントのユニットテストをするには、まずはテストファイル内にコンポーネントをimportしてレンダリングしてあげる必要があります。そのためには、render関数を使用します。 render 任意のJSXを受け取ってレンダリングする関数です。 render関数実行後、Reactコンポーネントにアクセスできるようになります。 これがコンポーネントのユニットテストを書く際の初めの一歩です。 render describe("RenderingTest", () => { test("正しく表示されてること", () => { render(<RenderTest />); }); }); debug render関数でReactコンポーネントにアクセスできるようにしたら、debug関数によってコンポーネントのHTML構造を確認することができます。実際に確認してみましょう。 debug screen.debug() ショートハンドで書くこともできます。 ショートハンド describe("RenderingTest", () => { test("正しく表示されてること", () => { const { debug } = render(<RenderTest />); debug(); // コンソールにレンダリング結果を出力 }); }); debug関数を実行すると以下のような結果が得られます。 出力結果 console.log <body> <div> <div> <h2> RenderTestSample </h2> <div> <img alt="ReactLogo" src="/logo192.png" /> </div> <div> これはレンダリングテストのサンプルです。 <span> forStudy </span> </div> <div> <label for="count" > Count: </label> <input id="count" placeholder="Enter" type="text" value="defaultValue" /> </div> <button name="increment" type="button" > Increment </button> <button name="decrement" type="button" > Decrement </button> <button name="reset" type="button" > Reset </button> </div> </div> </body> 単一または複数の要素を抽出してデバッグすることもできます。 単一/複数要素のデバッグ // 単一要素 screen.debug(screen.getByText("これはレンダリングテストのサンプルです。")) // 複数要素 screen.debug(screen.getAllByText("これはレンダリングテストのサンプルです。")) これでRTLから見えるHTML構造が確認できたので、これを元にテストを書いていく準備ができました。 次に、検証したい要素を取得する必要があります。 ② 検証対象の要素を取得する レンダリングした後は、RTLに用意された検索関数を使って、要素を取得していきます。 要素の選択をする際は、screen.[query]で取得できます。 要素取得の一例 screen.getByRole("heading"); クエリのタイプは以下の通りです。これらの違いは、要素が見つからない場合にエラーをスローするか、Promiseを再試行するかで、状況に応じて適切なクエリを使って要素を取得する必要があります。 単一要素を取得するクエリ 公式ドキュメントにもまとめられてますが、一つずつ見ていきましょう。 getBy クエリに一致するノードを返します。 一致する要素がない場合、または複数の一致が検出された場合にもエラーを投げます。 要素を取得する際の最も基本的なクエリーになるので、まずはgetByを使うことを検討します。 queryBy クエリに一致するノードを返し、一致する要素がない場合、nullを返します。 複数の一致が検出された場合はエラーを投げます。 要素がないことをテストしたい時に役に立ちます。 findBy 指定されたクエリに一致する要素が見つかったときに、resolveを返します。 デフォルトのタイムアウト後に、要素が見つからない場合、または複数の要素が見つかった場合、rejectを返します。 ボタンを押下後、DOMの変更を待って要素を取得したい等の場合に利用します。 ※単一要素を取得するクエリの注意点 これらはクエリーに一致する要素が複数見つかった場合はエラーを投げるので、その場合は後述する「複数要素を取得する際のクエリ」を使う必要があります。 複数要素を取得するクエリ getAllBy クエリに一致するすべてのノードの配列を返します。 一致する要素がない場合はエラーを投げます。 queryAllBy クエリに一致するすべてのノードの配列を返します。 一致する要素がない場合は、空配列を返します。 findAllBy 指定されたクエリに一致する要素が見つかった場合に、resolveを返します。 デフォルトのタイムアウト後に要素が見つからなかった場合、rejectを返します。 実際に要素を取得してみる 上記のクエリーを使って実際に要素を取得してみます。 以下に単純なコンポーネントがあります。 SampleComponent import React from "react"; export const Render: React.FC = () => { return ( <div> <h2>RenderTestSample</h2> <div> <img src={`${process.env.PUBLIC_URL}/logo192.png`} alt="ReactLogo" /> </div> <div> これはレンダリングテストのサンプルです。<span>forStudy</span> </div> <div> <label htmlFor="count">Count: </label> <input id="count" type="text" placeholder="Enter" value="defaultValue" /> </div> <button type="button" name="increment"> Increment </button> <button type="button" name="decrement"> Decrement </button> <button type="button" name="reset"> Reset </button> </div> ); }; getByRole 以下のリンクで取得できる要素を確認することができます。 使用例 expect(screen.getByRole("heading")).toBeTruthy(); 上記でheadingタグを抽出することはできますが、例えば、h1とh2がコンポーネントの中に存在する場合はどうするでしょうか? この場合、第二引数に以下のoptionsを渡すことで、特定要素を抽出することができます。 以下に例を示します。 options?: { exact?: boolean = true, hidden?: boolean = false, name?: TextMatch, normalizer?: NormalizerFn, selected?: boolean, checked?: boolean, pressed?: boolean, current?: boolean | string, expanded?: boolean, queryFallbacks?: boolean, level?: number, } optionsを指定して要素をフィルタリングする方法例 // heading要素を「level」でフィルタリングする expect(screen.getByRole("heading", { level: 1 })).toBeTruthy(); // h1 expect(screen.getByRole("heading", { level: 2 })).toBeTruthy(); // h2 // button要素を「name」でフィルタリングする expect(screen.getByRole("button", { name: /increment/i })).toBeTruthy(); // name属性が「increment」 expect(screen.getByRole("button", { name: /decrement/i })).toBeTruthy(); // name属性が「decrement」 getByLabelText フォームフィールドを抽出する際に適したメソッドです。 例えばコンポーネントの中に以下の要素があった場合 SampleComponent <label htmlFor="count">Count</label> <input id="count" type="text" placeholder="Enter" /> getByLabelTextを使用すると以下の結果が得られます。 testファイル debug(screen.getByLabelText("Count")); getByPlaceholderText フォームフィールドを取得したいけど、labelが定義されていないというケースもあると思います。 その際はgetByPlaceholderTextを使って取得しましょう。これでも、consoleには上記と同じ結果が出力されます。 testファイル debug(screen.getByPlaceholderText("Enter")); getByText divやspan、pタグの要素を見つける際に役立ちます。 コンポーネントの中に以下の要素があった場合、、、 SampleComponent <div> これはレンダリングテストのサンプルです。<span>forStudy</span> </div> getByTextを使用すると以下の結果が得られます。 親要素のテキストで抽出すると子要素も取得されるので、子要素のみ取得したい時は、対象のテキストを引数に渡しましょう。 testファイル debug(screen.getByText("これはレンダリングテストのサンプルです。")); debug(screen.getByText("forStudy")); getByDisplayValue フォーム要素に入力されてる値から要素を取得するメソッドです。 値の入力をテストする際に役立ちます。 SampleComponent <input id="count" type="text" placeholder="Enter" value="defaultValue" /> testファイル debug(screen.getByDisplayValue("defaultValue")); getByAltText 主にimgタグ、その他inputタグ、areaタグを取得する際に使用します。 SampleComponent <div> <img src={`${process.env.PUBLIC_URL}/logo192.png`} alt="ReactLogo" /> </div> testファイル debug(screen.getByAltText("ReactLogo")); ③ 取得した要素を検証する レンダリング結果をテストしているだけなので、非常にシンプルなテストですが、以下がテストコードのサンプルです。 TestSample/Render.test.js import { render, screen } from "@testing-library/react"; import { Render } from "../../pages/TestSample/components/Render"; describe("RenderingTest", () => { test("正しく表示されてること", () => { const { debug } = render(<Render />); debug(); // デバッグ expect(screen.getByRole("heading")).toBeTruthy(); expect(screen.getByRole("textbox")).toBeTruthy(); expect(screen.getByRole("button", { name: /increment/i })).toBeTruthy(); expect(screen.getAllByRole("button")[0]).toBeTruthy(); expect(screen.getAllByRole("button")[1]).toBeTruthy(); expect(screen.getByLabelText("Count:")).toBeTruthy(); expect(screen.getByPlaceholderText("Enter")).toBeTruthy(); expect( screen.getByText("これはレンダリングテストのサンプルです。") ).toBeTruthy(); expect(screen.getByText("forStudy")).toBeTruthy(); expect(screen.getByDisplayValue("defaultValue")).toBeTruthy(); expect(screen.queryByText("hoge")).toBeNull(); // 要素がないことを検証 }); }); テストを走らせてみましょう。 $ npm run test コンソールに以下のようにテスト結果が表示されます。 おわりに 以上、Unitテストを書く際の流れでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Micro Frontendsというフロントエンドマイクロサービス化をポートフォリオに導入してみた

今回ポートフォリオの作成で、Micro Frontendsという技術を採用したので、その概要や方法についてまとめていきたいと思います。 Micro Frontendsとは Micro Frontendsとは、バックエンドのMicro Servicesの考え方をフロントエンドにも取り入れた考え方です。 複雑で規模の大きいアプリケーションでは、バックエンド側だけではなくフロントエンド側もモノリスで複雑性が増して管理がしにくくなるといった問題がありますが、Micro Frontendsの考え方はその問題を解決する手段の一つです。 Micro Frontendsのメリット Micro Frontendsのメリットとしては、各フロントエンドのサービスごとに技術の選定が行え、疎結合なので耐障害性に強く、特定のミッションを達成するためのタスクの明確性をはかれるなどの恩恵を受けることができます。 Micro Frontendのデメリット Micro Frontendsのデメリットとしては、Gitのリポジトリーが増えたり、技術スタックがチームごとに異なっていたり、日本語文献が少なかったり、CSSの影響範囲の管理をしなければいけなかったりなどのことが挙げられます。 そのため、ある程度規模があるサービスでフロントエンドが複雑な状況であったり、チームごとにフロントエンドの技術スタックを分けたいという考えがあったり、サービスをまたいで共通したフロントエンドのコンポーネントを使いまわしたいという要望があったり、などの場合に採用の検討がされるのではないかと思います。 Micro Frontendsの実現方法 Micro Frontendsを提唱した、ThoughtWorks社の記事では、実現方法として以下の5つのパターンがあります。 「Server-side template composition」 「Build-time integration」 「Run-time integration via iframes」 「Run-time integration via JavaScript」 「Run-time integration via Web Components」 今回は表示を読み込むコンテナ側と、表示を提供するプロダクト側の依存が低く、導入がしやすいという観点から、Run-time integration via JavaScriptを採用しました。 この方式では各プロダクトがSPAで動作するように作られている前提で、各プロダクトを Micro Frontends のコンテナ側で読み込み、各プロダクトをパスの変更のみで提供するという動作するといった方法になります。 ポートフォリオの構成 ポートフォリオは世界のコロナ感染者と、日本の医療逼迫状況をグラフで表示するアプリケーションとなっており、React(Function Component)とTypeScriptで、Micro Frontendsを実現しています。 Micro Frontendsの表示を読み込むコンテナ側で、表示を作成するWorldコンポーネントとJapanコンポーネントはプロダクト側となっています。 (ちなみにせっかくマイクロフロントエンドにしてるんで、Kubernetesクラスターでフロントエンドとバックエンドの各マイクロサービスをコンテナ管理しったり、バックエンドはGoで書いたり、ArgoCDを取り入れたりしてます) プロダクト側の設定 表示を作成するコンポーネントであるプロダクト側の設定を行います。 パッケージインストール まず必要なパッケージをインストールします。 プロダクト側では、カレントパスの取得と設定を行うhistory、ルーティングを行うreact-router-dom、webpack環境のejectをさせないためのreact-app-rewiredが必要となります。 # yarn add react-app-rewired history react-router-dom 描画とアンマウントの設定 外部からコンポーネントの描画とアンマウントを行えるように、windowsオブジェクトの設定をindex.tsxで定義します。 実際にWorldコンポーネントで記載しているコードは以下のようになります。 index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import * as H from 'history'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { Provider } from 'react-redux'; import { store } from './app/store'; declare global { interface Window { renderWorld: (containerId: string, history: H.History) => void; unmountWorld: (containerId: string) => void; } } window.renderWorld = (containerId, history) => { ReactDOM.render( <React.StrictMode> <Provider store={store}> <App history={history} /> </Provider> </React.StrictMode>, document.getElementById(containerId) ); serviceWorker.unregister(); }; window.unmountWorld = (containerId) => { ReactDOM.unmountComponentAtNode( document.getElementById(containerId) as HTMLElement ); }; プロダクト側のコンポーネントを描画するために、renderWorldというwindowsオブジェクトのメソッドを新たに追加しています。 描画する場所については、引数としてわたってきたcontaineridのid名のエレメントに展開されます。 それとは反対にアンマウントする際は、umountWorldというwindowsオブジェクトのメソッドを追記し、id名のエレメントを削除します。 Webpackの設定 Run-time integration via JavaScriptでの読み込むJavaScriptファイルは、Sigle Chunkでビルド後に一つのJavaScriptファイルにまとめる必要があります。 そのためプロダクト側のWebpackのオーバーライドの設定ファイルを作成します。 config-override.js module.exports = {​​​​​​​​​ webpack: (config, env) => {​​​​​​​​​ config.optimization.runtimeChunk = false; config.optimization.splitChunks = {​​​​​​​​​ cacheGroups: {​​​​​​​​​ default: false, }​​​​​​​​​, }​​​​​​​​​; config.output.filename = "static/js/[name].js"; config.plugins[5].options.filename = "static/css/[name].css"; config.plugins[5].options.moduleFilename = () => "static/css/main.css"; return config; }​​​​​​​​​, }​​​​​​​​​; ビルドとサーバー起動時の設定 ビルドとサーバー起動時について、react-app-rewiredで行うようにする設定を、package.jsonに記載します。(create-react-appではreact-scriptsが採用されているが、JavaScriptが複数ファイルに分けられているので、react-app-rewiredでのSigle Chunkの設定が必要)。 package.json (snip) "scripts": {​​​​​​​​​ "start": "PORT=3001 react-app-rewired start", "build": "react-app-rewired build", }​​​​​​​​​ (snip) 以上の設定でサーバーを3001番ポートで起動して、asset-manifest.jsonにアクセスしてみると、Webpackでオーバーライドしているので以下のような表示になると思います。 asset-manifest.json (snip) "entrypoints": [ "static/css/main.css", "static/js/main.js" ] (snip) 上記のパスをコンテナ側で読み込みます。 CORSの有効設定 異なるオリジン間でのアクセスを必要とするため、Access-Control-Allow-Originの許可設定を行います。(制限が必要な場合は適宜書き換える) settProxy.js module.exports = (app) => { app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); next(); }); } コンテナ側の設定 プロダクト側のファイルを読み込み、コンテナ側でコンポーネントの表示を行う設定を行います。 パッケージインストール まず必要なパッケージをインストールします。 コンテナ側では、カレントパスの取得と設定を行うhistory、ルーティングを行うreact-router-domが必要となります。 # yarn add history react-router-dom プロダクト側のコンポーネントの読込設定 プロダクト側のコンポーネントを読み込むために、以下のようなMicroFrontend.tsxを作成します。 MicroFrontend.tsx import React, {​​​​​​​​​ useEffect }​​​​​​​​​ from 'react'; import * as H from 'history'; import axios from 'axios'; type MicroFrontendType = {​​​​​​​​​ name: string; host: string; history: H.History; }​​​​​​​​​; declare global {​​​​​​​​​ interface Window {​​​​​​​​​ renderWorld: (containerId: string, history: H.History) => void; unmountWorld: (containerId: string) => void; renderJapan: (containerId: string, history: H.History) => void; unmountJapan: (containerId: string) => void; [key: string]: (containerId: string, history?: H.History) => void; }​​​​​​​​​ }​​​​​​​​​ const MicroFrontend: {​​​​​​​​​ ({​​​​​​​​​ name, host, history }​​​​​​​​​: MicroFrontendType): JSX.Element; }​​​​​​​​​ = ({​​​​​​​​​ name, host, history }​​​​​​​​​: MicroFrontendType) => {​​​​​​​​​ useEffect(() => {​​​​​​​​​ const scriptId = `micro-frontend-script-${​​​​​​​​​name}​​​​​​​​​`; const renderMicroFrontend = () => {​​​​​​​​​ if (window[`render${​​​​​​​​​name}​​​​​​​​​`]) {​​​​​​​​​ window[`render${​​​​​​​​​name}​​​​​​​​​`](`${​​​​​​​​​name}​​​​​​​​​-container`, history); }​​​​​​​​​ }​​​​​​​​​; if (document.getElementById(scriptId)) {​​​​​​​​​ renderMicroFrontend(); return undefined; }​​​​​​​​​ (async () => {​​​​​​​​​ const {​​​​​​​​​ data }​​​​​​​​​ = await axios.get(`${​​​​​​​​​host}​​​​​​​​​/asset-manifest.json`); const script = document.createElement('script'); script.id = scriptId; script.crossOrigin = ''; script.src = `${​​​​​​​​​host}​​​​​​​​​${​​​​​​​​​data.files['main.js']}​​​​​​​​​`; script.onload = () => {​​​​​​​​​ renderMicroFrontend(); }​​​​​​​​​; document.head.appendChild(script); const link = document.createElement('link'); link.rel = 'stylesheet'; link.crossOrigin = ''; link.type = 'text/css'; link.href = `${​​​​​​​​​host}​​​​​​​​​${​​​​​​​​​data.files['main.css']}​​​​​​​​​`; link.onload = () => {​​​​​​​​​ renderMicroFrontend(); }​​​​​​​​​; document.head.appendChild(link); }​​​​​​​​​)(); return () => {​​​​​​​​​ if (window[`unmount${​​​​​​​​​name}​​​​​​​​​`]) {​​​​​​​​​ window[`unmount${​​​​​​​​​name}​​​​​​​​​`](`${​​​​​​​​​name}​​​​​​​​​-container`); }​​​​​​​​​ }​​​​​​​​​; }​​​​​​​​​); return <main id={​​​​​​​​​`${​​​​​​​​​name}​​​​​​​​​-container`}​​​​​​​​​ />; }​​​​​​​​​; export default MicroFrontend; 以上の設定では、rendeMicrofrontendでプロダクト側のasset-manifest.jsonで出力した情報を読み込んでコンポーネントを描画をしています。 パスを切り替えた場合などは、id名が設定されている既存のコンポーネントはアンマウントされ、新しいコンポーネントが描画されます。 そしてこのMicroFrontendを呼びだす、コンポーネントの設定を行います。 World.tsx import React from 'react'; import {​​​​​​​​​ HistoryType }​​​​​​​​​ from '../../../App'; import MicroFrontend from '../MicroFrontend'; const World = ({​​​​​​​​​ history }​​​​​​​​​: HistoryType): JSX.Element => {​​​​​​​​​ return ( <> <MicroFrontend history={​​​​​​​​​history}​​​​​​​​​ host="localhost:3001" name="World" />; </> ); }​​​​​​​​​; export default World; さらに該当するパスにアクセスした際のルーティングの設定を、App.tsxで定義します。 BrowserRouterは、historyAPIを外部から渡すことができるパッケージになります。 App.tsx import React from 'react'; import * as H from 'history'; import { BrowserRouter, Switch, Route } from 'react-router-dom'; import Japan from './features/covid/Japan/Japan'; import World from './features/covid/World/World'; import NotFound from './features/covid/NotFound/NotFound'; export type HistoryType = { history: H.History; }; const App: React.FC = () => { return ( <BrowserRouter> <Switch> <Route exact path="/world" component={World} /> <Route exact path="/japan" component={Japan} /> <Route component={NotFound} /> </Switch> </BrowserRouter> ); }; export default App; 以上の設定で、Run-time integration via JavaScript方式のMicro Frontendsの設定は完了です。 history APIの根本的な理解が必要だったり、日本語文献がなかったり、React+TypeScriptで実装されている例を見つけられなかったりで苦労し、現状複雑になってしまっていますが今後ブラッシュアップしていきたいと思います。 参考URL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自動車の品質管理からエンジニアになってみて

はじめに 未経験からエンジニアに転職して、現在はフリーランスエンジニアとして活動しています。 エンジニアになった経緯などを書きますので、エンジニア転職を検討している方の参考になれば幸いです! ⬇️僕の経歴です 年月 所属 2018/04~2020/04 自動車会社(新卒入社) 2019/05~2019/10 Progate, Railsチュートリアルで独学 2019/11~2020/02 ポテパンキャンプ 2020/04~2021/10 受託会社で正社員 2021/11~ 自社開発会社でフリーランス 1. 前職の内容 前職の自動車メーカーでは、海外の自動車のリコールなどを扱う品質管理の部門にいました。 品質管理といっても実際に、製品を扱ったり、図面を書いたりするわけではなく、 海外のお客様・研究所・部品メーカーの間に入って、スケジュールの調整を行う中間部門でした。 具体的には下記のような仕事の流れでした。 お客様から訴えがある ➡️ 過去に同じ問題がないかをデータで調べる ➡️ 同じ問題が多発していれば、研究所・工場や部品メーカーに原因追及・部品の修正を依頼 ➡️ 問題が深刻な場合、リコールを実施する 前職を通して... マイナスをゼロに戻すのが仕事だったので、いろいろ大変でしたが、 各地の工場や部品メーカー、海外出張に行けたのは良い経験だったと思います。 新卒一年目から、研究所や部品メーカーのお偉いさんたちがいる会議を進行していたので、 社会人としての基本的なマナーや年上の方を相手にする度胸はつきましたが、 今後活かせるスキルはあまり身についていない感じがしました。 また、10年以上先輩の社員の方も同じ仕事内容だったため、 このまま仕事を続けていても、仕事の幅は広がりにくいのも薄々感じていました。 2. エンジニアになろうと思ったきっかけ 憧れだった会社に入社できたので、一生懸命仕事をしていましたが 入社してから1年くらい経つと、下記を感じるようになってきました。 基本は年功序列なので、仕事を頑張っても評価されにくい 希望の部署に行きにくい 勤務地、仕事内容、給料が会社に決定される このまま仕事を続けていても問題はないかもしれないですが、 「この先、何十年同じような仕事を続けていいのか?」と疑問を抱くようになりました。 エンジニアに興味を持つ それから、副業や転職についていろいろ調べていると、 エンジニアの情報をよく見かけ、エンジニアという職業に興味を持つようになりました。 そして、知り合いからフリーランンスとして活動しているエンジニアの方を紹介して頂いて、 実際に話を聞く機会を頂きました。 話を聞いてみると、 自分のスキルが評価される フリーランスでも案件は多い 会社に依存せず仕事ができる ということがわかったので、エンジニアになることを決意しました。 そしてMacbookを購入し、ProgateやRailsチュートリアルでプログラミングを勉強しました。 ポテパンキャンプを受講 ある程度独学で継続できたので、本格的にエンジニア転職を視野に入れ、プログラミングスクールを検討しました。 プログラミングスクールは数多くありますが、 オンラインで受講できる(地方在住だったため) 現役エンジニアからのレビューを受けられる 転職サービスがついている という特徴からポテパンキャンプの受講を決意しました。 そしてポテパンキャンプのカリキュラムも終えて、無事に受託会社に転職が決定しました! 3. エンジニアとして働いてみた感想(前職と比べてどう変わったか) 転職先での経験 転職後は1~2ヶ月の研修をしてから案件に配属になりました。 受託会社だったため、複数の案件に関わり、下記の経験を積むことができました。 Railsを用いたAPIサーバーの開発 React, Typescriptを用いたフロントの開発 AWSを用いたインフラ構築 SwiftでのiOSアプリ開発 エンジニアになってみて... そして、ここがみなさんが一番気になるところだと思いますが、 僕は未経験からエンジニアに転職して本当に良かったと感じています!! エンジニア(特にフリーランス)になって 前職と異なり、会社に依存しないスキルが身についた 勤務地、仕事内容、給料を自分で決められるようになった Webアプリを作れるようになった 上記のように大きく変わりました。 特に、会社に依存しないスキルが身についたのは本当に良かったと思っています。 スキルがあればキャリアや生活環境を自分で決めることができるので、幸福度は上がりました。 4. 今後はどんなエンジニアを目指したいか 今後は下記のようなエンジニアになりたいなと思っています! スキル面 インフラ側もより詳しくなりたい(アプリ全体を把握したい) Go言語で開発したい サーバーレス開発をしたい キャリア面 より上流工程から関われるようになりたい エンジニアになりたい方を応援、サポートしたい 最後に 2年間挑戦し続けた結果、目指していた働き方・ライフスタイルを手に入れることができました。 キャリアチェンジを通して、確かに辛い時期はありましたが、エンジニアになって本当に良かったと思います。 エンジニア転職に悩んでいる方は、挑戦してみる価値はあると思うので、ぜひ挑戦してみてほしいです! そしてプログラミングスクールを検討している方は、ぜひポテパンキャンプを検討してみてください! 代表の宮崎さんが相談に乗ってくれます! この記事を通して、少しでも挑戦するきっかけになれば幸いです。 最後まで読んで頂きありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET6 ASP.NET + React + TypeScriptでリアルタイムチャットアプリを作る

新しい技術を勉強するに当たって、ToDoリストなどでは物足りない気がしたので、プッシュ通知などを実装したSlackのようなチャットシステムにグループウェアのようなスケジュール管理の機能を加えたWebアプリを作ってみようと思い、現在作成しています。 想定していたよりもだいぶ難しく、完成には程遠く、挫折しかけていますので、何か得るものがあればと思い、汚いソースで恥ずかしいながらもソースを公開しました。 ドメイン駆動 + クリーンアーキテクチャを勉強しながら作成していますが、理解できていない部分が多々ある気がしています。 コードレビュー大歓迎です。アンチパターンなどがあればご指摘いただけると嬉しいです。 勉強方法や、現時点での苦労した点や工夫した点などをまとめたいと思います。 言語、環境 バックエンド(API)は.NET6 ASP.NET(C#)、フロントエンド(UI)はReact + TypeScriptで作成しています。 開発環境はdockerのコンテナをVSCodeで開いて実装しています。 主な使用ライブラリ 以下のライブラリを使用しています。 バックエンド MediatR, WebPush, signalR, Dapper, Identity, FluentValidation フロントエンド react-router-dom, recoil, fluentui/react, fp-ts, tsyringe, react-hook-form 勉強方法、工夫した点 バックエンド Githubに上がっているいろんなプロジェクトのソースを参考に試行錯誤しながら作成しました。 特に以下のプロジェクトが大変参考になりました。 https://github.com/EddieAbbondanzio/Updog.in https://github.com/jasontaylordev/CleanArchitecture プロジェクトは以下の構成にしました。 Api スタートアッププロジェクト フロントエンドからのリクエスト、レスポンスを制御 MediatRを使用して、Application層に処理をさばく Application FluentValidationを使用して、Api層からのリクエストの検証を行う Domain層のロジックを使用したユースケースを実装 Domain ドメイン、エンティティを実装 DB関連の抽象クラスを定義して、Application層で使用する Infrastructure エンティティの永続化(DB保存)を実装(Domain層のDB関連の抽象クラスを実装) DBからレコードを読み込み、エンティティを組み立てる レコード→エンティティ、エンティティ→レコードの変換処理を実装 登録・更新・削除(Command)はORマッパーを使ってSQL文は自動生成する(EntityFrameworkを勉強する気力がなかったので、独自にORマッパーを作成しました) 検索(Query)はテーブルの結合や複雑な検索条件などがあり、SQLの自動生成は難しいためSQLはStringBuilderなどで自力で作成する signalRを使用して、双方向通信を行い、チャットの内容がリアルタイムに相手に通知されるようにしています。 相手が画面を開いていない場合は通知ができないので、WebPushを使用してプッシュ通知をします。(そのように実装する予定です) 上記1つ目のプロジェクトを参考に、Application層の戻り値はEitherモナドを使用しています。 モナドについてはいまいち理解が曖昧ですが、Eitherモナドを使用することにより、ソースがかなりスッキリした気がします。 Domain層にDB関連の抽象クラスのみ定義しておき、実装はInfrastructure層で行うのがクリーンアーキテクチャのポイントのようです。 プロジェクトの依存関係が以下から、 Application → Domain → Infrastructue 以下に変更されます。 Application → Domain ← Infrastructue Application層やDomain層のテストをする際に、DB関連の実装クラスをテスト用のクラスに差し替えるだけで、DB関連の処理をテスト用の処理に書き換えることができるようになります。 フロントエンド フロントエンドのプロジェクトはCreate React Appで作成しました。 テンプレートはcra-template-pwa-typescriptを使用しました。 eslintはairbnbの設定を使いたかったので、cracoというライブラリを使用して、設定内容を変更しています。 CSSフレームワーク(コンポーネント)はfluentui/reactを使用しています。 フォームの制御はreact-hook-formを使用しています。 fluentui/reactとreact-hook-formの併用が難しかったですが、ネットで調べながらなんとか実装できました。 バックエンドで使用しているDependencyInjection(DI)の機能がとても便利でしたので、フロントエンドでもtsyringeというライブラリを使用して、DIを導入しましたが、フロントエンドについてはDIは必要なかったかなという印象です。(ソースが冗長になった印象のほうが強い) これまたバックエンドで使用しているEitherモナドが便利だと感じましたので、fp-tsというライブラリを導入してフロントエンドでもEitherモナドを使えるようにしました。 フロントエンドではAPIの失敗は例外としてエラー画面を表示してしまうのではなく、例えば「IDが未入力です。」などのメッセージを表示する必要があります。(APIでエラーが帰ってきてもエラーパターンとして処理を継続する必要がある) APIの結果が成功か失敗かで処理を分ける必要があるので、Eitherモナドは役立っていると感じています。 recoilを使用して状態管理を行っていますが、signalRでリアルタイムに通知されるチャットのデータをうまく処理する方法はないか、検討中です。(現状、スパゲッティになっている) reactのコンポーネントはすぐに肥大化してしまうので、フォルダ構成、ファイル名の命名規則などのルールぎめが必要だと感じています。(一般的なベストプラクティスがあればよいのですが。。) まとめ 新しい技術を勉強するに当たって、GitHubのソースやいろいろなネット上の記事に助けられました。 少しでも本記事やソースが参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分的React環境構築メモ

React 環境構築 node.jsのインストール $ brew install anyenv $ echo 'eval "$(anyenv init -)"' >> ~/.bash_profile $ exec $SHELL -l $ anyenv install nodenv $ exec $SHELL -l $ mkdir -p $(anyenv root)/plugins $ git clone https://github.com/znz/anyenv-update.git $(anyenv root)/plugins/anyenv-update $ mkdir -p "$(nodenv root)"/plugins $ git clone https://github.com/nodenv/nodenv-default-packages.git "$(nodenv root)/plugins/nodenv-default-packages" $ touch $(nodenv root)/default-packages default-packagesの中身 yarn typescript ts-node typesync $ nodenv install -l $ nodenv install 14.4.0 # 最新バージョン $ nodenv global 14.4.0 # 最新バージョン Create React App $ npx create-react-app hello-world --template typescript $ cd hello-world $ yarn start ESLint導入 $ npm ls eslint # ESLintのバージョン確認 $ yarn upgrade-interactive --latest "@types/node": "14系", "@types/react": "16.9系", "@types/react-dom": "16.9系", Enter $ yarn eslint --init ? How would you like to use ESLint? ❯To check syntax, find problems, and enforce code style ? What type of modules does your project use? JavaScript modules (import/export) ❯JavaScript modules (import/export) ? Which framework does your project use? ❯ React ? Does your project use TypeScript? ❯ Yes ? Where does your code run? ❯ Browser ? How would you like to define a style for your project? ❯Use a popular style guide ? Which style guide do you want to follow? ❯Airbnb: https://github.com/airbnb/javascript ? What format do you want your config file to be in? ❯ JavaScript eslint-plugin-react@^7.20.0 \ @typescript-eslint/eslint-plugin@latest \ eslint-config-airbnb@latest \ eslint@^5.16.0 || ^6.8.0 || ^7.2.0 \ eslint-plugin-import@^2.21.2 eslint-plugin-jsx-a11y@^6.3.0 \ eslint-plugin-react-hooks@^4 || ^3 || ^2.3.0 || ^1.7.0 \ @typescript-eslint/parser@latest ? Would you like to install them now with npm? ❯No ここでエラーが出ても良い 拡張ルールセットとプラグインのインストール $ yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin \ eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import \ eslint-plugin-jsx-a11y eslint-config-airbnb $ typesync # package.jsonを見て足りない型定義パッケージがあれば追加 $ yarn .eslintrc.jsの設定 module.exports = { env: { browser: true, es2020: true, }, extends: [ 'plugin:react/recommended', 'airbnb', 'airbnb/hooks', 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 2020, project: './tsconfig.eslint.json', sourceType: 'module', tsconfigRootDir: __dirname, }, plugins: [ '@typescript-eslint', 'import', 'jsx-a11y', 'react', 'react-hooks', ], root: true, rules: { 'lines-between-class-members': [ 'error', 'always', { exceptAfterSingleLine: true, }, ], // should be rewritten as `['error', { allowAsStatement: true }]` in ESLint 7 or later // SEE: https://github.com/typescript-eslint/typescript-eslint/issues/1184 'no-void': 'off', 'padding-line-between-statements': [ 'error', { blankLine: 'always', prev: '*', next: 'return', }, ], '@typescript-eslint/no-unused-vars': [ 'error', { vars: 'all', args: 'after-used', argsIgnorePattern: '_', ignoreRestSiblings: false, varsIgnorePattern: '_', }, ], 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], 'react/jsx-filename-extension': [ 'error', { extensions: ['.jsx', '.tsx'], }, ], 'react/jsx-props-no-spreading': [ 'error', { html: 'enforce', custom: 'enforce', explicitSpread: 'ignore', }, ], // note you must disable the base rule as it can report incorrect errors // https://stackoverflow.com/questions/63818415/react-was-used-before-it-was-defined#answer-64024916 "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": ["error"], }, overrides: [ { files: ['*.tsx'], rules: { 'react/prop-types': 'off', }, }, ], settings: { 'import/resolver': { node: { paths: ['src'], }, }, }, }; .eslintignoreの設定 build/ public/ **/coverage/ **/node_modules/ **/*.min.js *.config.js .eslintrc.js # https://wonwon-eater.com/ts-eslint-import-error/ tsconfig.eslint.jsonの設定 { "extends": "./tsconfig.json", "include": [ "src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx" ], "exclude": [ "node_modules" ] } package.jsonに追記 "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'", "postinstall": "typesync" }, Prettierの導入 $ yarn add -D prettier eslint-plugin-prettier eslint-config-prettier $ yarn .eslintrc.jsに追記 extends: [ 'plugin:react/recommended', 'airbnb', 'airbnb/hooks', 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:prettier/recommended', 'prettier', 'prettier/@typescript-eslint', 'prettier/react', 'prettier/standard', ], plugins: [ '@typescript-eslint', 'import', 'jsx-a11y', 'prettier', 'react', 'react-hooks', ], .prettierrcの設定 { "bracketSpacing": true, "printWidth": 80, "semi": true, "singleQuote": true, "trailingComma": 'all', "useTabs": false, } $ npx eslint-config-prettier .eslintrc.js stylelintの導入 $ yarn add -D stylelint stylelint-config-standard stylelint-order stylelint-config-recess-order .stylelintrc.jsの設定 module.exports = { extends: [ 'stylelint-config-standard', 'stylelint-config-recess-order', ], plugins: [ 'stylelint-order', ], ignoreFiles: [ '**/node_modules/**', ], rules: { 'string-quotes': 'single', }, }; VSCode Code > Preferences > Settings >『Open Settings (JSON)』 settings.json { "css.validate": false, "less.validate": false, "scss.validate": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.fixAll.stylelint": true, }, "editor.formatOnSave": false, "eslint.enable": true, "eslint.packageManager": "yarn", } package.jsonの設定 "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "npm run lint:es && npm run lint:style", "lint:fix": "npm run lint:es:fix && npm run lint:style:fix", "lint:es": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint:es:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'", "lint:style": "stylelint 'src/**/*.css'", "lint:style:fix": "stylelint --fix 'src/**/*.css'", "lint:conflict": "npx eslint-config-prettier .eslintrc.js", "postinstall": "typesync" }, その他の設定 $ yarn -D add eslint-plugin-prefer-arrow plugins: [ '@typescript-eslint', 'import', 'jsx-a11y', 'prefer-arrow', 'prettier', 'react', 'react-hooks', ], ... rules: { ... 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], 'prefer-arrow/prefer-arrow-functions': [ 'error', { disallowPrototype: true, singleReturnOnly: false, classPropertiesAllowed: false, }, ], 'react/jsx-filename-extension': [ 'error', { extensions: ['.jsx', '.tsx'], }, ], ... } $ yarn add -D husky lint-staged package.jsonの設定 " devDependencies": { ... }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx}": [ "eslint --fix" ], "src/**/*.css": [ "stylelint --fix" ] } ChromeにReact Developer Toolsを追加 local:3000で有効にする↓ https://qiita.com/obr-note/items/395a842343bc06c1efb6 ライブラリ Semantic UI React 導入 $ yarn add semantic-ui-react semantic-ui-css src/index.tsxに追記 import 'semantic-ui-css/semantic.min.css' React Hook Form 導入 $ yarn add react-hook-form React Router(5系) 導入 $ yarn add react-router react-router-dom $ yarn src/index.tsxを設定 追記 import { BrowserRouter } from 'react-router-dom'; 編集 ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root'), ); src/App.tsxにルーティングを設定 import { Redirect, Route, Switch } from 'react-router'; const App: React.FC = () => ( <Switch> <Route exact path="/" component={ホーム} /> <Route path="/item/:itemId" component={各データページ} /> <Redirect push to="/" /> </Switch> ); 移動/戻る/進む ボタン import { Link, useHistory } from 'react-router-dom'; const Item: FC = () => { const history = useHistory(); return ( <> // aタグ不可 <Link to="/">トップページへ</Link> // historyを使う <button type="button" onClick={() => history.goBack()}> 戻る </button> <button type="button" onClick={() => history.goForward()}> 進む </button> <button type="button" onClick={() => history.push('/')}> トップページへ </button> </> ); }; Redux Reduxの導入 $ yarn add redux react-redux $ yarn Redux Toolkitの導入 $ yarn add @reduxjs/toolkit $ yarn feature/counter.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export type CounterState = { count: number; }; const initialState: CounterState = { count: 0 }; export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { added: (state, action: PayloadAction<number>) => ({ ...state, count: state.count + action.payload, }), decremented: (state) => ({ ...state, count: state.count - 1 }), incremented: (state) => ({ ...state, count: state.count + 1 }), }, }); index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import 'semantic-ui-css/semantic.min.css'; import { BrowserRouter } from 'react-router-dom'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { counterSlice } from './features/counter'; const store = configureStore({ reducer: counterSlice.reducer }); ReactDOM.render( <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider>, document.getElementById('root'), ); reportWebVitals(); containers/pages/SomePage import React, { FC } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { counterSlice, CounterState } from 'features/counter'; import SomePage from 'components/pages/SomePage'; const EnhancedSomePage: FC = () => { const count = useSelector<CounterState, number>((state) => state.count); const dispatch = useDispatch(); const { added, decremented, incremented } = counterSlice.actions; return ( <SomePage count={count} add={(amount: number) => dispatch(added(amount))} decrement={() => dispatch(decremented())} increment={() => dispatch(incremented())} /> ); }; export default EnhancedSomePage; ChromeへRedux DevToolsを追加 ※createStoreに設定が必要。Redux ToolkitのconfigureStoreにはデフォルトで設定されている。 https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja Redux Toolkitにおけるreducerの書き方 ※Redux Toolkitにはデフォルトでimmerが適用されている https://immerjs.github.io/immer/docs/introduction import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { uuid } from 'uuidv4'; type Task = { id: string; title: string; deadline?: Date; createdAt: Date; }; export type TodoState = { todoList: { [id: string]: Task }; doneList: { [id: string]: Task }; }; export const todoSlice = createSlice({ name: 'todo', initialState: { todoList: {}, doneList: {} } as TodoState, reducers: { taskCreated: ( state, action: PayloadAction<Pick<Task, 'title' | 'deadline'>>, )=>{ const id = uuid(); const createdAt = new Date(); state.todoList[id] = { ...action.payload, id, createdAt }; }, taskDone: (state, action: PayloadAction<string>) => { const id = action.payload; const task = state.todoList[id]; if (task) { state.doneList[id] = { ...task }; delete state.todoList[id]; } }, taskUpdated: (state, action: PayloadAction<Omit<Task, 'createdAt'>>) => { const { id, ...data } = action.payload; const task = state.todoList[id]; if (task) state.todoList[id] = { ...task, ...data }; }, }, }); uuidv4 重複しない数値をランダムに作成 導入 $ yarn add uuidv4 Firebase https://firebase.google.com/?hl=ja 1.コンソールへ移動 2.プロジェクトを追加 3.ウェブアプリを追加 4.scriptをコピー(※重要 どこかに残しておく) ライブラリ追加 $ yarn add firebase App.tsx import firebase from 'firebase'; const App: React.FC = () => { const { env } = process; const firebaseConfig = { apiKey: env.REACT_APP_API_KEY, authDomain: env.REACT_APP_AUTH_DOMAIN, databaseURL: env.REACT_APP_DATABASE_URL, projectId: env.REACT_APP_PROJECT_ID, storageBucket: env.REACT_APP_STORAGE_BUCKET, messagingSenderId: env.REACT_APP_MESSAGING_SENDER_ID, appId: env.REACT_APP_APP_ID, measurementId: env.REACT_APP_MEASUREMENT_ID, }; firebase.initializeApp(firebaseConfig); return ... }; .env https://create-react-app.dev/docs/adding-custom-environment-variables/#adding-development-environment-variables-in-env REACT_APP_API_KEY=XXXXXXXXXX REACT_APP_AUTH_DOMAIN=XXXXXXXXXX REACT_APP_DATABASE_URL=XXXXXXXXXX REACT_APP_PROJECT_ID=XXXXXXXXXX REACT_APP_STORAGE_BUCKET=XXXXXXXXXX REACT_APP_MESSAGING_SENDER_ID=XXXXXXXXXX REACT_APP_APP_ID=XXXXXXXXXX REACT_APP_MEASUREMENT_ID=XXXXXXXXXX SWR エラー回避 Warning: findDOMNode is deprecated in StrictMode. src/index.tsx 修正前 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root'), ); 修正後 ReactDOM.render(<App />, document.getElementById('root')); Do not use Array index in keys react/no-array-index-key linterエラー。keyはオブジェクト固有のidに設定する。 修正前 {Array.map((item, index) => ( <Item key={index} text={item.value} /> ))} 修正後 {Array.map((item) => ( <Item key={item.id} text={item.value} /> ))} A form label must be associated with a control.eslint(jsx-a11y/label-has-associated-control) linterエラー。labelはinputと紐付ける。 修正前 <label>Last Name</label> <input placeholder="Last Name" 修正後 <label htmlFor="lastName"> Last Name <input placeholder="Last Name" id="lastName" /> </label> Type '() => void' is not assignable to type 'void' TypeScriptの型エラー。型を合わせる。 修正前 const Home: FC<{ addFunction: void }> = ({ addFunction }) => ( ... ); const EnhancedHome: FC = () => { const addFunction = () => { ... }; return ( <> // エラー発生箇所 <Home addFunction={addFunction} /> </> ); }; 修正後 const Home: FC<{ addFunction: () => void }> = ({ addFunction }) => ( ... ); 以下同 Type '() => void' is not assignable to type 'FC<{}>'. Type 'void' is not assignable to type 'ReactElement | null' TypeScriptの型エラー。型を合わせる。 修正前 const Home: FC<{ addFunction: FC }> = ({ addFunction }) => ( ... ); const EnhancedHome: FC = () => { const addFunction = () => { ... }; return ( <> // エラー発生箇所 <Home addFunction={addFunction} /> </> ); }; 修正後 const Home: FC<{ addFunction: () => void }> = ({ addFunction }) => ( ... ); 以下同 onClickでイベントが発生しないno-unused-expressions 関数の中に関数を書いてしまっている。関数を子のコンポーネントに継承したときは入れ子に注意。 修正前 const greet = () => console.log('Hello!!'); ... <button type="button" onClick={() => greet}> push </button> 修正後 const greet = () => console.log('Hello!!'); ... <button type="button" onClick={greet}> push </button> prefer-destructuring linterエラー。分割代入にする。 Type 'CustomType | undefined' is not assignable to type 'CustomType' Array.prototype.find()の処理後などundefindが混じる場合はif文で型ガードする if ( typeof TargetValue !== 'undefined' ){ // CustomTypeの場合の処理 } else { // undefindの場合の処理 } 【今回の例】 ルーティングで/itmes/:itemIdとしたとき、配列から:itemIdに一致した要素のみ取得する itemIdが一致する要素がない場合、undefindが返ってきている import React, { FC } from 'react'; import { useSelector } from 'react-redux'; import { useParams, Navigate } from 'react-router-dom'; import ItemDetails from '../../components/pages/ItemDetails'; import { TodoState, TodoItemState } from '../../reducer'; const EnhancedItemDetails: FC = () => { const { itemId } = useParams(); const content = useSelector<TodoState, TodoItemState[]>( (state) => state.content, ); const contentItem = content.find((element) => element.id === Number(itemId)); if (typeof contentItem !== 'undefined') { return <ItemDetails item={contentItem} />; } return <Navigate to="/" replace />; }; export default EnhancedItemDetails; Warning: Functions are not valid as a React child. JSXに関数を返してしまっているエラー。関数の場合は最後に()をつけないと実行されないので注意。 修正前 <div>{somefunction}</div> 修正後 <div>{somefunction()}</div> Firebase net::ERR_CONNECTION_TIMED_OUT 単純にWifiが繋がっていなかった(物理的にインターネットと繋がっていなかった)だけ 実装例 ファイル構成 src/ components/ molecules/ something.tsx organisms/ something.tsx pages/ something.tsx templates/ something.tsx containers/ molecules/ something.tsx organisms/ something.tsx pages/ something.tsx templates/ something.tsx data/ someData.ts features/ someSlice.ts hooks/ useSomething.ts App.tsx index.tsx テキストフォーム テキストフォームの入力をconsoleに出力する例。 const App: FC = () => { const [input, setInput] = useState<{ firstName: string; lastName: string }>({ firstName: '', lastName: '', }); const onChangeFunc = (event: React.ChangeEvent<HTMLInputElement>) => { let { firstName, lastName } = input; if (event.target.id === 'firstName') { firstName = event.target.value; } else if (event.target.id === 'lastName') { lastName = event.target.value; } setInput({ firstName, lastName }); }; const onSubmitFunc = () => { event.preventDefault(); console.log(input); }; return ( <Form onSubmit={onSubmitFunc}> <Form.Field> <label htmlFor="firstName"> First Name <input placeholder="First Name" id="firstName" onChange={onChangeFunc} /> </label> </Form.Field> <Form.Field> <label htmlFor="lastName"> Last Name <input placeholder="Last Name" id="lastName" onChange={onChangeFunc} /> </label> </Form.Field> <Button type="submit">Submit</Button> </Form> ) };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsby×HeadlessCMS(Contentful)でポートフォリオサイトを作った話

ポートフォリオサイト、今まであったものを作り直し、GatsbyとHeadlessCMS(Contentful)で作ってみました。 できたのがこちら!? https://www.qulo.ooo/ 作り直しの経緯とこれらの組み合わせについてまとめてみることにします✍️ ?ポートフォリオサイトの経緯 1代目 FC2ホームページ 2代目 html+CSS(この頃はプログラミングのプの字もわからず…海外のデザインサイトを検証→ソースコードみて必要そうなパーツをつなぎ合わせてヨシとしたキメラ) ?どうして2代目から作り直そうと思ったか? 更新性が以下の理由から悪かった… 表示用画像をディスプレイサイズの最適化用に複数枚用意する必要があった 記事を増やすときはhtmlを編集、記事ファイルがその度に増えていく CSS記述だったため関数が使えず全体的にちょっとカラー変えたいみたいなときに該当するカラーコードをすべて書き換えなければいけない html CSS以外の言語でかいてみたいなとおもった そのために更新頻度が無&モダンな技術を試してみたいこともあり作り直すことにしました。 ?Gatsbyのよいところ そもそもGatsbyとは? Facebook社(最近Metaへの社名変更が話題になってましたね〜)が開発しているReact製の静的サイトジェネレーター。JavaScriptライブラリ。 スターター(テンプレート)がいっぱいある Netlifyというホスティングサービスを使い、それをGitHubと連携させることで、masterブランチに更新内容をマージするだけでサイトを更新することができるので更新性が◎ マークダウン式で記事をかけるので、QiitaやMediumといった別サイトへそのまま持っていくことができる ?Headless CMS(Contentful)のよいところ Gatsbyだけでもマークダウン式で記事をかくことはできるが、記事ファイルをリポジトリ内で管理しないといけない… =html CSSで記述していた2代目ポートフォリオサイトのときに問題となっていた、記事が増える度にファイル数が増えていく問題の解決になっていない! ?Headless CMSの導入により記事管理を外部サービスにお任せすることができるように! (サイトを多言語化したいといった場合、ファイル数が単純に倍になる問題も解決◎) なぜContentful? Gatsbyとの組み合わせで使用されているユーザーが多くコミュニティが活発なため 将来的に対応したい多言語化対応にはContentfulがつよそうだったため マークダウン式でかけるので、MediumやQiitaなど外部サービスにそのままもっていくこともできる ?使ってみた感想 Gatsby × Contentful めちゃ使いやすい! けど自分用にもドキュメントを残しておくべし なぜドキュメントが必要? HeadlessCMS使用している箇所はソースコードからも読み解きにくい。 例えば更新性があるところだけ使っている人もいれば、About画面など更新性の低いページもすべてHeadlessCMSで管理している人もいた。 「あれっ、ここの文言修正したいけどソースコードにない!」ということが起こりうるので、HeadlessCMSの使用範囲は記録しておく必要があるなぁと思った React(JavaScriptライブラリ)により、html CSSだと手が届かなかった表現も可能になるGatsby。 スターターセットを活用することでお手軽にはじめることができるので、 個人サイト興味あるけどなにからはじめたらいいかわからない…という方にもすごくおすすめです! ?他に検討したこと フレームワークについて FlutterWebなども検討したがSEOの観点からやめておいた ✅スライド資料公開中 Speaker Deckにスライド資料投稿しております! よかったら合わせてご覧ください?‍♀️
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む