Programming Journal

学習したことの整理用です。

newt と Next.jsでHP作成してみる

概要

友達の設計事務所のHP作成をすることになりました。
コンテンツは非エンジニアでも更新できるようにCMSで作成しました。
NewtというヘッドレスCMSを初めて使ったのですが、Vercelでデプロイするまで、簡単に作れたので内容をまとめます

CMSの選定

  • WordPressは使いたくない
  • 無料で使える
  • 簡単に導入できる
  • ヘッドレスCMS(API部分とコンテンツの管理だけが提供される)
  • Next.jsを使いたい

以上から、Newtを選択することにしました!

設計 / Next.jsでフロント部分を実装する

HPでは、Home, About, Works, Contact, Newsのコンテンツがあり、それぞれAPIを叩いてデータを取得するようにしました。

データ取得部分はベタ書きで仮の値を入れて実装しました。
src以下の構成はこんな感じで簡単にしました。
NewsとWorksは一覧を表示するページと、個々の詳細を表示するページがあります。

src
├── components
│   └── WorkCard.tsx
├── layouts
│   └── DefaultLayout.tsx
├── pages
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── about
│   │   └── index.tsx
│   ├── contact
│   │   └── index.tsx
│   ├── index.tsx
│   ├── news
│   │   ├── [slug]
│   │   │   └── index.tsx
│   │   └── index.tsx
│   └── works
│       ├── [slug]
│       │   └── index.tsx
│       └── index.tsx
├── styles
│   └── globals.css
└── types
    └── type.ts

Newt側の設定

スペース作成

新規スペース作成

スペース

スペースとは、チームや個人に関わるあらゆるコンテンツを統合的に管理するためのものです。 スペースには、Appと呼ばれるコンテンツ管理のグループをいくつも作成することができ、ワークスペースのメンバーは必要に応じてこれらのAppに参加することでコンテンツ管理作業を行います。

App作成

App

Appとは、コンテンツやコンテンツの管理者を、その関連度の高さを元にひとまとまりにした「コンテンツ管理のユニット」です。

Appを構成するリソース

私はこのような構成にしました 各Appにそれぞれ、モデル(コンテンツ管理の骨組み。データ構造のこと。 (ex) タイトル、スラッグ、メタ情報・・・)、ビュー(管理画面のUI)が存在します

App名 App UID
works works
about about
contact contact
news news

モデルを追加する

モデルとは、コンテンツ管理の骨組みとなるものです。

モデルは、1つ以上のフィールドによって構成されており、それらの組み合わせによってコンテンツのデータ構造や入稿画面のUIが決定されます。 モデル

フィールドを追加していきます。
右上の保存ボタンを忘れないこと!!これを忘れていて、詰まりました。

モデルの作成

これはメインコンテンツである、Workのモデルです。
worksでは一覧ページと詳細ページが存在するので、スラッグでidを指定して、個別のページを取得できるようになっています。
カバー画像は一枚で、サブ画像は複数設定できます。

ここでJSONのプレビューも確認できます

{
  "_id": "_id",
  "_sys": {
    "createdAt": "2022-01-01T00:00:00.000Z",
    "updatedAt": "2022-01-01T00:00:00.000Z",
    "raw": {
      "createdAt": "2022-01-01T00:00:00.000Z",
      "updatedAt": "2022-01-01T00:00:00.000Z",
      "firstPublishedAt": "2022-01-01T00:00:00.000Z",
      "publishedAt": "2022-01-01T00:00:00.000Z"
    }
  },
  "title": "text",
  "slug": "text",
  "meta": {
    "title": "text",
    "description": "text",
    "ogImage": {
      "_id": "imageId",
      "src": "imageUrl",
      "fileType": "image/png",
      "fileSize": 12345678,
      "fileName": "image.png",
      "width": 600,
      "height": 400
    }
  },
  "body": "<p>Plain text is available using the fmt operator.</p>",
  "coverImage": {
    "_id": "imageId",
    "src": "imageUrl",
    "fileType": "image/png",
    "fileSize": 12345678,
    "fileName": "image.png",
    "width": 600,
    "height": 400
  },
  "subImages": [
    {
      "_id": "imageId",
      "src": "imageUrl",
      "fileType": "image/png",
      "fileSize": 12345678,
      "fileName": "image.png",
      "width": 600,
      "height": 400
    }
  ],
  "member": "<p>Plain text is available using the fmt operator.</p>"
}

表示したいデータを登録していく

モデルを登録したら、表示したいデータを登録していきます。 「Work(モデル名)を追加」ボタンをクリックすると、先に設定したフィールド項目がフォームになっているので、埋めていきます。

Newt CDN API Tokenを生成する

グローバルに分散されたCDNを経由してデータを取得します。キャッシュされたリクエストに対して非常に高速なレスポンスを返すことができます。

また、「Newt CDN API」は公開されたコンテンツのみを取得することができ、下書き状態のコンテンツについては取得できません。

APIキー

スペース設定の「APIキー」画面から作成します

Next.js側の設定

こちらの公式ブログが詳しいです

www.newt.so

Vercelでホスティングする

こちらの公式ブログが詳しいです

GitHubのリポジトリとVercelを接続して、ホスティングする

ローカルでは、.env.localに登録していた環境変数をVercel側に登録するのを忘れていてエラーを出してしまいました、忘れずに。

自動デプロイ設定する

毎回毎回Vercel上で手動デプロイするのは不便なので、Newt側で非エンジニアがコンテンツを更新(公開)したときも自動でデプロイされるようにしたいです。

まず、VercelでProjectのsetting > gitからDeploy Hooksを作成する(hooks名、ブランチ名)。作成したURLはコピーしておく。

続いて、Newt側のスペース設定>Webhook で、VercelのDeploy hooksを登録します。先のURLを貼り付け。

完成!

半日くらいで作成できました!
一度作成してしまえばメンテナンスコスト0で、非エンジニアでも好きなタイミングでコンテンツを更新できるので、作ってよかったです。

また、このHPきっかけで友達の活動が編集者の方の目に止まり建築雑誌に掲載されたりと嬉しい反応もあり、友達も喜んでくれたので嬉しかったです! タテモノトカ

Storybook 6.x から 7.0へアップグレードするときに詰まったこと

概要

StorybookをStorybook 6.x から 7.0へアップグレードしたときに、公式のMigrationガイドを元に進めたものの、テストがFlakyになってしまったり、落ちてしまったり、修正に苦労したのでまとめます。 アップグレード方法はざっとだけ触れます

全体のおおまかな流れ

  1. Storybook7へアップグレード
  2. 廃止されたaddonの削除
  3. main.ts の修正
  4. 廃止された型の修正
  5. @storybook/testing-reactアンインストール
  6. 詰まったところ

公式のMigration Guide

Migration guide for Storybook 7.0

Storybook7へアップグレード

pnpm dlx storybook@latest upgrade

https://storybook.js.org/docs/react/migration-guide#automatic-upgrade

deprecatedになったaddonの削除 / 新規addon追加

互換性がなくなったaddonがあるとAttentionがでるので削除する

一覧 Community outreach to upgrade addons to 7.0 · Issue #20529 · storybookjs/storybook · GitHub

main.tsの修正

main.jsはこちらを参考に Configure Storybook

Next.jsのroutingなど、addon不要になったのが個人的に嬉しかった🎉

Integrate Next.js and Storybook automatically

廃止された型の修正

storybook/MIGRATION.md at next · storybookjs/storybook · GitHub

ComponentStory, ComponentStoryObj, ComponentStoryFn and ComponentMeta types are deprecated

The type of StoryObj and StoryFn have been changed in 7.0 so that both the "component" as "the props of the component" will be accepted as the generic parameter.

ジェネリックパラメータとして、コンポーネント自体とコンポーネントのプロパティが受け入れられるようになった

import type { Story } from '@storybook/react';
import { Button, ButtonProps } from './Button';

// This works in 7.0, making the ComponentX types redundant.
const meta: Meta<typeof Button> = { component: Button };

export const CSF3Story: StoryObj<typeof Button> = { args: { label: 'Label' } };

export const CSF2Story: StoryFn<typeof Button> = (args) => <Button {...args} />;
CSF2Story.args = { label: 'Label' };

// Passing props directly still works as well.
const meta: Meta<ButtonProps> = { component: Button };

export const CSF3Story: StoryObj<ButtonProps> = { args: { label: 'Label' } };

export const CSF2Story: StoryFn<ButtonProps> = (args) => <Button {...args} />;
CSF2Story.args = { label: 'Label' };

@storybook/testing-reactアンインストール

This package is deprecated as it is a core functionality of Storybook 7! · Issue #143 · storybookjs/testing-react · GitHub

Hey there! I wanted to thank you for using @storybook/testing-react!

@storybook/testing-react has been promoted to a first-class Storybook functionality in Storybook 7. This means that you no longer need this package, and this package will not be worked on anymore (especially regarding Storybook 6, unless there are security issues). Instead, you can import the same utilities, but from the @storybook/react package. Additionally, the internals of composeStories and composeStory have been revamped, so the way a story is composed is way more accurate, and it's possible this issue doesn't happen there anymore.

Please do the following:

Upgrade to Storybook 7 if you haven't already Uninstall @storybook/testing-react Update your imports from @storybook/testing-react to @storybook/react

@storybook/testing-reactは同じユーティリティを@storybook/reactパッケージからインポートできるようになったので、不要になる。アンインストールする。

// Component.test.jsx
- import { composeStories } from '@storybook/testing-react';
+ import { composeStories } from '@storybook/react';

// setup-files.js
- import { setProjectAnnotations } from '@storybook/testing-react';
+ import { setProjectAnnotations } from '@storybook/react';

詰まったところ

上記の一通りの更新作業を終えたところ、StoryをJestで再利用しているのですが、Storybookのplay関数を使ってテストしている箇所や、userEventのsetup関数を使っている箇所が尽く落ちるようになってしまった。

コードは公式より引用

export const InputFieldFilled: Story<InputFieldProps> = {
  play:  ({ canvasElement }) => {
    const canvas = within(canvasElement);
    userEvent.type(canvas.getByRole('textbox'), 'Hello world!');
  },
};
const { InputFieldFilled } = composeStories(stories);

test('renders with play function', async () => {
  const { container } = render(<InputFieldFilled />);

  // pass container as canvasElement and play an interaction that fills the input
  await InputFieldFilled.play({ canvasElement: container });

  const input = screen.getByRole('textbox') as HTMLInputElement;
  expect(input.value).toEqual('Hello world!');
});

対処方法

非同期処理に修正

-  play: ({ canvasElement }) => {
+  play: async ({ canvasElement }) => {
     const canvas = within(canvasElement)
-    userEvent.type(canvas.getByRole('textbox'), 'Hello world!')
+    await userEvent.type(canvas.getByRole('textbox'), 'Hello world!')
   },
 }

関連ライブラリを全部最新化 storybookだけアップデートしたものの、testing-library周りを最新化していなかった しばらくupdateしていなかったもの↓ これでplay関数周りは治った

   "@testing-library/dom": "9.3.3",
   "@testing-library/jest-dom": "6.1.3",
   "@testing-library/user-event": "14.5.1",

testing-library/jest-domのv6 アップデートについて詰まったところ

testing-library/jest-dom をv6系にupdateすると、型を手動で取り込む必要があるので、グローバルな型定義ファイル(global.d.ts)にマッチャーの型定義を手動でインポートして定義する必要があった。 公式issue ※公式issueではこの問題はv6でresolvedとあるが、できなかった

userEvent.typeで詰まったところ

userEvent.type not workingのissue issue おそらくこのissueと同じ?でuserEvent.typeを使用している箇所がflakyになってしまった。

↓公式のこの書き方だと、user.typeもoKだったけど、setup関数化して使っているところはflakyになる

Introduction | Testing Library

import userEvent from '@testing-library/user-event'

// setup function
function setup(jsx) {
  return {
    user: userEvent.setup(),
    // Import `render` from the framework library of your choice.
    // See https://testing-library.com/docs/dom-testing-library/install#wrappers
    ...render(jsx),
  }
}

test('render with a setup function', async () => {
  const {user} = setup(<MyComponent />)
  // ...
})
   const user = userEvent.setup()
   // Import `render` and `screen` from the framework library of your choice.
   // See https://testing-library.com/docs/dom-testing-library/install#wrappers
   render(<MyComponent />)

zodでうるう年判定(有効日付)のバリデーション

概要

日付を登録するフォームで、有効日付(うるう年など)かどうかを判定したい。 年月日が分かれている選択フォームの場合、日付のバリデーションで迷ったのでメモ

環境

react 18.2.0
date-fns 2.29.3
react-hook-form 7.43.9
zod 3.21.4

簡易なフォームをつくる

年・月・日が分かれているフォーム

フォーム

import "./styles.css";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { isValid } from "date-fns";

type FormData = {
  registerYear: string;
  registerMonth: string;
  registerDay: string;
};

const schema = z
  .object({
    registerYear: z.string().nonempty(),
    registerMonth: z.string().nonempty(),
    registerDay: z.string().nonempty()
  });

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  const onSubmit = handleSubmit((data) => console.log(data));

  return (
    <form onSubmit={onSubmit}>
      <div id="birthday">登録日</div>
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          marginBottom: "10px"
        }}
      >
        <div
          style={{
            marginBottom: "20px"
          }}
        >
          <input
            id="yearInput"
            type="number"
            min="2023"
            max="2040"
            aria-labelledby="birthday yearLabel"
            {...register("registerYear")}
          />
          <label id="yearLabel" htmlFor="yearInput">
            年
          </label>
          <input
            id="monthInput"
            type="number"
            min="1"
            max="12"
            aria-labelledby="birthday monthLabel"
            {...register("registerMonth")}
          />
          <label id="monthLabel" htmlFor="monthInput">
            月
          </label>
          <input
            id="dayInput"
            type="number"
            min="1"
            max="31"
            aria-labelledby="birthday dayLabel"
            {...register("registerDay")}
          />
          <label id="dayLabel" htmlFor="dayInput">
            日
          </label>
        </div>
        {errors.registerYear && <p>{errors.registerYear.message}</p>}
        <button
          id="savebutton"
          aria-labelledby="birthday savebutton"
          type="submit"
          style={{ width: "100px" }}
        >
          保存する
        </button>
      </div>
    </form>
  );
}

うるう年の判定をしたい

date-fnsisValidという一見、有効日付の判定を行えそうなメソッドがあるものの、うるう年、31日が存在しない日付の判定ができない。32日などどんな月でも有効でない日付はfalseになる。

ここでは、有効日付を確認するために、Dateオブジェクトを生成して確認する。

例:指定された日付が存在しない場合、JavaScriptのDateは以下のようになる

console.log(new Date("2023-09-30")); // Sat Sep 30 2023 09:00:00 GMT+0900 (Japan Standard Time)
console.log(new Date("2023-09-31")); // Sun Oct 01 2023 09:00:00 GMT+0900 (Japan Standard Time)

"2023-09-30"は有効な日付なのでそのまま解釈する
"2023-09-31"は存在しない日付だが、自動的に日付を"2023-10-01"に修正する

有効な日付でない場合、Dateオブジェクトが自動的に有効な日付に変換するので、フォームで選択した値でDateオブジェクトを生成して、選択した値と一致するかどうかで有効日付か否かを判定する。

// 追加
const getSelectedDate = ({
  registerYear,
  registerMonth,
  registerDay
}: FormData) => {
  return new Date(
    Number(registerYear),
    Number(registerMonth) - 1, // Dateオブジェクトは月の値が0から始まるので、選択された値から1を引く
    Number(registerDay)
  );
};

const schema = z
  .object({
    registerYear: z.string().nonempty(),
    registerMonth: z.string().nonempty(),
    registerDay: z.string().nonempty()
  })
  .refine(
    (data) => {
      const selectedDate = getSelectedDate(data);
     
      return (
        isValid(selectedDate) && // 無効な日付かチェック
 // 選択した年月日がDateオブジェクトによる無効値の自動修正によって変更されていないことを確認
        selectedDate.getFullYear() === Number(data.registerYear) &&
        selectedDate.getMonth() + 1 === Number(data.registerMonth) &&
        selectedDate.getDate() === Number(data.registerDay)
      );
    },
    { message: "存在しない日付です", path: ["registerYear"] }
  )

参考にした記事

JavaScript day.js, date-fns で実在する日付かどうか判定したい - かもメモ 【JavaScript】 うるう年判定の一番簡単な方法

WebViewを確認するアプリをつくる

概要

  • AndroidでWebViewを利用している箇所のデバッグする方法が全くわからなかったので、調べながらまとめてみました
  • Android StudioでWebViewを確認できるだけの簡易的なアプリをつくっていきます

Android Studioをダウンロードする

Download Android Studio & App Tools - Android Developers

時間かかる

Projectをつくる

  1. New Project
  2. Empty View Activityを選択する

Empty Views Activity

ファイル作成

  1. インストールが終わったらファイルを書き換えていく
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>
package com.example.myapplication;

import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = findViewById(R.id.web_view);

        webView.setWebViewClient(new WebViewClient());

        // JavaScriptを有効に
        webView.getSettings().setJavaScriptEnabled(true);

        // デバッグ可能に
        WebView.setWebContentsDebuggingEnabled(true);

        webView.loadUrl("https://www.hatenablog.com/");
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" /> // インターネット接続を許可する

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        android:usesCleartextTraffic="true" // 暗号化されていないトラフィックを送信または受信するかどうか。localhostでのデバッグをするので追加
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

ビルドする

ファイルが準備できたら、ビルドする(ツールバーのBuild-Make Project)

make project

エミュレーターを起動する

Device Manager > 端末を指定して起動、RUNを実行する

Device Manager

起動成功

WebViewでlocalhostをロードする

url部分を書き換える。 エミュレーター自身が127.0.0.1を指すため、開発マシンのlocalhost10.0.2.2を指定する

Android Emulator のネットワークをセットアップする  |  Android Studio  |  Android Developers

// 略

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

// 略

        webView.loadUrl("http://10.0.2.2:3000"); // http://10.0.2.2:<ポート番号>/
    }
}

完成

localhost

参考記事

WebView でのウェブアプリの作成  |  Android デベロッパー  |  Android Developers

[Android][Tips] WebViewで開いているページをデバッグする | DevelopersIO

aria-labelledby属性を試してみる

概要

aria-labelledby属性が???だったので調べていたところ、そもそもアクセシビリティについて無知だったので、WAIのことから動作確認も含めて試してみた

アクセシビリティとは?

What is accessibility? - Learn web development | MDN

Accessibility is the practice of making your websites usable by as many people as possible. We traditionally think of this as being about people with disabilities, but the practice of making sites accessible also benefits other groups such as those using mobile devices, or those with slow network connections.

できるだけ多くの人に対し、Webサイトを利用しやすくすること。障がいのある方のためだけではなく、モバイルデバイス利用者やネット速度が遅いユーザに対しても有益である。

WAI

Web Accessibility Initiative(WAI) アクセシビリティへの理解向上と基準、サポート資料を率先して行っている組織。 W3Cの内部組織として発足した。 Home | Web Accessibility Initiative (WAI) | W3C

W3Cとは(The World Wide Consortium)Web: HTML,CSS and more の国際標準化を推進している組織。

アクセシビリティガイドライン(WCAG)

WAIが策定しているアクセシビリティを推進するためのガイドライン WCAG 2 Overview | Web Accessibility Initiative (WAI) | W3C

WAI-ARIA (Accessible Rich Internet Applications)

WAI-ARIAW3C によって定められた仕様で、要素に適用できる追加の意味論を提供する一連の HTML 属性を定義しており、それが欠けているどのような場所でもアクセシビリティを向上させます。

WAI-ARIA Overview | Web Accessibility Initiative (WAI) | W3C WAI-ARIAの基本 - ウェブ開発を学ぶ | MDN

スクリーンリーダーを試してみる

macOSの場合はcommand + touchID3回クリックでVoid Overが使用できる。
後は読み上げたいWebサイトに行くだけでスクリーンリーダーが起動する。

Voice Over

aria-*属性を試してみる

よく使う属性2つをスクリーンリーダーで試しながら使ってみる

aria-label属性

アイコンボタンなど、視覚的には意味がわかるものの、テキストを持たない要素にラベルを与えるために使用する。
例えば、Xを閉じるボタンとして使用している例。
これをスクリーンリーダーで読み上げると、「閉じる ボタン」と読み上げてくれる。元のラベルである「X」は上書きされる。

<button aria-label="閉じる">X</button>

スクリーンリーダー

aria-labelledby属性

  • aria-labelと同じ動きをするが、属性値には文字列ではなく、要素のIDを指定する。
  • スペースで区切って、IDを複数指定することもできる。

生年月日を送信するフォームで、年・月・日が複数の入力フィールドに分かれている場合、「生年月日」という一つの情報を取り扱っているとaria-labelledby属性を使って表現する例

          <div id="birthday">生年月日</div>
          <input
            id="yearInput"
            type="number"
            aria-labelledby="birthday yearLabel" // 生年月日のラベルIDをつけている
          />
          <label id="yearLabel" htmlFor="yearInput">
            年
          </label>

          <input
            id="monthInput"
            type="number"
            aria-labelledby="birthday monthLabel"
          />
          <label id="monthLabel" htmlFor="monthInput">
            月
          </label>
          <input
            id="dayInput"
            type="number"
            aria-labelledby="birthday dayLabel"
          />
          <label id="dayLabel" htmlFor="dayInput">
            日
          </label>
        <button id="savebutton" aria-labelledby="birthday savebutton">
          保存する
        </button>

こうすると、birthdayと関連づけられるので、スクリーンリーダーでは、「生年月日 年」「生年月日 月」「生年月日 日」と読み上げてくれる

スクリーンリーダー 生年月日 月

スクリーンリーダー 生年月日 保存する ボタン

【React】スクロールダウン・アップでボタンの非表示・表示を切り替える

概要

スクロールアップ時に表示され、スクロールダウン時に非表示になるButtonコンポーネントをつくりたい

スクロールボタン

要件・流れ

  • スクロールアップ ---ボタン表示
  • スクロールダウン ---ボタン非表示

  • 以前のスクロール位置をuseStateで保持する

  • ボタンの非表示・表示はdisplay: block display: noneを切り替えて管理する
  • 前回のスクロール位置が現在のスクロール位置よりも大きい場合、ボタンを表示する。これは、ユーザーがページを上にスクロールしていることと同意
export const ScrollButton = () => {
// ボタン表示・非表示の状態
  const [isVisible, setIsVisible] = useState(true);
// 前回のスクロール位置
  const [prevScrollPosition, setPrevScrollPosition] = useState(0);

  const handleScroll = () => {
// 現在のスクロール位置を取得
    const currentScrollPos = window.scrollY;
// 前回のスクロール位置が現在のスクロール位置よりも大きい場合、ボタンを表示する
    const isVisible = prevScrollPosition > currentScrollPos;

    setIsVisible(isVisible);
    setPrevScrollPosition(currentScrollPos);
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);

// クリーンアップ
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [handleScroll]);

  return (
    <button style={{ display: isVisible ? "block" : "none" }}>
      Float Button
    </button>
  );
};

Refactor

現在だとスクロールイベントが高頻度で発生してしまうため、パフォーマンスに影響を与えてしまう可能性がある。

そのため、debounce関数を使って、イベントを間引きして実行するように修正していく。

import React, { useState, useEffect } from "react";
import { debounce } from "lodash";

export const ScrollButton = () => {
  const [isVisible, setIsVisible] = useState(true);
  const [prevScrollPosition, setPrevScrollPosition] = useState(0);

  const handleScroll = () => {
    const currentScrollPos = window.scrollY;
    const isVisible = prevScrollPosition > currentScrollPos;
    setIsVisible(isVisible);
    setPrevScrollPosition(currentScrollPos);
    console.log("scroll");
  };

  const debounceHandleScroll = debounce(handleScroll, 100);

  useEffect(() => {
    window.addEventListener("scroll", debounceHandleScroll);

    return () => {
      window.removeEventListener("scroll", debounceHandleScroll);
    };
  }, [debounceHandleScroll]);

  return (
    <button style={{ display: isVisible ? "block" : "none" }}>
      Float Button
    </button>
  );
};

bounce関数使用前

scroll without bounce

bounce関数使用後

scroll with bounce

参考にしたサイト

React, Vue.js, JavaScriptでdebounceの仕組みを理解 | アールエフェクト

MySQLでReadOnly権限のユーザーを作成する

概要

BIツール上でデータの更新・削除が行われることを防ぐために、MySQLでREADONLY権限のUserを作成したい。

前提

AWS踏み台サーバ経由でRDSに接続している

流れ

## EC2インスタンスへsshログインする
~/.ssh
❯ ssh -i ~/.ssh/id_rsa(秘密鍵)  ec2-user@xxxxxxx
## MySQLへログインする
[ec2-user@xxxxxxx]$ mysql -h RDSエンドポイント -p -u dbuser(ユーザー名)
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is xxxxx
Server version: 5.7.33-log Source distribution

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
### SELECT権限を持つユーザー作成
mysql> GRANT SELECT ON *.* TO (新しく作るユーザー名)@'%' IDENTIFIED BY '(パスワード)';

mysql> GRANT SELECT ON *.* TO read-user@'%' IDENTIFIED BY 'passward';
## ユーザ情報の反映
mysql> FLUSH PRIVILEGES;

確認コマンド

## ユーザーの権限情報が確認できる
mysql> SHOW GRANTS FOR 'ユーザー名'@'%';
## 現状のユーザー情報確認
mysql> select user, host from mysql.user;
+---------------+-----------+
| user          | host      |
+---------------+-----------+
| user          | %         |
| mysql.session | localhost |
| mysql.sys     | localhost |
+---------------+-----------+