フロントエンド開発といえば。
react アプリの初期化( npm init vite@latest <アプリ名> )

reactでvideo.js

video.js のインストール

npm i video.js
npm i -D @types/video.js

● VideoPlayer コンポーネントの作成

src/VideoPlayer.tsx

import React, { useEffect, useRef, useState } from 'react';
import videojs from 'video.js';
import Player from 'video.js/dist/types/player';

interface VideoPlayerProps {
  src: string;
  type?: string;
}

const VideoPlayer: React.FC<VideoPlayerProps> = ({
  src,
  type = 'video/mp4',
}) => {
  const videoRef = useRef<HTMLVideoElement | null>(null);
  const playerRef = useRef<Player | null>(null);
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    setIsMounted(true);
    return () => setIsMounted(false);
  }, []);

  useEffect(() => {
    if (isMounted && videoRef.current && !playerRef.current) {
      const videoElement = videoRef.current;

      const player = videojs(
        videoElement,
        {
          controls: true,
          fluid: true,
          sources: [
            {
              src: src,
              type: type,
            },
          ],
        },
        () => {
          console.log('Player is ready');
        }
      );

      playerRef.current = player;

      return () => {
        if (playerRef.current) {
          playerRef.current.dispose();
          playerRef.current = null;
        }
      };
    }
  }, [isMounted, src, type]);

  useEffect(() => {
    if (isMounted && playerRef.current) {
      playerRef.current.src({ type: type, src: src });
    }
  }, [isMounted, src, type]);

  return (
    <div data-vjs-player>
      <video ref={videoRef} className="video-js" />
    </div>
  );
};

export default VideoPlayer;

● コンポーネントの表示

src/App.tsx

import './App.css';
import VideoPlayer from './VideoPlayer';

function App() {
  return <VideoPlayer src="https://vjs.zencdn.net/v/oceans.mp4" />;
}

export default App;

● IVS

npm i amazon-ivs-player copy-webpack-plugin
No.2556
10/03 18:55

edit

Redux の 作り方

● Redux の 作り方A (いにしえの作り方。一応知っておいた方いいです。)

構成

src/
├── store/
│   ├── alert/
│   │   ├── actions.ts
│   │   ├── reducer.ts
│   │   └── types.ts
│   └── index.ts

types.ts

// src/store/alert/types.ts

export const SHOW_ALERT = 'SHOW_ALERT';
export const HIDE_ALERT = 'HIDE_ALERT';

export type AlertState = 'show' | 'hidden';

interface ShowAlertAction {
  type: typeof SHOW_ALERT;
}

interface HideAlertAction {
  type: typeof HIDE_ALERT;
}

export type AlertActionTypes = ShowAlertAction | HideAlertAction;

actions.ts

// src/store/alert/actions.ts

import { SHOW_ALERT, HIDE_ALERT, AlertActionTypes } from './types';

export const showAlert = (): AlertActionTypes => ({
  type: SHOW_ALERT,
});

export const hideAlert = (): AlertActionTypes => ({
  type: HIDE_ALERT,
});

reducer.ts

// src/store/alert/reducer.ts

import { AlertState, AlertActionTypes, SHOW_ALERT, HIDE_ALERT } from './types';

const initialState: AlertState = 'hidden';

export const alertReducer = (
  state = initialState,
  action: AlertActionTypes
): AlertState => {
  switch (action.type) {
    case SHOW_ALERT:
      return 'show';
    case HIDE_ALERT:
      return 'hidden';
    default:
      return state;
  }
};

index.ts

// src/store/index.ts

import { createStore, combineReducers } from 'redux';
import { alertReducer } from './alert/reducer';

const rootReducer = combineReducers({
  alert: alertReducer,
});

export type RootState = ReturnType<typeof rootReducer>;

export const store = createStore(rootReducer);

● Redux の 作り方B (Redux Toolkit の createSlice での作り方。)

構成

src/
├── store/
│   ├── alertSlice.ts
│   └── index.ts

alertSlice.ts

// src/store/alertSlice.ts

import { createSlice } from '@reduxjs/toolkit';

export type AlertState = 'show' | 'hidden';

const initialState: AlertState = 'hidden';

const alertSlice = createSlice({
  name: 'alert',
  initialState,
  reducers: {
    showAlert: (state) => 'show',
    hideAlert: (state) => 'hidden',
  },
});

export const { showAlert, hideAlert } = alertSlice.actions;
export default alertSlice.reducer;

index.ts

// src/store/index.ts

import { configureStore } from '@reduxjs/toolkit';
import alertReducer from './alertSlice';

export const store = configureStore({
  reducer: {
    alert: alertReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
No.2550
09/13 11:30

edit

Emotion から goober へ乗り換える

● goober のインストール

goober では className を使用するので clsx も 同時に入れておくと便利です

npm i goober
npm i clsx

● import文を書き換えて css を className に書き換える

import { css } from "@emotion/react";
  ↓
import { css } from "goober";

css はそのまま使えます

const myStyle = css`
  background-color: #fff;
  border-radius: 8px;
  padding: 8px 16px;
  position: fixed;
  font-size: 20px;
  color: #493333;
`;
import { css } from "@emotion/react";
  ↓
import { css } from "goober";

jsx の css を classNameに書き換えます。

<div css={'myStyle'}>テスト</div>
  ↓
<div className={'myStyle'}>テスト</div>

● goober で props で CSSスタイルを受け付ける

"use client";
import { clsx } from "clsx";
import type { CSSAttribute } from "goober";
import { css } from "goober";
import type { FC } from "react";

type Props = {
  cssProp?: CSSAttribute | ReturnType<typeof css>;
};

export const MyComponent: FC<Props> = ({ cssProp }) => {
  const baseSkeletonStyle = css`
    border: solid 1px #ccc;
  `;

  return <div className={clsx(baseSkeletonStyle, cssProp)}></div>;
};

呼び出す側

<MyComponent
  cssProp={css`
    width: 50px;
    height: 20px;
  `}
/>

● goober で引数を受けとる関数を作成する

const userDocTitleStyle = (labelColor: string) => css`
  position: relative;
  display: block;
  height: 70px;
  padding: 8px 16px 8px 24px;
  &:before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    border-left: 8px solid ${labelColor};
    opacity: 1;
  }
`;

● emotionのアンインストール

tsconfig.json

  "compilerOptions": {
    // この行を削除 "jsxImportSource": "@emotion/react",

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  //この行を削除 compiler: {
  //この行を削除   emotion: true,
  //この行を削除 },
};

export default nextConfig;

サーバーコンポーネントの過去ファイルから次の行を削除

/* @jsxImportSource react */

npm コマンドで アンインストール

npm uninstall @emotion/react

● extractCssを使ってSSR対応する

https://codesandbox.io/p/devbox/7m9zzl6746?

No.2546
09/26 16:38

edit

fetch と axios のざっくりとした違い

● axios

  const res = await axios.get('https://httpbin.org/status/404');
  console.log('● res');
  console.log(res);

  // res.data        → jsonデータが返る
  // ネットワークエラー → 例外発生
  // APIエラー(404)   → 例外発生

● fetch

  const res = await fetch('https://swapi.dev/api/people/1');
  console.log('● res');
  console.log(await res.json());

  // await res.json() → jsonデータが返る
  // ネットワークエラー  → 例外発生
  // APIエラー(404)    → 例外発生しない 以下のように返ってくる ↓
  // res.ok = false
  // res.status: 404,
  // res.statusText: 'NOT FOUND',

No.2540
08/27 14:55

edit

ts-auto-guardの使い方

● ts-auto-guardとは?

外からやってくるjsonデータを検査する関数を型から自動生成するものです。

● ts-auto-guard のインストール

npm i -D ts-auto-guard

● 使い方 1. 自動生成

型を記述してあるファイル名を指定して実行

npx ts-auto-guard --export-all src/types/post.ts

同じフォルダに Post.guard.ts という名前で 自動生成されます。

● 使い方 2. データ受信ライブラリに組み込む

Tanstack Query や RTK Query などに組み込みます。

ガードを実行する関数を作成します

/**
 * レスポンスを検査するガード関数を作成して返す。
 * - developmentの場合は受け取ったガード関数を実行する関数を返す
 * - development以外の場合は何も作成せずundefinedを返す
 */
function createValidateResponseType<T>(
  guardFunc: (response: unknown) => response is T,
): ((response: unknown) => T) | undefined {
  if (process.env.NODE_ENV !== 'development') {
    return undefined;
  }
  return (response: unknown): T => {
    if (guardFunc(response)) {
      return response;
    }
    throw new Error(
      `バリデーションエラー。受け取ったデータが(${guardFunc.name})関数にマッチしません。オブジェクトのプロパティの命名と型を確認してください。`,
    );
  };
}

Tanstack Query( React Query )の場合

selectオプションで実行します

import { isPost } from '@/types/post.guard';

const useDataQuery = () => {
  return useQuery({
    queryKey: ['data'],
    queryFn: fetchData,
      // データ受信後にバリデーションを実行する
    select: createValidateResponseType(isPost),
  });
};

RTK Queryの場合

transformResponseオプションで実行します

import { isPost } from '@/types/post.guard';

export const myApi = createApi({
  ........ 
  endpoints: (builder) => ({
    getPost: builder.query<Post | null, void>({
      query: (): FetchArgs => ({
        url: '/post',
      }),
      // データ受信後にバリデーションを実行する
      transformResponse: createValidateResponseType(isPost),
    }),
  }),

● 自動生成される型ガードファイルは、anyを使うことがあるので、除外しておきましょう

vi .eslintignore

例:

src/common/api/*.guard.ts
No.2537
09/09 11:27

edit

webアプリ開発時のCORS対応に必ず登場する HAR の使い方

● HAR の保存方法

Google Chrome → F12 → Network  → Fetch/XHRタブを選択 → 送信されたリクエストを右クリックして「Save all as HAR with content」

● HAR Analyzer

https://toolbox.googleapps.com/apps/har_analyzer/?lang=ja

No.2535
07/31 10:13

edit

eslint で 現在の設定値を確認する

● eslint で 現在の設定値を確認する

npx eslint --print-config 設定ファイル名
npx eslint --print-config .eslintrc.json | pbcopy

とすると、 設定一覧が出てきます。

No.2516
08/23 09:08

edit

RTK Query

● RTK Queryのインストール

npm install @reduxjs/toolkit react-redux

● 作成するファイル

・src/providers/ReduxProvider.tsx
・src/stores/myApi.ts
・src/stores/store.ts

● ファイルの作成 1

src/providers/ReduxProvider.tsx

'use client';
import React from 'react';

import { store } from '../stores/store';

import { Provider } from 'react-redux';

export const ReduxProvider = (props: React.PropsWithChildren) => {
  return <Provider store={store}>{props.children}</Provider>;
};

● ファイルの作成 2

src/stores/store.ts

import { configureStore } from '@reduxjs/toolkit';
import { myApi } from './myApi';
import { setupListeners } from '@reduxjs/toolkit/query';

export const store = configureStore({
  reducer: {
    [myApi.reducerPath]: myApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(myApi.middleware),
});

setupListeners(store.dispatch);

● ファイルの作成 3

src/stores/myApi.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

type People = {
  name: string;
  height: string;
  mass: string;
};

export const myApi = createApi({
  reducerPath: 'myApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://swapi.dev/api/' }),
  endpoints: (builder) => ({
    getPeople: builder.query<People, string>({
      query: (id) => `people/${id}`,
    }),
  }),
});

export const { useGetPeopleQuery, useLazyGetPeopleQuery } = myApi;

以下の命名でフックが作成されます。

// use + <定義した名称> + Query
// useLazy + <定義した名称> + Query

違いは「呼び出した時に自動でクエリ実行される」or「クエリを即座に実行せず、後で手動で実行するための関数を返す」です。

● 既存ファイルの修正

src/main.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { ReduxProvider } from './providers/ReduxProvider.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ReduxProvider>
      <App />
    </ReduxProvider>
  </React.StrictMode>
);

● hooks (useクエリ)を使ってデータ取得するサンプル

src/App.tsx

import './App.css';
import { useGetPeopleQuery } from './stores/myApi';

function App() {
  const { data, isLoading } = useGetPeopleQuery('2');

  return (
    <>
      <h1>sample</h1>
      {isLoading && <div>Loading...</div>}
      <div>{JSON.stringify(data)}</div>
    </>
  );
}

export default App;

useGetPeopleQuery('2'); を実行した瞬間に API のリクエストが走ります。

● hooks (useLazyクエリ)を使ってデータ取得するサンプル

const [trigger, {data, isFetching}, lastPromiseInfo] = useLazyGetTimeQuery();
const handleClick =  async () => {
  trigger()
}

return(
  <>
    <h4>{isFetching ? 'ローディング中' : JSON.stringify()}</h4>
    <button type={'button'} onClick={handleClick} >LazyQuery実行</button>
  </>
)

useLazyクエリ の場合はtrigger関数を受け取って使用します。また useLazyGetTimeQuery() 実行時に API へのリクエストは走りません

trigger関数は Promise を返すので以下のように使うこともできます。

const [trigger, {data, isFetching}, lastPromiseInfo] = useLazyGetTimeQuery();

const handleClick = async () => {
  try {
    const result = await trigger();
    console.log("クエリ結果:", result.data);
  } catch (error) {
    console.error("エラー発生:", error);
  }
};

useLazyクエリ の場合はtrigger関数を受け取って使用します。また useLazyGetTimeQuery() 実行時に API へのリクエストは走りません

● RTK Query の Middleware

エラーログを表示するミドルウェアを作成してみます。

src/middlewares/errorLogMiddleware.ts

import {
  Middleware,
  isRejectedWithValue,
  MiddlewareAPI,
} from '@reduxjs/toolkit';
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';

type EventData = {
  eventCategory: string;
  eventAction: string;
  eventLabel: string;
};

export const errorLogMiddleware: Middleware =
  (_: MiddlewareAPI) => (next) => (action) => {
    if (isRejectedWithValue(action)) {
      const errorPayload = action.payload as FetchBaseQueryError;
      const errorData: string = errorPayload.data as string;

      // エラー情報からイベントデータを作成
      const eventData: EventData = {
        eventCategory: 'API Error',
        eventAction: errorPayload.status as string,
        eventLabel: errorData,
      };

      // エラーを表示
      console.error('● eventData');
      console.error(eventData);
    }

    return next(action);
  };

src/stores/store.ts

import { configureStore } from '@reduxjs/toolkit';
import { myApi } from './myApi';
import { setupListeners } from '@reduxjs/toolkit/query';
import { errorLogMiddleware } from '../middlewares/errorLogMiddleware';

export const store = configureStore({
  reducer: {
    [myApi.reducerPath]: myApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(myApi.middleware).concat(errorLogMiddleware),
});

setupListeners(store.dispatch);

その他参考 : https://qiita.com/7tsuno/items/2301a35283db7cd54df9
https://zenn.dev/snamiki1212/scraps/3c7190317c5e8f

● RTK Queryの mutation

キャッシュ制御として

tagTypes: ['Item']

または

  .enhanceEndpoints({
    addTagTypes: ["Item"],
  })

をセットします。そして、クエリの場合は providesTags を、ミューテーションの場合は invalidatesTags をセットします。

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Item'],
  endpoints: (builder) => ({
    getItems: builder.query({
      query: () => '/items',
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({ type: 'Item', id })),
              { type: 'Item', id: 'LIST' },
            ]
          : [{ type: 'Item', id: 'LIST' }],
    }),
    createItem: builder.mutation({
      query: (newItem) => ({
        url: '/items',
        method: 'POST',
        body: newItem,
      }),
      invalidatesTags: [{ type: 'Item', id: 'LIST' }],
    }),
    updateItem: builder.mutation({
      query: ({ id, ...updateData }) => ({
        url: `/items/${id}`,
        method: 'PUT',
        body: updateData,
      }),
      invalidatesTags: (result, error, { id }) => [
        { type: 'Item', id },
        { type: 'Item', id: 'LIST' },
      ],
    }),
    deleteItem: builder.mutation({
      query: (id) => ({
        url: `/items/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: (result, error, id) => [
        { type: 'Item', id },
        { type: 'Item', id: 'LIST' },
      ],
    }),
    getItemById: builder.query({
      query: (id) => `/items/${id}`,
      providesTags: (result, error, id) => [{ type: 'Item', id }],
    }),
  }),
});

export const {
  useGetItemsQuery,
  useCreateItemMutation,
  useUpdateItemMutation,
  useDeleteItemMutation,
  useGetItemByIdQuery,
} = api;

export default api;
Unlike useQuery, useMutation returns a tuple. The first item in the tuple is the "trigger" function and the second element contains an object with status, error, and data.
useQuery とは異なり、useMutation はタプルを返します。タプルの最初の項目は「トリガー」関数で、2 番目の要素にはステータス、エラー、データを含むオブジェクトが含まれます。

例:

  // ミューテーション関数を取得
  const [createPost, { isLoading, isSuccess, isError, error }] = useCreatePostMutation();

    try {
      const result = await createPost(formData).unwrap();
      alert('メモが正常に作成されました!');
      console.log( result );
    } catch (err) {
      console.error('メモの作成に失敗しました:', err);
    }

● mutationの時に登場する unwrap()

mutation 実行時に戻ってくるデータは以下のデータが Promise で返ってきます。

{
  data?: MyData;
  error?: SerializedError | FetchBaseQueryError;
}

unwrap すると

MyData

が、そのまま取得できるのでぜひ活用しましょう。

const [updatePost, { isLoading: isSaving, isError }] =useUpdatePostMutation();

  // 保存する関数
  const handleSavePost = async (id: string, data: UpdatePostRequest) => {
    try {
      const result = await updateMemo({
        id: id,
        data: data,
      }).unwrap();
      console.log("Saved ok: ", result);
    } catch (error) {
      console.error("Failed to save post:", error);
    }
  };

● RTK Query データを 別のコンポーネントから強制的に invalidate する

RTK Query では mutation の後に invalidate するタグを渡して invalidate する使い方が基本ですが、 以下のように強制 invalidate できます。

src/stores/myApi.ts

export const myApi = createApi({
  reducerPath: 'myApi',
  baseQuery: fetchBaseQuery({ baseUrl: '' }),
  tagTypes: ['GetTimeTag'],
  endpoints: (builder) => ({
    getTime: builder.query<WorldTime, void>({
      query: () => `https://worldtimeapi.org/api/timezone/Asia/Tokyo`,
      keepUnusedDataFor: 4,  // キャッシュの保持期間を4秒に設定
      providesTags:['GetTimeTag']
    }),
  }),
});

export const { useGetTimeQuery } = myApi;
export const selectGetTimeResult = myApi.endpoints.getTime.select();

src/components/InvalidateButton.tsx

import { FC } from 'react';
import { myApi, selectGetTimeResult } from '../stores/myApi.ts';
import { useDispatch, useSelector } from 'react-redux';

export const InvalidateButton: FC = () => {
  const dispatch = useDispatch();
  const cacheDataTimeStamp =
    useSelector(selectGetTimeResult)?.fulfilledTimeStamp;

  const isCacheValid = (cacheDataTimeStamp: number | undefined): boolean => {
    if (cacheDataTimeStamp === undefined) return false;

    const currentTime = Date.now();
    const cacheDuration = 4 * 1000; // 4秒
    return currentTime - cacheDataTimeStamp < cacheDuration;
  };

  const handleClick = () => {
    if (isCacheValid(cacheDataTimeStamp)) {
      alert('キャッシュが有効✅です。');
    } else {
      alert('invalidate❌します。');
      dispatch(myApi.util.invalidateTags(['GetTimeTag']));
    }
  };

  return (
    <button onClick={handleClick}>
      GetTimeTagを持つデータのキャッシュをクリア
    </button>
  );
};
No.2514
10/23 11:00

edit

react で svg を import して扱う

● vite + react の場合

import された .svg は string として扱われます。

import temperatureLogo from '/temperature.svg';

console.log('● temperatureLogo');
console.log(temperatureLogo);

temperatureLogo の中身

/temperature.svg

● next.js の場合

import された .svg は オブジェクト(any型) として扱われます。

import temperatureLogo from '/temperature.svg';

console.log('● temperatureLogo');
console.log(temperatureLogo);

temperatureLogo の中身

{
  src: '/_next/static/media/temperature.2bfd6197.svg',
  height: 800,
  width: 800,
  blurWidth: 0,
  blurHeight: 0
}

ただしこのオブジェクトany型です
回避する場合は svg.d.ts を作成して tsconfig.json で読み込ませます

./src/types/svg.d.ts

interface ImportImageAttributes {
  src: string;
  height: number;
  width: number;
  placeholder?: string;
  blurWidth?: string;
  blurHeight?: string;
  blurDataURL?: string;
};

declare module '*.svg' {
  const content: ImportImageAttributes;
  export default content;
}
  "include": ["./src/types/svg.d.ts", "next-env.d.ts"],

● next.js で import した svg を Reactコンポーネントとして扱う

https://medium.com/@selmankoral/how-to-fill-your-sgv-on-react-8d67b3517c14

No.2512
04/24 15:35

edit

husky+lint-staged からの引っ越し先 → lefthook

● lefthookのインストール

・npm でインストールする場合

npm i -D lefthook

・Go でインストールする場合

go install github.com/evilmartians/lefthook@latest

・Ruby でインストールする場合

gem install lefthook

● lefthookの使い方

・1. lefthook.yml をプロジェクトルートに作成する
・2. コマンド lefthook install を実行して lefthook.yml から .git/hooks/ ファイルを生成する

あとは hooks ファイルがそれぞれのタイミングで自動起動します。

● lefthook.yml の書き方

https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md

● lefthook install を自動で行う

package.json の prepare または postinstall で指定しておくと、npm i 実行後に自動で lefthook install が走ります。

package.json

{
  "scripts": {
    "postinstall": "lefthook install"
  },

● lefthook

https://github.com/evilmartians/lefthook

No.2502
06/25 11:39

edit

npm init vite@latest で 作成したreactアプリで purgecss したい

Viteを使用して作成されたReactアプリでPurgeCSSを組み込む場合、設定はWebpackベースのプロジェクトとは異なります。
Viteは、デフォルトでRollupまたはesbuildを使ってバンドルを行うため、Webpackのプラグインを直接使用することはできません。
しかし、PostCSSを介してPurgeCSSを使用することができます。
npm i -D postcss postcss-cli @fullhuman/postcss-purgecss
vi postcss.config.js

postcss.config.js

import purgecss from '@fullhuman/postcss-purgecss';

export default {
  plugins: [
    purgecss({
      variables:true, // true: delete unused variables
      safelist: { // safelist
        standard: ['html', 'body'],
      },
      content: [
        'YOUR/DIR/bigsize.css',
        './src/**/*.tsx',
        './src/**/*.jsx',
      ],
    }),
  ],
};

オプションはこちら

https://purgecss.com/plugins/postcss.html

ViteはデフォルトでPostCSSをサポートしており、プロジェクトのルートディレクトリにpostcss.config.jsまたはpostcss.config.cjsファイルを配置することで、PostCSSの設定を行うことができます。

● next.js の場合

https://www.linkedin.com/pulse/removing-unused-css-purgecss-hasibul-islam

postcss は next.js が元々持っているので、インストール不要です。

パッケージのインストール

npm i -D @fullhuman/postcss-purgecss
npm i -D postcss-flexbugs-fixes postcss-preset-env        
// postcss.config.js

module.exports = {
  plugins: [
    // restore the Next.js default behavior
    "postcss-flexbugs-fixes",
    [
      "postcss-preset-env",
      {
        autoprefixer: {
          flexbox: "no-2009",
        },
        stage: 3,
        features: {
          "custom-properties": false,
        },
      },
    ],
    [
      // configure PurgeCSS
      "@fullhuman/postcss-purgecss",
      {
        content: ["./src/app/**/*.{js,jsx,ts,tsx}"],
        defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
        safelist: {
          standard: ["html", "body"],
        },
      },
    ],
  ],
}; 
No.2477
02/29 17:46

edit

React hooksの概要 useContext による Provider

1. Providerコンポーネント /src/providers/MyValueProvider.tsx

'use client';

import React, {
  createContext,
  useContext,
  useState,
  ReactNode,
  Dispatch,
  SetStateAction,
} from 'react';

type Myvalue = 'ja' | 'en';

const initialValue = 'ja';

// 値を保持するコンテキスト
export const MyvalueContext = createContext<Myvalue>(initialValue);

// 値を更新する関数を提供するコンテキスト
const MyvalueDispatcherContext = createContext<
  Dispatch<SetStateAction<Myvalue>> | undefined
>(undefined);

interface MyvalueProviderProps {
  children: ReactNode;
}

const MyvalueProvider: React.FC<MyvalueProviderProps> = ({ children }) => {
  const [myvalue, setMyvalue] = useState<Myvalue>(initialValue);

  return (
    <MyvalueContext.Provider value={myvalue}>
      <MyvalueDispatcherContext.Provider value={setMyvalue}>
        {children}
      </MyvalueDispatcherContext.Provider>
    </MyvalueContext.Provider>
  );
};

// 値のカスタムフック
const useMyvalue = () => useContext(MyvalueContext);

// 値を更新する関数用のカスタムフック
const useMyvalueDispatcher = () => {
  const context = useContext(MyvalueDispatcherContext);
  if (context === undefined) {
    throw new Error(
      'useMyvalueDispatcher must be used within a MyvalueProvider'
    );
  }
  return context;
};

export { MyvalueProvider, useMyvalue, useMyvalueDispatcher };

2. 上位層のコンポーネント(reactなら App.tsx など)(next.js なら /app/page.tsx や /app/layout.tsx など)

{children}

  ↓

<MyvalueProvider>{children}</MyvalueProvider>

3. コンテキストを受け取るコンポーネント ( src/app/ClientCommponent.tsx )

'use client';

import { FC } from 'react';
import { MyvalueSub1 } from './MyvalueSub1';
import { MyvalueSub2 } from './MyvalueSub2';
import { MyvalueButton } from './MyvalueButton';

export const ClientCommponent: FC = () => {
  return (
    <div>
      <h1>ClientCommponent</h1>
      <MyvalueButton />
      <MyvalueSub1 />
      <MyvalueSub2 />
    </div>
  );
};

4. コンテキストの値を表示するコンポーネント ( src/app/MyvalueSub1.tsx )

'use client';

import { FC } from 'react';
import { MyvalueContext } from '@/providers/MyvalueProvider';

export const MyvalueSub1: FC = () => {
  return (
    <div style={{ border: '10px solid red', padding: '10px', margin: '10px' }}>
      <h1>MyvalueSub1</h1>
      <MyvalueContext.Consumer>
        {(value) => {
          return <div>{value}</div>;
        }}
      </MyvalueContext.Consumer>
    </div>
  );
};

5. コンテキストの値を表示するコンポーネント ( src/app/MyvalueSub2.tsx )

'use client';

import { FC, useContext } from 'react';
import { MyvalueContext } from '@/providers/MyvalueProvider';

export const MyvalueSub2: FC = () => {
  const value = useContext(MyvalueContext);

  return (
    <div
      style={{ border: '10px solid orange', padding: '10px', margin: '10px' }}
    >
      <h1>MyvalueSub2</h1>
      <div>{value}</div>
    </div>
  );
};

6. コンテキストの値更新するボタンコンポーネント ( src/app/MyvalueButton.tsx )

'use client';

import { useMyvalueDispatcher } from '@/providers/MyvalueProvider';
import { FC } from 'react';

export const MyvalueButton: FC = () => {
  const setMyvalue = useMyvalueDispatcher();
  const handleClick = () => {
    setMyvalue(new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
  };
  return (
    <button
      style={{ background: 'lightgray', padding: '10px', margin: '10px' }}
      onClick={handleClick}
    >
      MyvalueButton
    </button>
  );
};

6. プロバイダで囲まれたコンポーネントは全て最レンダリングがかかるので memo化しましょう

MyvalueProvider 以下のコンポーネントはコンテキストが変更されると全て最レンダリングされるので、以下のようにmemoしておきます。

export const Footer: FC = () => {
  return (
      <h1>Footer</h1>
  );
};

  ↓

export const Footer: FC = memo(() => {
  return (
      <h1>Footer</h1>
  );
});
Footer.displayName = 'Footer';
No.2473
03/29 08:59

edit

アプリが bun で 動いているのか node.js で動いているのかを判別する

アプリが bun で 動いているのか node.js で動いているのかを判別する

const bunVersion = process.versions.bun ?? ""
No.2469
02/17 21:19

edit

react で i18n

npm init vite@latest i18n-ts-app
npm i react-i18next
npm i i18next
npm i i18next-browser-languagedetector
cd src
mkdir i18n
vi i18n/configs.ts

src/i18n/configs.ts

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import translation_en from './en.json';
import translation_ja from './ja.json';

const resources = {
  ja: {
    translation: translation_ja,
  },
  en: {
    translation: translation_en,
  },
};

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: 'ja',
    interpolation: {
      escapeValue: false,
    },
    debug: process.env.NODE_ENV !== 'production', // production 以外の時は debug モードにする
  });

export default i18n;

src/i18n/ja.json

{
  "header": {
    "アカウント": "アカウント(日本語)"
  }
}

src/i18n/en.json

{
  "header": {
    "アカウント": "Account(English)"
  }
}

main.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import './i18n/configs'; //i18n設定

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

src/App.tsx

import './App.css';
import { useTranslation } from 'react-i18next';

function App() {
  const { t, i18n } = useTranslation();

  return (
    <>
      <h1> 翻訳された文字列が表示されます</h1>
      <div>{t('header.アカウント')}</div>
      <div>現在の言語: {i18n.language}</div>
    </>
  );
}

export default App;
No.2468
02/16 17:00

edit

Redux のフック

● ざっくりRedux

1. ディスパッチ = アクションをストアに送信する

  • ディスパッチは実行したい「関数」そのものではなく、「アクションオブジェクト」をストアに送信します。
  • アクションオブジェクトには、実行したい「関数」を特定するための情報(type プロパティなど)を含めます。

2. ストアはディスパッチを受け取り、リデューサーへ処理を渡す

  • ストアはディスパッチされたアクションを受け取り、そのアクションの type に基づいて、適切なリデューサーに処理を渡します。
  • リデューサーはアクションの内容を元に新しい状態を計算します。

3. リデューサーが状態を更新する

  • リデューサーは純粋な関数で、アクションに基づいて状態を計算します。
  • リデューサー自体は実行可能な関数ですが、副作用のない状態変換ロジックのみを持っています。(純粋関数
  • リデューサーが実行され、新しい状態がストアに保存されます。

● 純粋関数とは

純粋な関数(Pure Function)は、副作用のない関数を指します。これには次の2つの特性があります。

1. 同じ入力に対して必ず同じ結果を返す
 ・入力値が変わらない限り、常に同じ出力を返します。(参照透過性)
 ・例: const add = (a, b) => a + b;

2. 副作用を持たない
 ・関数外の状態に影響を与えたり、関数外の状態から影響を受けたりしません。
 ・例: グローバル変数を変更しない、APIコールやファイルシステムの操作をしない

● 副作用とは

プログラムにおける副作用は、関数が以下のような状態変化や外部とのやりとりを行うことを指します。

1. 外部データの読み書き
 ・データベース、ファイルシステム、ネットワーク通信、ブラウザのローカルストレージなど。
 ・例: ファイルを書き込む、APIからデータを取得する
2. グローバル変数や外部状態の変更
 ・関数外の変数を変更すること。
 ・例: グローバルなカウンタ変数をインクリメントする
3. ユーザーインターフェースの操作
 ・DOMの変更やアラート表示など。
 ・例: ユーザーにアラートを表示する、DOMの要素を直接操作する
4. ランダム性の導入
 ・関数の結果がランダムである場合。
 ・例: Math.random() のような乱数生成
5. 時間に依存する要素
 ・関数の結果が時間に依存する場合。
 ・例: Date.now() を使う

● Redux を調べているとスライス(createSlice)が登場するがこれは何?

createSlice → 便利な関数です。

createSlice を使用すると、アクションタイプ、アクションクリエーター、リデューサーを一つのファイルにまとめることができ、コードが簡潔になります。

● Redux のフック

useSelector

  • 用途: Redux ストアの状態からデータを選択(抽出)するために使用します。
  • 使い方: useSelector(selectorFunction) は、selectorFunction を使用して Redux ストアの状態から特定の部分を選択します。このフックを使用すると、コンポーネントは選択した状態の部分が更新されるたびに再レンダリングされます。

useDispatch

  • 用途: Redux ストアにアクションをディスパッチ(運ぶ)するために使用します。
  • 使い方: const dispatch = useDispatch() でフックを使用して、dispatch 関数を取得します。この関数を通じてアクションをディスパッチすることができます(例: dispatch({ type: 'ACTION_TYPE' }))。

useStore

  • 用途: コンポーネント内で直接 Redux ストアにアクセスするために使用します。
  • 使い方: const store = useStore() でフックを使用して、Redux ストアのインスタンスを取得します。これは、主に現在のストアの状態へのアクセスや、ストアの dispatch 関数の使用を目的としていますが、通常は useSelectoruseDispatch によってカバーされるため、useStore の使用は限定的です。

useActions (Redux Toolkit)

  • 用途: アクションクリエーターを自動的にディスパッチ関数にバインドするために使用されます。このフックは Redux Toolkit の bindActionCreators ユーティリティに基づいており、特定のアクションクリエーターを簡単に使用できるようにします。
  • 使い方: このフックは Redux Toolkit の標準機能ではありませんが、bindActionCreators を使用してカスタムフックとして簡単に作成できます。アクションクリエーターをディスパッチ関数にバインドし、その結果として得られるバインドされたアクションクリエーター関数をコンポーネントから直接呼び出すことができます。

● Redux の createAction / createReducer / createSlice

https://zenn.dev/luvmini511/articles/819d8c7fa13101

● Redux の reselect と createSelector

https://zenn.dev/forcia_tech/articles/20220428_hatano_redux_reselect

● Redux-Thunk (または Redux-Saga)の非同期処理

Redux-Thunkで非同期処理ができる仕組みを理解しよう #React - Qiita

unwrap() して使用します

const resultAction = await dispatch(fetchUserData(userId));
const user = unwrapResult(resultAction);

または

const resultAction = await dispatch(fetchUserData(userId)).unwrap();
No.2466
09/13 11:16

edit

react + typescript で GoogleMap

yarn add @vis.gl/react-google-maps

.env

NEXT_PUBLIC_GOOGLE_MAP_API_KEY=<YOUR-GOOGLE-MAP-API-KEY>

GoogleMap.tsx

"use client";

import { FC } from "react";
import { APIProvider, Map, Marker } from "@vis.gl/react-google-maps";

export const GoogleMap: FC = () => {
  const container = {
    width: "75%",
    height: "500px",
  };

  const position = {
    lat: 35.015312546648474,
    lng: 135.7650548364636,
  };

  return (
    <APIProvider apiKey={process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY}>
      <Map center={position} zoom={15}>
        <Marker position={position} />
      </Map>
    </APIProvider>
  );
};

https://cloud.google.com/blog/ja/products/maps-platform/introducing-react-components-for-the-maps-javascript-api

No.2460
01/30 17:03

edit

React Hook Form で Input argument is not an HTMLInputElement エラーが出る場合

React Hook Form で textarea(DOMネイティブ)を使用していても
「 Input argument is not an HTMLInputElement エラー」が出ることがあります。
その場合はLastPass 拡張機能を疑いましょう。

Google Chrome  → 拡張機能を管理 → LastPassを削除

以上です。

No.2438
01/11 09:55

edit

zodで最大文字数チェックの時に「現在 xx文字です」を表示させる

● zodで最大文字数チェックの時に「現在 xx文字です」を表示させる

max() の代わりに refine() を使用して 動的なメッセージを表示します。

  .max(15, { message: "ユーザー名は最大15文字です。" })

  ↓

    .refine(
      (arg: string) => arg.length <= 15,
      (arg: string) => ({
        message: `ユーザー名は最大15文字です。現在 ${arg.length} 文字使用しています。`,
      }),
    ),
No.2437
01/11 08:45

edit

jest実行時に @/ のパスを解決する

jest.config.js

moduleNameMapper を以下のように追加します

module.exports = {
  preset: "ts-jest",
  .......
  moduleNameMapper: {
    "^@/(.+)$": "<rootDir>/src/$1", // jest実行時に '@/'を解決
  },
}
No.2433
12/23 13:05

edit

zodで userName の値が正しい場合に、ユニークであるかをさらに検証する

● zodで userName の値が正しい場合に、ユニークであるかをさらに検証する

src/PATH/TO/YOUR/COMPONENT/validations/userNameSchema.ts

import { z } from "zod"
import debounce from "lodash/debounce"

/**
 * ユーザー名がユニークかどうかを検証する
 */
const isUserNameUnique = async (userName: string): Promise<boolean> => {
  // TODO: 実際の検証ロジックを書くこと
  console.log(`● isUserNameUnique (${userName})`)
  await new Promise((resolve) => setTimeout(resolve, 500))

  return userName === "aaaaa" ? false : true
}

/**
 * ユーザー名がユニークかどうかを検証する(debounced)
 */
const debouncedIsUserNameUnique = debounce(isUserNameUnique, 500)

/**
 * userName 入力時に検証するスキーマ
 */
const inputValidationSchema = z
  .string()
  .min(5, { message: "ユーザー名は最低5文字必要です。" })
  .max(15, { message: "ユーザー名は最大15文字です。" })
  .refine((userName) => /^[A-Za-z0-9_]+$/.test(userName), {
    message: "ユーザー名には文字、数字、アンダースコア(_)のみ使用できます。",
  })

export const userNameSchema = z.object({
  userName: inputValidationSchema.refine(
    async (userName) => {
      // 「ユーザー名がユニークかどうかの検証」前のバリデーションでエラーがある場合は true を返して中断してこのバリデーションエラーは表示させない
      const result = inputValidationSchema.safeParse(userName)
      if (!result.success) return true

      // ユーザー名がユニークかどうかを検証する
      const isUnique = await debouncedIsUserNameUnique(userName)
      return isUnique ? true : false
    },
    {
      message: "このユーザー名は既に使用されています。",
    },
  ),
})


export type UserNameSchema = z.infer<typeof userNameSchema>

コンポーネントでは特別に処理を書く必要はありません

MyComponent.tsx

  // react-hook-form
  const formOptions = {
    resolver: zodResolver(userNameSchema),
  }
  const {
    register,
    formState: { errors },
    handleSubmit,
  } = useForm<UserNameSchema>(formOptions)

  const onSubmit: SubmitHandler<UserNameSchema> = (data) => {
    console.log(data)
  }
No.2432
12/23 17:43

edit

Cypressより早いE2Eテストフレームワーク Playwright

● Playwrightのインストール

npm init playwright@latest

● 1. 実際にブラウザを操作してブラウザ操作を記録

npx playwright codegen

起動するとブラウザが立ち上がり、すでに録画状態になっています。
テストしたいウェブサイトにアクセスして様子をクリックしたりするとInspector 画面にコードが書き出されます。

● 1. 実際にブラウザを操作してブラウザ操作を記録(Cookieなどストレージを指定)

cookies.json

{
  "cookies": 
	[
	  {
	    "name": "cookie_name",
	    "value": "cookie_value",
	    "domain": "example.com",
	    "path": "/",
	    "expires": -1,
	    "httpOnly": true,
	    "secure": true,
	    "sameSite": "Lax"
	  }
	]
}
npx playwright codegen --load-storage=cookies.json https://example.com

● 2. テストを新規作成して記録したコードをペースト

ファイル名は好きな命名で良いでしょう。拡張子も同じくです。

vi tests/作成したいテストファイル名.spec.ts

拡張子の設定は以下のようにします。(例 : *.playwright.ts をテストファイルとみなす)

playwright.config.ts

export default defineConfig({
  testMatch: "*.playwright.ts",

● 3. ペーストしたブラウザ操作の最後にテストを追記

要素 xxx が画面に表示されている

await expect(page.locator('h1')).toBeVisible();     // 要素h1が画面に表示されている
await expect(page.locator('h2')).not.toBeVisible(); // 要素h1が画面に表示されていない

ページのタイトルが xxx と完全一致(部分一致)する

await expect(page).toHaveTitle(/のマイページ/); // 部分一致(正規表現オブジェクトで指定)
await expect(page).toHaveTitle('Dashboard'); // 完全一致(文字列で指定)

URLが xxx である

await expect(page.url()).toBe('https://localhost/mypage/');

● 4. テストを実行する

npx playwright test --ui

インストール時に自動的にインストールされる。サンプルテストファイルが実行されます。実行されるのは以下のファイルです。

tests/example.spec.ts
tests/demo-todo-app.spec.ts

● 5. テスト結果の表示

npx playwright show-report

● 6. テストをCLIのみで実行する

3つのブラウザで実行
npx playwright test --project=chromium
Google Chrome のみで実行
npx playwright test --project=chromium

● VRT reg-suit のインストール

npm install -D reg-suit

インストール

npx reg-suit init

実行

npx reg-suit run

● Playwrightのカスタムフィクスチャ

フィクスチャとは何するもの?

フィクスチャとは、テストを実行する際に必要となる前提条件や共通のセットアップを提供する仕組みのことです。

具体的には:

・テスト実行前の環境準備(セットアップ)
・テスト間で共有される共通のコード
・テストデータの準備
・テスト後のクリーンアップ処理(ティアダウン)

を管理する機能です。

・テスト間でデータを共有したり、カスタム実行を作成したりする

No.2425
11/26 09:29

edit

jest で 描画されたコンポーネントをブラウザで確認する jest-preview

● jest-preview のインストール

npm install --save-dev jest-preview

● jest-preview の起動

npx jest-preview

ブラウザが次のURLで自動的に開きます http://localhost:3336/

● テストケースに debug() メソッドを追加

MyComponent.spec.tsx (デバッグしたいjestファイル)

import { debug } from "jest-preview"
    // eslint-disable-next-line testing-library/no-debugging-utils
    debug()

jest を実行する

npm run test MyComponent.spec.tsx

これだけでブラウザに画面が表示されます。 便利!

No.2424
11/24 17:01

edit

Drizzle ORM 次世代 データベース O/R マッパー

● 1. Drizzle ORM のインストール

DBはMySQLを使用してみます。

npm install drizzle-orm mysql2
npm install -D drizzle-kit 
npm install --save-dev dotenv dotenv-cli
npm install ts-node

● 作成するファイルまたはディレクトリ

├── drizzle/
├── drizzle.config.ts
├── tsconfig.cli.json
├── src/
│       ├── db/
│       │       ├── database.ts
│       │       ├── migrate.ts
│       │       ├── schema.ts
│       │       └── seed.ts
├── tsconfig.cli.json

● tsconfig.json の修正

"target": "esnext" に変更します

{
  "compilerOptions": {
    "target": "esnext",

● DBのテーブル定義

src/db/schema.ts

import {
  json,
  serial,
  text,
  boolean,
  datetime,
  timestamp,
  mysqlTable,
  varchar,
} from "drizzle-orm/mysql-core";

type Permission = {
  code: string;
  description: string;
};

export const users = mysqlTable("users", {
  // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  role: varchar("varchar", { length: 16, enum: ["admin", "user"] }),
  verified: boolean("verified").notNull().default(false),
  permissionJson: json("relatedJson").$type<Permission[]>(),
  createdAt: datetime("createdAt"),
  updatedAt: timestamp("updatedAt"),
});

引用 : https://bit.ly/3uyMD1A

● 2. (DBマイグレーション)DBのマイグレーションコマンドの追加

package.json へ 次のコマンドを追加します
NODE_ENV=development を設定して、src/db/database.ts 内に .env.development ファイルから接続情報を読み込むよう定義します

  "scripts": {
    "db:migrate:generate": "NODE_ENV=development  drizzle-kit generate:mysql",
    "db:migrate:execute": "NODE_ENV=development  ts-node --project tsconfig.cli.json ./src/db/migrate.ts",
    "db:seed": "NODE_ENV=development  ts-node --project tsconfig.cli.json ./src/db/seed.ts",
    "db:studio": "NODE_ENV=development  npx drizzle-kit studio"
  },

tsconfig.cli.json

{
  "compilerOptions": {
    "module": "nodenext"
  },
  // ts-node
  "ts-node": {
    "esm": true,
    "experimentalSpecifierResolution": "node"
  }
}

● 2. (DBマイグレーション)設定ファイルの追加

.env.development

DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=drizzle_sample_db
DB_USERNAME=root
DB_PASSWORD=

src/db/database.ts

NODE_ENV == "development" の場合は .env.development を読み込むようにしています

import { drizzle } from "drizzle-orm/mysql2";
import mysql, { Connection } from "mysql2/promise";
import * as dotenv from "dotenv";

if (process.env.NODE_ENV == "development") {
  dotenv.config({ path: ".env.development" });
} else {
  dotenv.config();
}

if (!("DB_HOST" in process.env)) throw new Error("DB_HOST not found on env");

export const dbCredentials = {
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT, 10),
  user: process.env.DB_USERNAME,
  database: process.env.DB_DATABASE,
  password: process.env.DB_PASSWORD,
  multipleStatements: true,
};

console.log(dbCredentials);

export const getConnection = async (): Promise<Connection> => {
  return mysql.createConnection({
    ...dbCredentials,
  });
};

export const getDb = async (connection: Connection) => {
  return drizzle(connection);
};

drizzle.config.ts

database.ts の接続情報を呼び出しています

import type { Config } from "drizzle-kit";
import { dbCredentials } from "./src/db/database";

export default {
  schema: "./src/db/schema.ts",
  out: "./drizzle/migrations",
  driver: "mysql2", // 'pg' | 'mysql2' | 'better-sqlite' | 'libsql' | 'turso'
  dbCredentials: {
    ...dbCredentials,
  },
} satisfies Config;

● 2. (DBマイグレーション)マイグレーションファイルの生成

npm run db:migrate:generate

● 2. (DBマイグレーション)マイグレーションの実行

src/db/migrate.ts

import "dotenv/config";
import { migrate } from "drizzle-orm/mysql2/migrator";
import { getDb, getConnection } from "./database";

const migrateAsync = async () => {
  const connection = await getConnection();
  const db = await getDb(connection);

  await migrate(db, {
    migrationsFolder: "./drizzle/migrations",
  });

  await connection.end();
};

void migrateAsync();

マイグレーションを実行する

npm run db:migrate:execute

push というコマンドもありますが。。。

drizzle-kit に push というコマンドもありますが、条件によってはマイグレーションとの併用はうまくいかないようです。

drizzle-kit push:mysql

● 3.(DBシード)シードの実行

npm i -D @faker-js/faker

src/db/seed.ts

import { users } from "./schema";
import { faker } from "@faker-js/faker";
import { getConnection, getDb } from "./database";

const seedAsync = async () => {
  const connection = await getConnection();
  const db = await getDb(connection);

  const data: (typeof users.$inferInsert)[] = [];

  for (let i = 0; i < 20; i++) {
    const zeroOrOne = i % 1;
    data.push({
      name: faker.internet.userName(),
      role: ["admin", "user"][zeroOrOne] as (typeof users.$inferInsert)["role"],
      permissionJson: [
        {
          code: "READ",
          description: "read data",
        },
        {
          code: "WRITE",
          description: "write data",
        },
      ],
      verified: true,
    });
  }

  await db.insert(users).values(data);
  await connection.end();
  console.log("✅ seed done");
};

void seedAsync();

シードの実行

npm run db:seed

● dbへの接続

src/trpc/server/database.ts

import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";

const sqlite = new Database("sqlite.db");
export const db = drizzle(sqlite);
No.2423
11/24 11:46

edit

React で Signals を 軽量グローバルストアとして使用する

● 1. Signals のインストール

npm install @preact/signals-core
npm install @preact/signals-react

ついでに id 生成の cuid もインストールしておきます

npm install --save @paralleldrive/cuid2

● 2. Todo ストアの作成

以下をグローバルストアとして公開します

・読み取り専用なtodoのリスト :  todoList
・todoを追加する関数 :  addTodoFromName() 

src/stores/todo.ts

import { createId } from '@paralleldrive/cuid2';
import { signal, computed, ReadonlySignal } from '@preact/signals-react';

interface Todo {
  id: string;
  name: string;
}

const initialState: Todo[] = [
  {
    id: 'id001',
    name: 'hoge',
  },
  {
    id: 'id002',
    name: 'fuga',
  },
];

const privateTodoList = signal<Todo[]>(initialState);

const todoList: ReadonlySignal<Todo[]> = computed(() => privateTodoList.value);

const addTodoFromName = (name: string) => {
  if (name === '') return;

  const newTodo: Todo = {
    id: createId(),
    name: name,
  };
  privateTodoList.value = [...privateTodoList.value, newTodo];
};

export { todoList, addTodoFromName };

● 3. Todoコンポーネントの作成

hooks は登場しません。 import するだけです

src/components/SignalsTodoComponent.tsx

import { FC, useRef } from 'react';
import { todoList, addTodoFromName } from '../stores/todo';

export const SignalsTodoComponent: FC = () => {
  const ref = useRef<HTMLInputElement>(null);

  const addTodoToList = () => {
    if (!ref.current) return;

    addTodoFromName(ref.current.value);
    ref.current.value = '';
  };

  return (
    <div style={{ border: '1px solid red' }}>
      <ul>
        {todoList.value.map((v) => {
          return (
            <li key={`key__${v.id}`}>
              ({v.id}) : {v.name}
            </li>
          );
        })}
      </ul>

      <input type="text" ref={ref} />
      <button type="button" onClick={addTodoToList}>
        追加
      </button>
    </div>
  );
};

● 4. ステートを別のコンポーネントで利用する

ヘッダコンポーネントからストアの値を参照してみます。

src/components/Header.tsx

import { FC } from 'react';
import { todoList } from '../stores/todo';

export const Header: FC = () => {
  return <h1>Header Todoの数は({todoList.value.length})です</h1>;
};

● 5. 表示してみる

src/App.tsx

import './App.css';
import { Header } from './components/Header';
import { SignalsTodoComponent } from './components/SignalsTodoComponent';

function App() {
  return (
    <>
      <Header />
      <SignalsTodoComponent />
    </>
  );
}

export default App;
添付ファイル1
No.2419
11/29 09:46

edit

添付ファイル

scaffdog

1. scaffdogのインストール

npm install scaffdog

2. フォルダ .scaffdog の 中に設定ファイルを作成する

mkdir .scaffdog
vi .scaffdog/config.js

config.js

export default {
  files: ['*'],
}
vi .scaffdog/mycomponent.md

テンプレートの作成

---
name: 'mycomponent'
description: 'React component を生成します'
root: 'src'
output: '**/*'
ignore: []
---

# Variables(自由に書き換えてください)

**コンポーネント名**
- component_name: MyComponent



# `{{ component_name }}.tsx`

\```tsx

import { FC, ReactNode } from "react"

type Props = {
  title: string;
  onclickfunction: () => void;
  children: ReactNode;
}

export const Child: FC<Props> = (props) => {
  return (
    <div>
      <h1>{{ component_name }}!!!</h1>
      this is {{ component_name }} Component
      <button onClick={props.onclickfunction}>ボタン</button>
    </div>
  );
};
\```

3. 実行する

package.json

  "scripts": {
    "scaffdog:generate": "scaffdog generate --force",
  },

テンプレートの name を指定して実行します

npm run scaffdog:generate --output "mycomponent" 
No.2418
11/15 17:07

edit

初期化変数の変更に追従する useState → useSyncedState

src/hooks/useSyncedState.ts

import { Dispatch, SetStateAction, useEffect, useState } from 'react';

export function useSyncedState<T>(
  initialValue: T
): [T, Dispatch<SetStateAction<T>>] {
  const [state, setState] = useState(initialValue);

  useEffect(() => {
    setState(initialValue);
  }, [initialValue]);

  return [state, setState];
}

使い方は useState と同じです。

No.2414
07/25 14:09

edit

ビューポートの高さと特定の要素のビューポート上端からの相対的なY座標の両方を返すカスタムフックを作成する

import { useState, useEffect, useRef, RefObject } from 'react';

interface ViewportPositionHook {
  elementRef: RefObject<HTMLDivElement>;
  relativeY: number | null;
  viewportHeight: number;
}

function useViewportPosition(): ViewportPositionHook {
  // 要素のrefを保持するためのstate
  const elementRef = useRef<HTMLDivElement>(null);
  // 要素のビューポートに対する相対Y座標を保持するためのstate
  const [relativeY, setRelativeY] = useState<number | null>(null);
  // ビューポートの高さを保持するためのstate
  const [viewportHeight, setViewportHeight] = useState<number>(window.innerHeight);

  const updatePosition = () => {
    // ビューポートの高さを更新
    setViewportHeight(window.innerHeight);

    if (elementRef.current) {
      // ビューポートに対する要素の相対位置を取得
      const rect = elementRef.current.getBoundingClientRect();
      // ビューポートの上端からの相対的なY座標をstateにセット
      setRelativeY(rect.top);
    }
  };

  useEffect(() => {
    // リサイズイベントにハンドラーを追加
    window.addEventListener('resize', updatePosition);
    // リサイズイベントが発生した際に位置を更新
    updatePosition();

    // コンポーネントのアンマウント時にイベントリスナーをクリーンアップ
    return () => window.removeEventListener('resize', updatePosition);
  }, []);

  return { elementRef, relativeY, viewportHeight };
}

// 使用例
const MyComponent: React.FC = () => {
  const { elementRef, relativeY, viewportHeight } = useViewportPosition();

  return (
    <div>
      <div ref={elementRef}>この要素のY座標</div>
      {relativeY !== null && (
        <p>この要素のビューポート上端からの相対的なY座標: {relativeY}px</p>
      )}
      <p>ビューポートの高さ: {viewportHeight}px</p>
    </div>
  );
}

export default MyComponent;

No.2413
11/09 15:15

edit

マウント時には実行しない useUpdateEffect

● useUpdateEffect

/hooks/useUpdateEffect.ts

import { useEffect, useRef, DependencyList, EffectCallback } from "react"

const useUpdateEffect = (
  effect: EffectCallback,
  dependencies?: DependencyList
) => {
  const isInitialMount = useRef<boolean>(true)

  useEffect(() => {
    if (isInitialMount.current) {
      isInitialMount.current = false
    } else {
      return effect()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, dependencies)
}

export default useUpdateEffect

● useUpdateEffect の 使い方

 /*
   * useUpdateEffect
   * (指定の変数 input 変更時)(アンマウント時)に実行
   */
useUpdateEffect(() => {
  // 実行したい処理

  return () => {
    // クリーンアップの処理
  }

  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [input])
No.2412
11/09 10:47

edit

shadcn コンポーネントを使用する

● shadcnのインストール

npx shadcn-ui@latest init

● shadcnのコンポーネントの中から使いたいコンポーネントをそれぞれインストールする

例えばボタンをインストールする場合は次のコマンドを実行します

npx shadcn-ui@latest add button

shadcnはnpmパッケージとして提供されてません。 つまり依存関係としてインストールする必要はありません。 直接指定したディレクトリにインストールされるという形になります

● パスを追加する

・1. tsconfig に以下を追加します

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

・2. Vite で react を使用している場合は次のようにします

vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'), // '@' は 'src' フォルダを指すように設定
    },
  },
  plugins: [react()],
});

● Button コンポーネントを使用する

以下のように呼び出して使用します

import './App.css';
import { Button } from '@/components/ui/button';

function App() {
  return (
    <div>
      <Button>test</Button>
    </div>
  );
}

export default App;

● shadcnのインストールディレクトリを変更する

以下のように変更してからインストールコマンドを実行すると新しい場所にインストールされます。 (古いコンポーネントを手動で削除します。)

components.json

  "aliases": {
    "components": "@/components/shadcn",
    "utils": "@/components/shadcn/lib/utils"
  }

● Storybook で Button コンポーネントをカスタマイズして使用する

・ 1. .storybook/preview.ts に css を適用する

import '../src/index.css'; // Tailwind CSS のスタイルシートへのパス
No.2406
10/26 16:09

edit

コンポーネント開発速度を加速させるStorybookをReact に 追加する

● Storybook の インストール

npx storybook@latest init

● Storybook を起動する

npm run storybook

● ./src/stories を削除する

自動で /src/stories にファイルが生成されるので不要な場合は削除しましょう。

● StorybookのHTMLでグローバルcssを読み込ませる

.storybook/preview.ts の 1行目に以下のインポート文を追加するだけでOKです

import "../src/app/globals.css"

● 有名な プロダクトの Storybookを参考にする

https://storybook.js.org/showcase

● add-onを活用する

・ @storybook/addon-essentials

(Essential addons には以下のアドオンが内包されています。)

Docs	ストーリー定義を元に、ドキュメントを自動生成する
Controls	コンポーネントに対するパラメータ(≒props)をGUI上で動的に切り替える
Actions	コンポーネントから発火されたイベント(≒emit)をロギングする
Viewport	様々な画面サイズでストーリーを描画できるようにする
Backgrounds	背景色を変更する
Toolbars & globals	グローバルパラメータの埋め込みと、それを設定するツールバーを作成する

引用 : https://zenn.dev/sa2knight/books/aca5d5e021dd10262bb9/viewer/1c420c

・ @storybook/addon-links

(Storybook Linksアドオンは、Storybookのストーリー間をナビゲートするリンクを作成するために使用できます。)  

https://github.com/storybooks/storybook/tree/master/addons/links  

・ @storybook/addon-interactions

(play() 関数によるインタラクションを Storybook 上のタブで確認するためのアドオンです。)

https://github.com/storybookjs/storybook

・ storybook-react-i18next

(storybookの i18n 対応)

https://github.com/stevensacks/storybook-react-i18next

・ @storybook/addon-viewport

(Storybook を活用しているが、モバイル用のデザインもデフォルトではレスポンシブな表示形式になるため確認が手間。)

https://zenn.dev/ryuta1346/articles/230ea8b7a5ce7c

No.2405
06/05 10:46

edit

Reactなのに EventEmitter 使うパターン

● eventemitter3 のインストール

useState や useImperativeHandle を使わずに、コンポーネントから別のコンポーネントの関数を実行したい場合に EventEmitter を使うというパターンがあります。

npm install eventemitter3

EventEmitterParentComponent.tsx

import { FC } from 'react';
import { EventEmitterChild1Component } from './EventEmitterChild1Component';
import { EventEmitterChild2Component } from './EventEmitterChild2Component';
import EventEmitter from 'eventemitter3';

export const emitter = new EventEmitter();

export const EventEmitterParentComponent: FC = () => {
  return (
    <div>
      <EventEmitterChild1Component />
      <EventEmitterChild2Component />
    </div>
  );
};

ParentComponent と命名していますが、親子関係は全く関係ありません。
アプリのどこかで

import EventEmitter from 'eventemitter3';
export const emitter = new EventEmitter();

できていればokです。

EventEmitterChild1Component.tsx

import { FC, useEffect } from 'react';
import { emitter } from './EventEmitterParentComponent';

export const EventEmitterChild1Component: FC = () => {
  const showAlert = () => {
    alert('発火しました!');
  };

  useEffect(() => {
    emitter.on('eventName', showAlert);

    // cleanup
    return () => {
      emitter.off('eventName', showAlert);
    };
  });

  return <div>EventEmitterChild1Component</div>;
};

EventEmitterChild2Component.tsx

import { FC } from 'react';
import { emitter } from './EventEmitterParentComponent';

export const EventEmitterChild2Component: FC = () => {
  const handleClick = () => {
    emitter.emit('eventName');
  };

  return (
    <div>
      <button onClick={handleClick}>ボタンです</button>
    </div>
  );
};

オプションなど詳しくはこちら
https://qiita.com/twrcd1227/items/e03111230dad483129ab

● イベントが登録されているかどうかを調べる

const EventEmitter = require('events');
const emitter = new EventEmitter();

// テスト用のイベントリスナーを登録
emitter.on('testEvent', () => console.log('test'));

// 方法1: listenerCount() - リスナーの数を取得
const count = emitter.listenerCount('testEvent');
console.log('Listener count:', count);  // 1

// 方法2: listeners() - リスナー関数の配列を取得
const listeners = emitter.listeners('testEvent');
console.log('Has listeners:', listeners.length > 0);  // true

// 方法3: eventNames() - 登録済みイベント名の配列を取得
const events = emitter.eventNames();
console.log('Has event:', events.includes('testEvent'));  // true

// 実践的な使用例
function safeEmit(emitter, eventName, ...args) {
  if (emitter.listenerCount(eventName) > 0) {
    emitter.emit(eventName, ...args);
    return true;
  }
  return false;
}

// 使用例
const wasEmitted = safeEmit(emitter, 'testEvent');  // true
const noListener = safeEmit(emitter, 'nonexistent'); // false

● EventEmitterとは

https://tinyurl.com/248kr4oy

● Typesafe Event Emitter

https://typescript-jp.gitbook.io/deep-dive/main-1/typed-event

No.2404
11/08 19:51

edit

React の jest で x秒待つ

● React の jest で x秒待つ

2秒待ちます

    await act(async () => {
      await new Promise((resolve) => setTimeout(resolve, 2000))
    })
No.2402
10/20 08:43

edit

mswでreactアプリのモック

● reactアプリの作成( msw-ts-app というサンプルアプリを作成します )

npm init vite@latest msw-ts-app
cd msw-ts-app
npm install

● mswのインストール

npm i -D msw

● モックの定義

・1. URLとそれに対する処理を紐づける handlers.ts を作成する

mkdir src/mocks
vi src/mocks/handlers.ts

handlers.ts を以下の内容で保存する

import { rest } from 'msw';
import mockUser from 'mocks/resolvers/mockUser';

const handlers = [
  rest.get('/users/', mockUsers),
  rest.get('/users/:id', mockUser),
];

export default handlers;

・2. 処理内容を記述する resolvers/xxxxx.ts を作成する

・2-1 Userの型を作成する

mkdir src/models
vi src/models/User.ts

以下の内容で保存

export type User = {
  id: number;
  username: string;
  age: number;
};

・2-2 mockUserリゾルバを作成する

mkdir src/mocks/resolvers
vi src/mocks/resolvers/mockUser.ts

mockUser.ts を以下の内容で保存する

import { ResponseResolver, MockedRequest, restContext } from 'msw';
import { User } from '../../models/User';

const users: User[] = [
  {
    id: 1,
    username: '山田 太郎',
    age: 25,
  },
  {
    id: 2,
    username: '斎藤 次郎',
    age: 37,
  },
  {
    id: 3,
    username: '山田 花子',
    age: 41,
  },
];

const mockUser: ResponseResolver<MockedRequest, typeof restContext> = (
  req,
  res,
  ctx
) => {
  // パスパラメーターの取得
  const { id } = req.params;
  const user: User | undefined = getMockUserData(Number(id));
  return res(ctx.json(user));
};

const getMockUserData = (id: number): User | undefined => {
  // idでusersを検索
  const user = users.find((user) => user.id === id);
  return user;
};

export default mockUser;

続けて mockUsers.ts を以下の内容で保存する

vi src/mocks/resolvers/mockUser.ts
import { ResponseResolver, MockedRequest, restContext } from 'msw';
import { User } from '../../models/User';

const users: User[] = [
  {
    id: 1,
    username: '山田 太郎',
    age: 25,
  },
  {
    id: 2,
    username: '斎藤 次郎',
    age: 37,
  },
  {
    id: 3,
    username: '山田 花子',
    age: 41,
  },
];

const mockUser: ResponseResolver<MockedRequest, typeof restContext> = (
  req,
  res,
  ctx
) => {
  // クエリパラメーターの取得
  const perPage = req.url.searchParams.get('perPage');
  const user: User[] = getMockUserList(Number(perPage));
  return res(ctx.json(user));
};

const getMockUserList = (perPage: number): User[] => {
  return users.slice(0, perPage);
};

export default mockUser;

・3. サービスワーカーを設定する browser.ts を作成する

vi src/mocks/browser.ts

browser.ts を以下の内容で保存する

import { setupWorker } from 'msw';
import handlers from 'mocks/handlers';

const worker = setupWorker(...handlers);

export default worker;

・4. main.tsx に mswを追加する

src/main.tsx に以下を追加

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';

// msw(追加)
import worker from './mocks/browser';

// msw(追加)
if (process.env.NODE_ENV === 'development') {
  void worker.start();
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

・5. サービスワーカーを public ディレクトリに生成する

次の準備されているコマンドを実行するだけでOKです

npx msw init public/ --save

これで準備が整いました。

● アプリを起動して msw の挙動を確認する

1. msw起動の確認

npm run dev

http://localhost:5174/ へアクセスして、ブラウザの console を確認する

[MSW] Mocking enabled.

と表示されていれば起動は成功しています。

2. 実際に reactアプリでデータを取得して表示する。

vi src/useUser.ts
import { useEffect, useState } from 'react';
import { User } from './models/User';

export function useUser(id: number) {
  const [data, setData] = useState<User | undefined>(undefined);

  useEffect(() => {
    const fetchDataAsync = async () => {
      try {
        const url = `/users/${id}`;
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      } catch (e) {
        throw new Error('fetch error');
      }
    };

    fetchDataAsync();
  }, [id]);

  return { data };
}
vi src/useUsers.ts
import { useEffect, useState } from 'react';
import { User } from './models/User';

export function useUsers(perPage: number) {
  const [users, setUsers] = useState<User | undefined>(undefined);

  useEffect(() => {
    const fetchDataAsync = async () => {
      try {
        const url = `/users/?perPage=${perPage}`;
        const res = await fetch(url);
        const users = await res.json();
        setUsers(users);
      } catch (e) {
        throw new Error('fetch error');
      }
    };

    fetchDataAsync();
  }, [perPage]);

  return { users };
}

App.tsx を以下のように変更する

import { useRef, useState } from 'react';
import './App.css';
import { useUser } from './useUser';
import { useUsers } from './useUsers';

function App() {
  const [id, setId] = useState<number>(1);
  const { data } = useUser(id);
  const { users } = useUsers(2);
  const ref = useRef<HTMLInputElement>(null);

  const handleGetUser = () => {
    if (ref.current?.value) setId(Number(ref.current.value));
  };

  return (
    <>
      <h1>app</h1>
      <hr />
      <pre style={{ textAlign: 'left' }}>{JSON.stringify(users, null, 2)}</pre>
      <hr />
      <div>
        id : <input type="text" ref={ref} onChange={handleGetUser} />
      </div>
      <pre style={{ textAlign: 'left' }}>{JSON.stringify(data, null, 2)}</pre>
    </>
  );
}
export default App;

添付ファイル1
app.png ( 13.8 KBytes ) ダウンロード
No.2393
09/07 10:47

edit

添付ファイル

ジェネレーター(自動生成)好きにぴったりのパッケージ hygen

● hygen

https://www.hygen.io/

対抗馬としてはplopscaffdog というジェネレーターもあります

● hygen の特徴

テンプレートが ejs 形式です。ejsが苦手な場合はおすすめ致しません。

● hygen のインストール

npm install --save-dev hygen
npx hygen init self

● 自作の hygen 自動生成コマンドを作成する

hygen <好きな名前> new

と言うコマンドを作成することができます。

● hygen helloworld new 「A. 自動生成コマンド」を作成する

npx hygen generator new helloworld

_templates/helloworld/new/hello.ejs.t が生成されます。

● hygen helloworld new 「A. 自動生成コマンド」を実行する

npx hygen helloworld new

app/hello.js が自動生成されます。

● hygen helloworld new 「B. 対話型コマンド」を作成する

・1. ファイルを生成

npx hygen generator with-prompt helloworld

_templates/helloworld/with-prompt/hello.ejs.t
_templates/helloworld/with-prompt/prompt.js

が生成されます。

・2. ディレクトリ名「with-prompt」を変更

with-prompt を 短い名前に変更しておくと、実行時に入力が楽になります。

・3. テンプレートを変更

_templates/helloworld/prompt/hello.ejs.t

---
to: app/hello.js
---
const hello = "こんにちは <%= userName %>."

console.log(hello)

_templates/helloworld/prompt/prompt.js

// see types of prompts:
// https://github.com/enquirer/enquirer/tree/master/examples
//
module.exports = [
  {
    type: 'input',
    name: 'userName',
    message: "What's your name?"
  }
]

・4. 自動生成の実行

npx hygen helloworld prompt

app/hello.js が以下の内容で出力されます

const hello = "こんにちは あいうえお."

console.log(hello)

● テンプレートの中で配列を定義する

<%
  myParams = ['param001', 'param002'];
%>
<% ary.forEach(function (v, k) { %>
<p><%= k %>: <%= v %></p>
<% }); %>

● 上書きしますか?プロンプトを表示させずに、強制的に上書きする

コマンドの先頭に HYGEN_OVERWRITE=1 を追加します。

HYGEN_OVERWRITE=1 npx hygen generator new --name foobar

● 大文字、小文字、ケバブケース、キャメルケースなどケースをを変換する

変数 name を変換します

・小文字

<%= name.toLowerCase() %>

・パスカルケース

<%= h.inflection.camelize(name) %>

・キャメルケース

<%= h.inflection.camelize(name, true) %>

・スネークケース

h.changeCase.snakeCase(name)

https://www.hygen.io/docs/templates/

● ある条件の時だけ生成するようにする

to: が null の場合は出力されないのでこれを利用します。

---
to: "<%= redirect ? `/router/routes/${folder.toLowerCase().replace(/ +/g, '')}/routes.redirect.js` : null %>"
unless_exists: true
---

● 変数だけを格納するテンプレートファイルを使う

次のように 他のファイルより、先に読み込ませるためアンダースコア始まりのファイル名を つけたファイルに変数を指定ます。
また to: null としてファイルとしては出力しないようにしておくと、変数だけを格納するテンプレートファイルができます。

_const.ejs.t

---
to: null
---
<%
 // 出力するディレクトリ名入れてください
 outputDir = '05sendFileMessage'
%>
No.2384
11/15 15:23

edit

new hooks in React 18

● new hooks in React 18

useTransition
useDeferredValue
useId
useSyncExternalStore
useInsertionEffect

解説はこちらがよくまとまっています

https://rightcode.co.jp/blog/information-technology/react18-hooks-syain

React18 の useId で a11y対応する

No.2380
08/23 13:03

edit

再レンダリングデバッグの時に有用なコンポーネント

const ChildComponent = () => {
  const id = new Date().getTime();

  return (
    <div>
      <p>{id}</p>
    </div>
  );
};
No.2379
08/02 21:09

edit

zod の string または null または undefined 、 「number または NaN」定義

● string または undefined → optional()

const schema = z.object({
  name: z.string().optional(), // nameフィールドはstringかundefined
});

● string または null → nullable()

const schema = z.object({
  name: z.string().nullable(),  // nameフィールドはstringかnull
});

● string または null または undefined → nullish()

const schema = z.object({
  name: z.string().nullish(),  // nameフィールドはstringかnullかundefined
});

その他スキーマについては 忘れそうなzodスキーマメモ - Qiita

TypeScriptのゾッとする話 ~ Zodの紹介 ~

import * as z from "zod";

z.number();               // 単純な数値( NaNとBigInt型は含まない )
z.number().min(5);        // 5以上の数値( >= 5 )
z.number().max(5);        // 5以下の数値( <= 5 )
z.number().int();         // 整数型の数値
z.number().positive();    // 0よりも大きい数値( > 0 )
z.number().nonnegative(); // 0以上の数値( >= 0 )
z.number().negative();    // 0より小さい数値( < 0 )
z.number().nonpositive(); // 0以下の数値( <= 0 )
No.2372
07/27 14:28

edit

React Hook Form で使用できるコンポーネントを作成する

以下がミニマルな形です。

SimpleSelect.tsx

import { ComponentPropsWithoutRef, forwardRef } from "react"

type Ref = HTMLSelectElement

export type SelectProps = ComponentPropsWithoutRef<"select">

export const SimpleSelect = forwardRef<Ref, SelectProps>(
  ({ name, onChange, ...rest }, ref) => {
    return (
      <select name={name} ref={ref} onChange={onChange} {...rest}>
        <option value="01">北海道</option>
        <option value="02">青森県</option>
        <option value="47">沖縄</option>
      </select>
    )
  }
)
SimpleSelect.displayName = "SimpleSelect"

とりあえず name ,ref , onChange があれば動作します。

また以下のように name ,ref , onChange, onBlur は明示的に記述せずに {...rest} に含めてしまうのもよくやる書き方です

  (rest, ref) => {
    return (
      <select {...rest}>
        <option value="01">北海道</option>
        <option value="02">青森県</option>
        <option value="47">沖縄</option>
      </select>
    )
  }

● register関数 はどのような値に展開されるのか?

React Hook Form で 出てくる register関数 は以下のようなデータが返ってきます。

{ ...register("myName") }

{
	name: 'myName', 
	onChange: ƒ, 
	onBlur: ƒ, 
	ref: ƒ
}

React Hook Form + zodで数値型のselectの空文字対応

No.2371
08/01 20:48

edit

No.2355
05/25 16:50

edit

graphql-codegen を使って、フックを自動生成し tanstack query + graphql クエリを実行する

● graphql-codegen を使って、フックを自動生成し tanstack query + graphql クエリを実行する

npm i -D @graphql-codegen/cli
npm i -D @graphql-codegen/client-preset
npm i -D @graphql-codegen/typescript-react-query

codegen.ts

キャッシュを入れたいので、react-query を使います

import { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "http://localhost:4100/graphql",
  documents: ["./src/graphql/**/*.graphql"],
  ignoreNoDocuments: true, // for better experience with the watcher
  hooks: { afterAllFileWrite: ["prettier --write"] },
  generates: {
    "./src/graphql/generated.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-query",
        {
          add: {
            content: "// generated by graphql-codegen. DO NOT EDIT.",
          },
        },
      ],
      config: {
        fetcher: "fetch",
        // fetcher: {
        //   func: "graphql/custom-fetcher#useFetchData",
        //   isReactHook: true,
        // },
      },
    },
  },
};
export default config;

fetcher の指定でよく使うのは、"fetch", "graphql-request", または

        fetcher: {
          func: "./custom-fetcher#useFetchData",
          isReactHook: true,
        },

です。

・custom-fetcher を指定した場合

src/graphql/custom-fetcher.ts を以下のような内容で作成します (例、next.js + firebaesトークンを自動取得)

import { getAccessToken } from "@/libs/firebaseAuth";

export const useFetchData = <TData, TVariables>(
  query: string,
  options?: RequestInit["headers"],
): ((variables?: TVariables) => Promise<TData>) => {
  return async (variables?: TVariables) => {
    if (!process.env.NEXT_PUBLIC_API_BASE_URL) {
      throw new Error("NEXT_PUBLIC_API_BASE_URL is not set");
    }
    const url = process.env.NEXT_PUBLIC_API_BASE_URL + "/graphql";
    const token = await getAccessToken(true);
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
        ...(options ?? {}),
      },
      body: JSON.stringify({
        query,
        variables,
      }),
    });
    const json = await res.json();
    if (json.errors) {
      const { message } = json.errors[0] || "Error..";
      throw new Error(message);
    }
    return json.data;
  };
};

・gaphqlクエリの設置

src/graphql/queries/UsersFindAll.graphql

query UsersFindAll {
  usersFindAll {
    id
    email
    name
    authProvider
    authId
    createdAt
    updatedAt
  }
}

自動生成の実行

graphql-codegen --config codegen.ts

・コンポーネントでのhooksの使用(fetcher: "fetch"の場合)

  const { data } = useUsersFindAllQuery<UsersFindAllQuery>({
    endpoint: "http://localhost:4000/graphql",
    fetchParams: {
      headers: {
        "content-type": "application/json",
        "Authorization": `Bearer ${token}`,
      },
    },
  });

・コンポーネントでのhooksの使用(fetcher: "graphql/custom-fetcher#useFetchData" の場合)

  const { data } = useUsersFindAllQuery();

・キャッシュを利用する(fetcher: "graphql/custom-fetcher#useFetchData" の場合)

キャッシュ時間を無限にして取得する場合。

  const { data } = useUsersFindAllQuery(
    {},
    { staleTime: Infinity, cacheTime: Infinity },
  );

参考 : https://bit.ly/3WplbNO

No.2345
06/09 11:09

edit

React Hook Form + zod

● React Hook Form + zod のインストール

npm install react-hook-form zod
npm install @hookform/resolvers

● React Hook Form + zod のコンポーネントサンプル

・1. バリデーションの定義を別ファイルにします

src/validations/mysourceCreateSchema.ts

import { z } from "zod";

export const mysourceCreateSchema = z.object({
  source: z.string().nonempty("翻訳したい文章を入力してください"),
  answer: z.string().nullish(),
});

export type MysourceCreateSchema = z.infer<typeof mysourceCreateSchema>;

・2. コンポーネントから呼び出します

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from "@hookform/resolvers/zod";
import { MysourceCreateSchema, mysourceCreateSchema } from "@/validations/mysourceCreateSchema";

const MyForm: React.FC<Props> = ({ onSubmit }) => {
  // react-hook-form
  const { register, formState, handleSubmit } = useForm<MysourceCreateSchema>({
    resolver: zodResolver(mysourceCreateSchema)
  });
  const { errors } = formState;

  const onSubmit: SubmitHandler<MysourceCreateSchema> = (data) => {
    console.log("● data");
    console.log(data);
  };

  return (
    {/* noValidate: htmlのバリデーション機能をオフにする */}
    <form noValidate onSubmit={handleSubmit(handleFormSubmit)}>
      <div>
        <label htmlFor="source">Source</label>
        <input {...register("source")} type="text" />
        {errors.source && <span>{errors.source.message}</span>}
      </div>
      <div>
        <label htmlFor="answer">Answer</label>
        <input {...register("answer")} type="text" />
        {errors.answer && <span>{errors.answer.message}</span>}
      </div>
      <button type="submit" disabled={submitting}>Submit</button>
    </form>
  );
};

● Zodライブラリの型定義

Zodは、TypeScriptで使用するためのスキーマ検証ライブラリです。 このライブラリは、入力値の型を厳密にチェックし、型安全性を向上させます。

Zodライブラリには、次の型定義が含まれています。

z.array:配列の要素に対する型を定義する
z.nullable:nullまたは指定された型の値を許容する
z.optional:undefinedまたは指定された型の値を許容する
z.nullish:nullまたはundefinedの値を許容する

z.literal:特定の値のリテラルを指定する
引数に渡せる値は string | number | bigint | boolean | null | undefined のみとなっています。

z.union:複数の型を許容する
z.nonempty:空ではない配列または文字列を許容する
z.any:任意の値を許容する
z.unknown:不明な値を許容する

これらの型定義は、TypeScriptの型を厳密に制限することで、プログラムの信頼性を高めることができます。

● FieldWrapper を作成してエラーの表示をまとめる

参考 : https://github.com/alan2207/bulletproof-react/blob/master/src/components/Form/FieldWrapper.tsx

import * as React from "react";
import { FieldError } from "react-hook-form";

type FieldWrapperProps = {
  children: React.ReactNode;
  error?: FieldError | undefined;
};

export type FieldWrapperPassThroughProps = Omit<
  FieldWrapperProps,
  "className" | "children"
>;

export const FieldWrapper = (props: FieldWrapperProps) => {
  const { error, children } = props;
  return (
    <>
      {children}
      {error?.message && <div className={"error_input"}>{error.message}</div>}
    </>
  );
};

使い方

<FieldWrapper error={errors.source}>
  <TextField
    {...register("source")}
    multiline={true}
    variant="outlined"
    fullWidth={true}
    placeholder="翻訳したい文章を入力します"
  />
</FieldWrapper>
No.2336
02/05 11:54

edit

react , graphql の Colocated Fragments

⚫︎ Colocated Fragments ( フラグメント コロケーション )

// Avatar.jsx
import gql from 'graphql-tag';

export const Avatar = ({ user }) => {
  return (
    <div>
      <a href={`/user/${user.id}`}>
        <h3>{user.name}</h3>
        <img src={user.image} />
      </a>
    </div>
  );
};

Avatar.fragments = {
  user: gql`
    fragment Avatar on User {
      id
      name
      image
    }
  `
};

https://dev.to/ricardoromox/colocated-fragments-organizing-your-graphql-queries-in-react-24a6

● @graphql-codegen/client-preset

こちらを利用すると、各種ライブラリで同様の実装パターンが実現できます。

GraphQL Code Generator v3 Roadmapで推されているclient-presetを紹介する

npm install -D @graphql-codegen/client-preset

https://the-guild.dev/graphql/codegen/plugins/presets/preset-client

対応ライブラリ

React
@apollo/client (since 3.2.0, not when using React Components ())
@urql/core (since 1.15.0)
@urql/preact (since 1.4.0)
urql (since 1.11.0)
graphql-request (since 5.0.0)
react-query (with graphql-request@5.x)
swr (with graphql-request@5.x)

Vue
@vue/apollo-composable (since 4.0.0-alpha.13)
villus (since 1.0.0-beta.8)
@urql/vue (since 1.11.0)
No.2329
05/07 15:55

edit

apollo client で graphqlからの query の戻り値に型をつける

● apollo client で graphqlからの query の戻り値に型をつける

・ A. 明示的に型を指定する

UserData を指定する

const { error, loading, data } = useQuery<UserData>(QUERY_VIEWER)

・B. apollo client:codegen コマンドを使用する

パッケージのインストール

npm i @apollo/client

npm i @graphql-codegen/add
npm i @graphql-codegen/cli

npm i @graphql-codegen/typescript 
npm i @graphql-codegen/typescript-operations 

参考: GraphQL Code Generator(graphql-codegen) おすすめ設定 for TypeScript
@graphql-codegen/typed-document-node を使ってみた - あ、しんのきです

セットアップを実行する

npx graphql-codegen init

いろいろ質問されるので、以下のように答えます

? What type of application are you building? Application built with React
? Where is your schema?: (path or url) http://localhost:4000/graphql
? Where are your operations and fragments?: 何も入力しない
? Where to write the output: src/__generated__/
? Do you want to generate an introspection file? No
? How to name the config file? codegen.ts
? What script in package.json should run the codegen? codegen
Fetching latest versions of selected plugins...

質問を全て答えると package.json に パッケージが追加されているので

npm install

で 追加パッケージをインストールします。

また

  "scripts": {
    "codegen": "graphql-codegen --config codegen.ts"
  },

も追加されています。

ファイル codegen.ts も自動で追加されています

・ codegen.ts の設定を変更する

・1. 自動生成後に prettier をかけるようにする

const config: CodegenConfig = {
  ...........................
  hooks: { afterAllFileWrite: ['prettier --write'] }
}

・2. 自動生成前に query,mutation,subscription を自動生成

npm i -D gql-generator-node @graphql-tools/load @graphql-tools/graphql-file-loader

・自動生成の実行

npm run codegen

・ 自動生成された型を使用する

import { gql } from "@apollo/client";

        const { data } = await client.query({
          query: gql`
            query {
              find {
                id
                name
              }
            }
          `,
        });
        console.log("● data");
        console.log(data);

 ↓

        const { data } = await client.query({
          query: gql`
            query {
              find {
                id
                name
              }
            }
          `,
        });
        console.log("● data");
        console.log(data);

graphql-codegen 後に prettier をかけたい
GraphQL Code Generator v3 Roadmapで推されているclient-presetを紹介する

No.2313
05/10 11:02

edit

vite-plugin-pwa を使って PWA 対応する

● vite-plugin-pwa

https://github.com/vite-pwa/vite-plugin-pwa
(Reactに 限りません。viteの プラグインなので、Vueなどviteを使用しているアプリで使えます)

● vite-plugin-pwa のインストール

npm i vite-plugin-pwa -D 

● vite-plugin-pwa を読み込む

すでに存在する vite.config.ts に以下の行を追加します

import { VitePWA } from 'vite-plugin-pwa'
import type { VitePWAOptions } from "vite-plugin-pwa";

const pwaOptions: Partial<VitePWAOptions> = {
  manifestFilename: "manifest.webmanifest.json",
  minify: true,
  registerType: "autoUpdate",
  manifest: {
    lang: "ja",
    name: "MY APP",
    short_name: "MyApp",
    scope: "/",
    start_url: "/",
    display: "standalone",
    orientation: "portrait",
    background_color: "#fff",
    theme_color: "#fff",
    icons: [
      {
        src: "icon.png",
        sizes: "512x512",
        type: "image/png",
      },
    ],
  },
};

続けて vite.config.tsplugins に以下を追加します

例: Vue3 の場合

export default defineConfig({
  // VitePWA を追加する
  plugins: [vue(), VitePWA(pwaOptions)],

● PWA に必要な2ファイルを自動生成する

npm run build

以下の2つのファイルが自動生成されます

files generated
  dist/sw.js
  dist/workbox-3625d7b0.js

● アプリの更新を検知する

https://qiita.com/satamame/items/36c6761d363ca3894824

● オプション一覧

https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/types.ts

参考 https://qiita.com/satamame/items/2af28e120676df9ab375

No.2298
04/03 16:04

edit

react で scss(sass) を使用する

npm i -D sass

App.tsx

import "./assets/scss/style.scss";
No.2288
03/07 18:18

edit

Vite で react-app を初期化した後の eslint , prettier インストール方法

● eslint

npm i -D eslint
npm i -D eslint-plugin-react-hooks
npm i -D eslint-plugin-unused-imports
npx eslint --init

簡単な英語で質問されるので答えます。

インストール完了後に一度 eslint を実行してみます

./node_modules/.bin/eslint src --ext js,jsx,ts,tsx

.eslintrc.cjs を少し修正します。

module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "overrides": [
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint",
        "unused-imports" // 追加
    ],
    "rules": {
        // 追加 ↓
        'react/react-in-jsx-scope': 'off',

        // 追加 ↓
        "@typescript-eslint/no-unused-vars": "off", // or "no-unused-vars": "off",
        "unused-imports/no-unused-imports": "error",
        "unused-imports/no-unused-vars": [
            "warn",
            { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
        ],

        "indent": [
            "error",
            2
        ],
        "linebreak-style": [
            "error",
            "unix"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "semi": [
            "error",
            "always"
        ]
    },
    // 追加 ↓
    settings: {
        react: {
            version: 'detect',
        },
    },
};

再度 eslint を実行してみます

./node_modules/.bin/eslint src --ext js,jsx,ts,tsx

package.json を編集して、以下のコマンドからも実行できるようにします

  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext .js,.jsx,.ts,.tsx src/",
    "lint:fix": "npm run lint -- --fix"
  },
npm run lint

● eslintのプラグイン

React+TSプロジェクトで便利だったLint/Format設定紹介

● prettier

npm i -D prettier eslint-config-prettier
npm i -D prettier-plugin-organize-imports
vi .prettierrc.json
{
  "trailingComma": "all",
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": false,
  "jsxSingleQuote": false,
  "arrowParens": "always",
  "bracketSpacing": true,
  "jsxBracketSameLine": false,
  "semi": true,
  "endOfLine": "lf",
  "plugins": ["prettier-plugin-organize-imports"]
}
vi .eslintrc.cjs

.eslintrc.cjs

extendsprettier を追加

{
  "extends": [
      "prettier"
  ]
}

引用 : https://bit.ly/3SEqw20

No.2286
08/15 14:44

edit

フロントエンド側で画像圧縮する

● image-resize

https://github.com/kode-team/image-resize

npm i image-resize
import ImageResize from "image-resize";

const getCompressedImageFile = async (file: File) => {
  const imageResize = new ImageResize();
  let res = await imageResize
    .updateOptions({ height: 1000, quality: 0.75 })
    .get(file);
  res = await imageResize.resize(res);

  const blob = (await imageResize.output(res, {
    outputType: "blob",
  })) as Blob;

  return new File([blob], file.name, { type: file.type });
};

使い方

const newFile = getCompressedImageFile(file);

● browser-image-compression

https://github.com/Donaldcwl/browser-image-compression

npm install browser-image-compression
import imageCompression from "browser-image-compression";

const getCompressedImageFile = async (file: File) => {
  const options = {
    maxWidthOrHeight: 1000,
  };
  return await imageCompression(file, options);
};

使い方

const newFile = getCompressedImageFile(file);
No.2278
01/31 15:49

edit

Amazon S3 へ 署名付きURLを使ってブラウザのみでファイルアップロードする

● COR エラーの対応

何も設定していない状態だと、以下のエラーが 返ってきます。

Referrer Policy: strict-origin-when-cross-origin

設定方法 https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html

CORの書き方サンプル https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html

● エラーの対応 403 : Forbidden

添付ファイル1
s3-cor.png ( 72.5 KBytes ) ダウンロード
No.2268
01/04 11:28

edit

添付ファイル

tanstack query ( react query ) の使い方

● install

yarn add @tanstack/react-query

yarn add axios
yarn add @types/axios

● 使用する

( nextjs app Router の場合 )

components/TanstackQueryProvider.tsx

'use client';

import { FC, ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

type Props = {
  children: ReactNode;
};

export const TanstackQueryProvider: FC<Props> = ({ children }) => {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

layout.tsx

import { TanstackQueryProvider } from '../../components/TanstackQueryProvider';
        <TanstackQueryProvider>{children}</TanstackQueryProvider>

( react の場合) src/main.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
);

● 2つのコンポーネントを行き来してキャッシュが有効であることを確認する

コンポーネント Aaa.tsx の例
(jsonplaceholder から取得してみます) pages/aaa.tsx

import { useRouter } from 'next/router';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const Aaa = () => {
  const router = useRouter();

  const { data, isLoading, isError } = useQuery(['todos', 1], async () => {
    const { data } = await axios.get(
      'https://jsonplaceholder.typicode.com/todos/1'
    );

    await new Promise((resolve) => setTimeout(resolve, 3000));

    return data;
  });

  return (
    <div>
      <Link href="/bbb">bbbへ画面遷移</Link>

      <h1>Aaa</h1>

      {isLoading && <div>loading...</div>}
      {data && <div>title: {data.title}</div>}
    </div>
  );
};

export default Aaa;

コンポーネント Bbb.tsx の例 pages/bbb.tsx

import { useRouter } from 'next/router';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const Bbb = () => {
  const router = useRouter();

  const { data, isLoading, isError } = useQuery(['todos', 1], async () => {
    const { data } = await axios.get(
      'https://jsonplaceholder.typicode.com/todos/1'
    );

    await new Promise((resolve) => setTimeout(resolve, 3000));

    return data;
  });

  return (
    <div>
      <Link href="/aaa">aaaへ画面遷移</Link>

      <h1>Bbb</h1>

      {isLoading && <div>loading...</div>}
      {data && <div>title: {data.title}</div>}
    </div>
  );
};

export default Bbb;

● react query + graphql

npm i graphql-request

カスタムした graphqlクライアントを作成しておきます

src/graphqlClient.ts (この例では、ローカルストレージに保存されたjwtトークンを送信しています)

import { GraphQLClient } from "graphql-request";
const createGraphqlClientWithToken = () => {
  const baseUrl = process.env.NEXT_PUBLIC_APP_API_BASE_URL;
  if (baseUrl === undefined)
    throw new Error("axiosClient: APP_API_URL is undefined");
  const token =
    typeof window !== "undefined" ? localStorage.getItem("idToken") : "";
  return new GraphQLClient(baseUrl + "/graphql", {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
};
export { createGraphqlClientWithToken };

・1. GraphQLClient だけで graphql クエリの実行

import { GraphQLClient, gql } from "graphql-request";

  const graphQLClient = createGraphqlClientWithToken()
  const getDataAsync = async () => {
    const data = await graphQLClient.request(FindOrCreateUserQueryDocument);
  };

  useEffect(() => {
    getDataAsync();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

・2. graphql-codegen を使って、型を自動生成し tanstack query + graphql クエリの実行

codegen.ts

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:4000/graphql",
  documents: "../MY-SAMPLE-APP-api/src/query.gql",
  generates: {
    "src/graphql/generated.ts": {
      plugins: [
        {
          add: {
            content: "// Code generated by graphql-codegen. DO NOT EDIT.",
          },
        },
        {
          add: {
            content: "// @ts-nocheck",
          },
        },
        "typescript",
        "typescript-operations",
        "typescript-react-query",
      ],
      config: {
        fetcher: "graphql-request",
      },
    },
  },
};

export default config;

自動生成の実行

graphql-codegen --config codegen.ts

src/graphql/generated.ts に ファイルが自動生成されるので、それを呼び出します。

コンポーネントで以下のように記述します

import { createGraphqlClientWithToken } from "@/graphqlClient";
import { useQuery } from "@tanstack/react-query";
import {FindOrCreateUserQuery,FindOrCreateUserDocument} from "@/graphql/generated";
  const graphQLClient = createGraphqlClientWithToken();
  const { data, error, isLoading } = useQuery({
    queryKey: ["FindOrCreateUserQueryDocument"],
    queryFn: async () =>
      graphQLClient.request<FindOrCreateUserQuery>(FindOrCreateUserDocument),
  });
No.2258
06/06 17:56

edit

firestore で 各ユーザー配下のサブコレクション を横断して検索する

● firestore で 各ユーザー配下のサブコレクション を横断して検索する

FlutterでFirestoreのサブコレクションからコレクショングループを使って横断的にドキュメントを取得する

以下のようなデータがある場合、 /news サブコレクションを横断的に検索したい場合があります。

/users(コレクション)
   ├── user-0001 <ドキュメント>
   │    └── news(コレクション)
   │        ├── news-0001 <ドキュメント>
   │        └── news-0001 <ドキュメント>
   │   
   └── user-0002 <ドキュメント>
        └── news(コレクション)
            ├── news-0001 <ドキュメント>
            └── news-0001 <ドキュメント>

● news(コレクション)を横断的に検索する(検索条件なし。全て取得)

const searchQuery = query(collectionGroup(firebaseDB, 'news')).withConverter(firestoreConverter)
const querySnapshot = await getDocs(searchQuery).catch(e => {
      console.error(e)
})

● news(コレクション)を横断的に検索する(検索条件 ID=XXXXXXXXXXX で検索して取得)

const searchQuery = query(collectionGroup(firebaseDB, 'news'), where('ID', ' ==', 'XXXXXXXXXXX')).withConverter(
        firestoreConverter
      )
const querySnapshot = await getDocs(searchQuery).catch(e => {
      console.error(e)
})
No.2243
11/25 23:53

edit

typescript の jest を swc で高速化する

● typescript の jest を swc で高速化する

npm i -D @swc/jest

jest.config.jsに以下を追加する(jest.config.ts ではなく .js で記述します。)

  transform: {
    '^.+\\.tsx?$': '@swc/jest'
  },

以上です

No.2241
04/20 17:56

edit

React Suspenseのシンプルな形

let userNameGlobal = "";

const getUserName = async () => {
  await new Promise((resolve) => setTimeout(resolve, 4000));

  return "yamada taro";
};

const ChildComponent = () => {
  const func = getUserName().then((data) => {
    userNameGlobal = data;
  });

  if (!userNameGlobal) {
    throw func;
  }
  return <h1>{userNameGlobal}</h1>;
};

jsx

      <Suspense fallback={<h1>Loading...</h1>}>
        <ChildComponent />
      </Suspense>
No.2232
10/31 15:40

edit

MUI の 複数行TextField 入力時に 「Command ( Control )」+「Enter」のショートカットキーでフォーム送信をする 

import { useRef } from 'react'
import TextField from '@mui/material/TextField'

  const refSubmitButtom = useRef<HTMLButtonElement>(null)
  const submitFormOnKeyDown = (e: any) => {
    if (!(e.key == 'Enter' && (e.metaKey == true || e.ctrlKey == true))) return

    refSubmitButtom.current?.click()
  }
<TextField
	onKeyDown={e => {
	  submitFormOnKeyDown(e)
	}}
/>
<LoadingButton type='submit' ref={refSubmitButtom}>送信ボタン</LoadingButton>
No.2231
10/28 10:55

edit

Firestoreで createdAt , upcatedAt を追加する

Firestoreで createdAt , upcatedAt を追加する

import { addDoc, collection, serverTimestamp } from 'firebase/firestore'

serverTimestamp() 現在時刻をセットすることができます

// 追加するdata
const d = dayjs().format('YYYY-MM-DD_HH_mm_ss')
const data = {
  name: `Tあり追加のテスト-${d}`,
  text: `追加時刻は ${d}`,
  createdAt: serverTimestamp(),
  updatedAt: serverTimestamp()
}

// addDoc() で追加する
const colRef = collection(firebaseDB, 'users', user.uid, 'hogehoge')
await addDoc(colRef, data)

● コンバーターで自動付与する

createdAt が null の場合は serverTimestamp() でサーバーの時間を自動に設定する。
updatedAt は 常に serverTimestamp() でサーバーの時間を自動に設定する。

  toFirestore(news: News): DocumentData {
    return {
      ...news.toArray(),
      createdAt: news.createdAt === null ? serverTimestamp() : Timestamp.fromDate(new Date(news.createdAt)),
      updatedAt: serverTimestamp()
    }
  },

● データ新規登録または更新時にcreatedAt, updatedAt のカラムが含まれるかどうかをチェックするセキュリティルール

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /users/{userID}/{document=**} {
      //  read ( get , list に分類される)と write ( create , update , delete に分類される)

      allow read: if
        isSameAsLoginUser(userID)                 // rule (現在ログインしているユーザのuidが同じIDかどうか?)

      allow create, update: if
        hasFields(["createdAt", "updatedAt"]) &&  // rule (createdAt , updatedAt を含むか?)
        isSameAsLoginUser(userID)                 // rule (現在ログインしているユーザのuidが同じIDかどうか?)

      allow delete: if
        isSameAsLoginUser(userID)                 // rule (現在ログインしているユーザのuidが同じIDかどうか?)
    }

    // columnList で 渡されてきたすべてのカラムが登録したいデータオブジェクトに存在するか
    function hasFields(columnList) {
      return request.resource.data.keys().hasAll(columnList);
    }

    // userID で 渡されてきたユーザーのuidと現在ログインしているユーザのuidが同じIDかどうか?
    function isSameAsLoginUser(userID) {
      return request.auth.uid == userID;
    }

	}
}

引用 : https://zenn.dev/yucatio/articles/c5cc8718f54fd7

セキュリティルール については合わせてこちらも読んでおくと良いでしょう
https://zenn.dev/kosukesaigusa/articles/efc2528898954d95a6ae

No.2230
11/01 10:49

edit

Firestoreのデータ作成方法 (addDoc , setDoc)

● addDocによるデータ追加 ( ID自動設定)

/users/<ユーザーuid>/hogehoge コレクションに1件データを追加します。 IDは自動的にセットされます

    // ● 追加するdata
    const d = dayjs().format('YYYY-MM-DD_HH_mm_ss')
    const data = {
      name: `追加のテスト-${d}`
    }
    // addDocによる追加
    const colRef = collection(firebaseDB, 'users', user.uid, 'hogehoge').withConverter(firestoreConverter)
    await addDoc(colRef, data)

● setDocによるデータ追加またはUPDATE ( ID手動設定)

/users/<ユーザーuid>/hogehoge コレクションに1件データを追加します
IDを渡して手動でセットします
既に同じIDのデータがある場合はUPDATEとなります

    // ● 追加するdata
    const d = dayjs().format('YYYY-MM-DD_HH_mm_ss')
    const data = {
      name: `追加のテスト-${d}`
    }
    // setDocによる追加
    const docRef = doc(collection(firebaseDB, 'users', user.uid, 'hogehoge'), `MY-CUSTOM-ID-${d}`).withConverter(firestoreConverter)
    await setDoc(docRef, data)

.withConverter(firestoreConverter)が不要な場合は取り除きましょう

No.2229
11/22 17:02

edit

Firestoreのデータ取得方法まとめ

● Firestore データの取得方法

データ取得方法は以下の3種類

・getDoc()   「単一のドキュメントを取得」
・getDocs() 「コレクション(複数のドキュメント)を取得」
・onSnapshot() 「コレクション(複数のドキュメント)をサブスクライブし、中のドキュメントのどれかに変更がある場合自動的に全てのコレクションを自動で再読込(購読件数分の再リクエストが発生します)」

の3種類です。

公式 : https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

・getDoc

getDoc() メソッドに「ドキュメントリファレンス」を突っ込みます

import { doc, getDoc } from "firebase/firestore"

const docRef = doc(db, "cities", "MY-ID")
const docSnap = await getDoc(docRef)

console.log( '● 取得したデータ' )
console.log( docSnap.data() )

なお、docRefはコレクションリファレンスからも作成できます。

const docRef = doc(db, 'cities', 'MY-ID')

  ↓ このように記述することができます

const colRef = collection(db, 'cities')
const docRef = doc(colRef, 'MY-ID')

・getDocs

getDocs() メソッドに「コレクションリファレンス」を突っ込みます

import { getDocs, collection, DocumentData } from 'firebase/firestore'

const [users, setUsers] = useState<DocumentData[]>([])

const colRef = collection(firebaseDB, 'users')
const querySnapshot = await getDocs(colRef)
setUsers(querySnapshot.docs.map(doc => doc.data()))

・onSnapshot

import { getDocs, onSnapshot, query } from 'firebase/firestore'
import DocumentData = firebase.firestore.DocumentData

const [users, setUsers] = useState<DocumentData[]>([])

const q = query(collection(firebaseDB, 'users', user.uid, 'transactions'))
await onSnapshot(q, snapshot => {
  setUsers(snapshot.docs.map(doc => doc.data()))
})

JSX

<div>
  <h1>getDocs</h1>
  {users.map((user, i) => {
    return <div key={i}>{user.name}</div>
  })}
</div>

● withConverter を指定してデータ取得時に自動でコンバートする

const docRef = doc(colRef, 'MY-ID')

 ↓ withConverter メソッドを追加します

const docRef = doc(colRef, 'MY-ID').withConverter(myFirestoreConverter)

公式 : https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FirestoreDataConverter


● react-query を使用して取得する

以下の中から必要なパッケージをインストールします。

@react-query-firebase/analytics
@react-query-firebase/auth
@react-query-firebase/database
@react-query-firebase/firestore
@react-query-firebase/functions

yarn add @react-query-firebase/auth @react-query-firebase/firestore


● react-firebase-hooks を使用して取得する

https://github.com/CSFrequency/react-firebase-hooks

import { useCollection } from 'react-firebase-hooks/firestore'

const dbQuery = query(collection(firebaseDB, 'users'))
const [value, loading, error] = useCollection(dbQuery)

jsx

<div>
  <h1>react-firebase-hooks</h1>
    {error && <strong>Error: {JSON.stringify(error)}</strong>}
    {loading && <span>Loading...</span>}
    {value && (
      <ul>
        {value.docs.map(doc => (
          <li key={doc.id}>
            <strong>{doc.id}</strong> : {JSON.stringify(doc.data())},{' '}
          </li>
        ))}
      </ul>
    )}
</div>

https://github.com/CSFrequency/react-firebase-hooks/tree/v4.0.2/firestore#full-example

No.2228
10/27 16:15

edit

react や nextjs アプリを vscode .devcontainer(Docker) に対応にする

先に react や nextjs アプリを作成した後で、次の2ファイルを追加します。

1. 「.devcontainer/devcontainer.json」 の作成

ディレクトリ .devcontainer を作成して、以下のファイルを作成します

<アプリの dev container 名>は好きな名前をつけます

.devcontainer/devcontainer.json

{
  "name": "<アプリの dev container 名>",
  "dockerComposeFile": ["../docker-compose.yml"],
  "service": "app",
  "workspaceFolder": "/workspace",
  "settings": {},
  "extensions": []
}

2. 「docker-compose.yml」 の作成

<コンテナ名>は好きな名前をつけます
ports 3011 は好きな番号を指定(3000がデフォルト)

docker-compose.yml

version: '3'

volumes:
  node_modules_volume:

services:
  app:
    image: node:16.17.1
    ports:
      - 3011:3011
    volumes:
      - .:/workspace:cached
      - node_modules_volume:/workspace/node_modules
    working_dir: /workspace
    container_name: <コンテナ名>
    command: /bin/sh -c "while sleep 1000; do :; done"

3. package.json のアプリ起動ポートを変更

  "scripts": {
    "dev": "next dev",

  ↓

  "scripts": {
    "dev": "next dev -p 3011",

4. vscodeで開いて「Reopen in Container」を選択する

以上です!

No.2221
10/06 11:37

edit

Remix

● Remixのインストール(対話形式)

npx create-remix@latest

いろいろ質問されるので選択していきます。

? What type of app do you want to create? (Use arrow keys)

テンプレートを選択するにはこちらを選択します

 A pre-configured stack ready for production 

● Remixのインストール(テンプレート指定方式)

1. テンプレート(Remix-stack)こちらから選択する

https://github.com/topics/remix-stack

2. テンプレートを指定して Remix をインストールする

例 : franck-boucher / mantine-stack をインストールする場合

npx create-remix --template franck-boucher/mantine-stack
No.2210
09/08 12:57

edit

dayjs (day.js) + typescript で 日付を扱う

dayjs (7.38kb) は date-fns (23.02kb) よりバンドルサイズが小さいのが利点です。

● dayjsのインストール

npm install dayjs
npm install dayjs @types/dayjs
npm install dayjs/plugin/timezone

● 現在時刻を表示する

console.log(dayjs().format('YYYY-MM-DD HH:mm:ss'))

● 日付を加算、減算する(次の日、前の日など)

日時の加算には addメソッド
日時の減算には subtractメソッド

https://day.js.org/docs/en/manipulate/add

// 年の場合 → 「year または y」を指定して加算(または減算)します
dayjs().add(2, 'y')
dayjs().subtract(2, 'year')

● タイムゾーンの自動変換

バックエンドサーバーがUTCで日付けを取り扱っていて、タイムゾーン付き文字列で返してくる場合は ブラウザのタイムゾーンに自動変換されます。

2023-01-10T22:45:00.000Z
T が日付と時間の区切り。
Z がタイムゾーンUTC 時刻であること。 を意味します。

dayjsでの自動変換例

dayjs(new Date("2023-01-10T22:45:00.000Z")).format("HH:mm")
↓
(日本語が設定されているブラウザの場合は、dayjs関数実行時にブラウザのローカルタイムゾーンJST-9が設定される。)
07:45

● timezoneを扱う .tz() メソッドを使用できるようにする

import dayjs from 'dayjs'
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(timezone);
dayjs.extend(utc);

● 現在時刻を UTC の文字列で表示する

console.log(dayjs().utc().format('YYYY-MM-DD HH:mm:ss')) // 2023-11-12 23:59:00

console.log(dayjs().utc().format()) // 2023-11-12T23:59:00Z

● タイムゾーン付き文字列を各タイムゾーンごとに変換して表示する

受け取った時刻を Asia/Vladivostok で表示

import dayjs from 'dayjs'
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";

dayjs.extend(timezone);
dayjs.extend(utc);

console.log(
dayjs("2023-10-10T13:50:40+09:00")
  .tz("Asia/Vladivostok")
  .format("YYYY-MM-DD HH:mm:ss")
);
// 2023-10-10 14:50:40

受け取った時刻を Asia/Tokyo で表示

import dayjs from 'dayjs'
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";

dayjs.extend(timezone);
dayjs.extend(utc);

console.log(
dayjs("2023-10-10T13:50:40+09:00")
  .tz("Asia/Tokyo")
  .format("YYYY-MM-DD HH:mm:ss")
);
// 2023-10-10 13:50:40

https://qiita.com/kidatti/items/272eb962b5e6025fc51e

参考 : https://bit.ly/3cywKQ8
https://qiita.com/taisuke-j/items/58519f7ecd5ae3a1db0c

No.2207
09/16 17:23

edit

react で テストを jest or vitest で比較してみる

● A. react + typescript + jest アプリでテストを実行する

アプリの初期化

npx create-react-app react-jest-app --template typescript

その他パッケージのインストール

npm install recoil @types/recoil
npm install -D jest @types/jest ts-jest 
npm install -D @babel/preset-react
npm install -D @testing-library/react

アプリの作成

(割愛します。ボタンをクリックすると数字が1つずつ増えていくボタンとそれをRecoilを通して表示すると言う簡単なアプリです)

app.test.tsx を作成して実行する

import App from "../App";
import {
  render,
  screen,
  fireEvent,
  RenderResult,
} from "@testing-library/react";

describe("App.tsx", () => {
  test("アプリをマウントすると Counterに「ボタン 0」が表示される", () => {
    render(<App />);
    expect(screen.getByRole("button").innerHTML).toEqual("ボタン 0");
  });

  test("アプリをマウントすると Viewerに「表示 0」が表示される", () => {
    render(<App />);
    expect(screen.getByTestId("viewer-count-value").innerHTML).toEqual(
      "表示 0"
    );
  });

  test("ボタンをクリックすると 「ボタン 1」に表示が変わる", async () => {
    render(<App />);
    const button = screen.getByRole("button");
    await fireEvent.click(button);
    // console.log(button.innerHTML);
    expect(screen.getByRole("button").innerHTML).toEqual("ボタン 1");
  });

  test("ボタンを5回クリックすると 「ボタン 5」に表示が変わる", async () => {
    render(<App />);
    const button = screen.getByRole("button");

    await fireEvent.click(button);
    await fireEvent.click(button);
    await fireEvent.click(button);
    await fireEvent.click(button);
    await fireEvent.click(button);

    expect(screen.getByRole("button").innerHTML).toEqual("ボタン 5");
  });
});

テストの実行

npx react-scripts test 

● B. react + typescript + vite + vitest + happy-dom アプリでテストを実行する

アプリの初期化

(react を選択してから、react-tsを選択します)

npm init vite@latest react-vitest-app

その他パッケージのインストール

npm install recoil @types/recoil
npm install -D vitest
npm install -D @testing-library/react
npm install happy-dom

vitest 用に vite.config.ts を変更

vite.config.ts

/// <reference types="vitest" />

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'happy-dom',
  },
})

テストの実行

npx vitest --reporter verbose

参考 : https://bit.ly/3JuAaPV
参考 : https://dev.classmethod.jp/articles/intro-vitest/

● jest , vitest の違いについて

vitestの場合

  afterEach(() => {
    cleanup();
  })

を実行して、

    render(<App />);

を毎回初期化する必要があるようです。

速度について

速度についてはテストケースが少ない場合はさほど違いはありませんがvitestの方が若干早いです

No.2197
08/31 21:51

edit

No.2192
07/21 15:29

edit

How to use async and await inside a React fc?

import axios from "axios";
import React, { useEffect, useState } from "react";

export default function App() {
  const [val, setVal] = useState();

  const getAnswer = async () => {
    const { data } = await axios("https://yesno.wtf/api");
    setVal(data.answer);
  };

  useEffect(() => {
    getAnswer();
  }, []);

  return <div>{val}</div>;
}

https://bit.ly/3RKVCDZ

No.2190
07/19 20:56

edit

ReactNode や ReactChild の別の型との関係性

● ReactNode

    type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;

● ReactChild

    type ReactChild = ReactElement | string | number;

参考 : https://dackdive.hateblo.jp/entry/2019/08/07/090000

No.2189
07/19 15:45

edit

React jsx の if文、for文、console.log in JSX

● (React jsx の if文)論理演算子&& を利用する

例2: isAdmin == true の場合に 私は管理者ですと表示します。

{isAdmin && <div>私は管理者です</div>}

例2: 未ログインの場合に class="hidden" とします。

className={! isLogin && 'hidden'}>

● (React の if文)三項演算子を利用する

<div>
    {isLogin
        ? 'ログイン済み'
        : '未ログイン'
    }
</div>

● (React の if文の入れ子)「三項演算子+論理演算子&&」を利用する

ログインしつつ、管理者の場合に「管理者です」を表示する

          {isLogin ? ( isAdmin && <div>管理者です</div> )
                : (null)
          }

● (React の for文)配列.map と 無名関数を利用する

const data = [
    { text: "hogehoge" },
    { text: "fugafuga" }
  ];
<ul>
  {data.map((v,i) => {
    return <li>{v.text} {i}番目</li>;
  })}
</ul>

i は 0から始まります

配列じゃないものが渡ってくる可能性がある場合は事前にチェックします

{Array.isArray( data ) &&
  data.map((v,i) => {
    return <li>{v.text} {i}番目</li>;
  })
}

Missing "key" prop for element in iterator のアラートが出る場合はkeyを渡します

<ul>
  {data.map((v) => {
    return <li key={v.id}>{v.text}</li>;
  })}
</ul>

また return と {} は省略できます。

      <ul>
      {data.map((v) => 
        <li key={v.id}>{v.name}</li>
      )}
    </ul>

開発中に console.log を使用する場合は return と {} は残しておいた方が便利かもしれません。

<ul>
  {data.map((v) => {
    console.log( v.id );
    return <li key={v.id}>{v.text}</li>;
  })}
</ul>

● jsx の中でオブジェクトのオブジェクトをループさせる

{Object.keys(myObject).map(k => {
  const v = myObject[k]
  return (
    <div> {v.name} </div>
  )
})}

● jsxの中で console.log を使用する

・1. 1行ずつ記述する

{console.log( 'hoge' )}
{console.log( 'fuga' )}

・2. 即時関数でまとめて記述する

{(function () {
	console.log('hoge')
	console.log('fuga')

	return null
})()}
No.2057
08/03 11:05

edit

Next.js で クライアントサイドfetch を サーバーサイドへ移す ( pages router / SSR )

●クライアントサイド

import useSWR from "swr";
import axios from "axios";

export default function GoogleBooks() {
  console.log("😀");

  const fetcher = (url: string) =>
    axios(url).then((res) => {
      return res.data;
    });

  const { data, error } = useSWR(
    `https://www.googleapis.com/books/v1/volumes?q=typescript`,
    fetcher
  );

  if (error) return <div>failed to load</div>;
  if (!data) return <div>now loading...</div>;

  // この console.log は ブラウザのコンソールに表示される。
  console.log("===== data =====");
  console.log(data);

  return (
    <div>
      <ul>
        {data.items.map((item: any, i: number) => (
          <li key={i}>{item.volumeInfo.title}</li>
        ))}
      </ul>
    </div>
  );
}

●サーバーサイド

getServerSideProps() を使用するとサーバーサイド処理になります。

// Server Side Rendering
export async function getServerSideProps() {
  const response = await fetch(
    encodeURI("https://www.googleapis.com/books/v1/volumes?q=typescript")
  );
  return {
    props: {
      bookList: await response.json(),
    },
  };
}

export default function GoogleBooksSSR(props: any) {
  console.log("😎");

  // この console.log は サーバー側のターミナルに表示される。
  console.log("===== props =====");
  console.log(props);

  return (
    <div>
      <ul>
        {props.bookList.items.map((item: any, i: number) => (
          <li key={i}>{item.volumeInfo.title}</li>
        ))}
      </ul>
    </div>
  );
}

なお useSWR もサーバーサイドで利用できるようです
【React】useSWRはAPIからデータ取得をする快適なReact Hooksだと伝えたい - パンダのプログラミングブログ

No.2155
11/06 10:16

edit

Next.js で ホットリロードが聞かない時の変更箇所

next.config.js

module.exports = {
  webpack: (config, { dev }) => {
    if (dev) {
      config.watchOptions = {
        poll: 1000,
        aggregateTimeout: 200,
      };
    }

    return config;
  },
};
No.2154
02/15 17:07

edit

Reactで各種lint ( eslint , stylelint , husky + lint-staged )

● 1. Reactアプリの初期化

npx create-react-app sample-ts-app-lint --template typescript
cd sample-ts-app-lint

● 2. ESlintのインストール

yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D eslint-plugin-react eslint-plugin-react-hooks

● 設定ファイル(.eslintrc.js)の自動生成

yarn eslint --init

質問に答えていくとファイルが(.eslintrc.js)生成されます。 そこに自分でオプションを書き加えて行きます

.eslintrc.jsの例

module.exports = {
    "env": {
        "browser": true,
        "es2021": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:react-hooks/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
}
  1:1  error  'module' is not defined  no-undef

を消すために

"node": true

を記述します

● ESlintの実行

eslint を実行

eslint  . 

eslint を実行(デバッグ実行)

eslint  --debug . 

.gitignore に記載されているパスを除外しながら拡張子 .js , .jsx .ts , .tsx に対して eslint を実行

eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore . 

.gitignore に記載されているパスを除外しながら拡張子 .js , .jsx .ts , .tsx に対して eslint を実行(デバッグ表示)

DEBUG=eslint:*  eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore .

.gitignore に記載されているパスを除外しながら拡張子 .js , .jsx .ts , .tsx に対して eslint を実行(対象ファイル表示)

(Macの場合です。 /User でgrep をかけています)

DEBUG=eslint:*  eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore . 2>&1 | grep /User

eslintの現在の設定を確認する

eslint --print-config src/index.ts

package.jsonにも記述しておきます

  "scripts": {
    "lint:js": "eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore . ",
  },

↑ yarn run lint:js で起動します

● 3. stylelintのインストール

yarn add -D stylelint stylelint-config-prettier stylelint-config-standard

● stylelint設定ファイルの作成

stylelint.config.js

module.exports = {
  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
  rules: {},
}

● stylelintの実行

yarn stylelint **/*.{scss,css} --ignore-path .gitignore

package.jsonにも記述しておきます

"lint:css": "stylelint **/*.{scss,css} --ignore-path .gitignore",

yarn run lint:css で実行できるようになります

● 4. husky + lint-staged でコミット前にステージングされているファイルを対象にeslintを自動実行する

huskyで消耗したくない人はこちら
husky v5 で消耗している人には simple-git-hooks がお薦め - Qiita

husky lint-staged npm-run-all のインストール

yarn add -D husky lint-staged npm-run-all

package.json へ設定を追加

  "lint-staged": {
    "*.{ts,tsx}": "eslint",
    "*.{css,scss}": "stylelint"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "scripts": {
    "lint:js": "eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore . ",
    "lint:css": "stylelint **/*.{scss,css} --ignore-path .gitignore",
    "lint": "npm-run-all lint:js lint:css"
  },

● lint-staged の実行

yarn lint-staged

● lint-staged の実行(デバッグ表示)

動作が思ってたものと違う場合はこちらで実行して確認します

yarn lint-staged --debug

● lint-staged のオプション表示

yarn lint-staged --help

● Git hooks の有効化

npx husky-init && yarn

.husky/pre-commit が自動生成されます。

● 5. git commit でエラーが出ることを確認する

1. eslint . でエラーが出る状態にして
2. git aad -A
3. git commit

としてエラーが出ることを確認する

● vscode の設定

.vscode/settings.json

{
    "tslint.enable": false,
    "eslint.enable": true,
    "eslint.validate": [
      "javascript",
      "javascriptreact",
      { "language": "typescript", "autoFix": true },
      { "language": "typescriptreact", "autoFix": true }
  ],
}

参考 : https://qiita.com/karak/items/12811d235b0d8bc8ad00

● eslint が遅い時の調査方法

 TIMING=1 npm run lint
No.2153
04/09 10:56

edit

環境変数を dotenv-cli で変更する

yarn add -D dotenv-cli

package.json

  "scripts": {
    "build:development": "dotenv -e .env.development react-scripts build",
    "build:staging": "dotenv -e .env.staging react-scripts build",
    "build:production": "dotenv -e .env.production react-scripts build",
  },
No.2152
02/04 16:18

edit

mui

● MUI v5 のインストール

( emotion )で使用する場合 (デフォルト)

yarn add @mui/material @emotion/react @emotion/styled

( styled-components )で使用する場合

yarn add @mui/material @mui/styled-engine-sc styled-components

エラーが出る場合は以下もインストールします

yarn add @emotion/react @emotion/styled

● MUI SVG icons のインストール

全てのアイコンをビルドすることになるので、ビルド時間を考えると個別にsvgで取り込むほうが良いです。 https://fonts.google.com/icons

インストールする場合は、こちらのコマンドから

yarn add @mui/icons-material

● テーマで背景色とテキストの色を変更する

src/layouts/customTheme.ts

import { createTheme } from "@mui/material";

const customTheme = createTheme({
  palette: {
    background: {
      default: "#F7F7F9",
    },
    text: { primary: "#4c4e64de" },
  },
});

export default customTheme;

App.tsx

import { CssBaseline, ThemeProvider } from "@mui/material";
import customTheme from "./layouts/customTheme";

function App() {
  return (
    <>
      <ThemeProvider theme={customTheme}>
        <CssBaseline></CssBaseline>
        ...................
      </ThemeProvider>
    </>
  );
}

export default App;
No.2150
02/28 23:46

edit

React Hook Form ( + Yup + schema-to-yup )で バリデーションの設定をjsonで書いてフォームを作成する

● Yup + schema-to-yup のインストール

npm i  react-hook-form
npm i  @hookform/resolvers yup
npm i  schema-to-yup
または
yarn add react-hook-form
yarn add @hookform/resolvers yup
yarn add schema-to-yup

● 設定の記述例

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { buildYup } from "schema-to-yup";

MyLogin.tsx

  const validationSchema = {
    $schema: "http://json-schema.org/draft-07/schema#",
    $id: "http://example.com/person.schema.json",
    type: "object",
    properties: {
      email: {
        type: "string",
        format: "email",
        required: true,
      },
      password: {
        type: "string",
        required: true,
      },
    }
  };

  const validationConfig = {
    errMessages: {
      email: {
        required: "メールアドレスを入力してください",
        format: "メールアドレスの形式が正しくありません",
      },
      password: {
        required: "パスワードを入力してください",
      },
    },
  };
  const yupSchema = buildYup(validationSchema, validationConfig);

  const formOptions = { resolver: yupResolver(yupSchema) };
  const { register, handleSubmit, setValue, formState: { errors } } = useForm(formOptions);
  const onSubmit = async (data: any) => {
    alert('form送信されました');
  }

jsxはとてもシンプルに記述できます

return (
<form onSubmit={handleSubmit(onSubmit)}>
    <input id="email" type="email" {...register('email')} />
    <div>{errors.email?.message}</div>

    <input id="password" type="text" {...register('password')} />
    <div>{errors.password?.message}</div>
</form>
)
No.2144
04/13 16:10

edit

Recoil

Reduxよりシンプルに扱えるステート管理ツールです。
ただし、ブラウザリロードするとリセットされてしまうので永続化は別途設定する必要があります。

・1. Recoilのインストール

yarn add recoil @types/recoil

知っておくといい概念

Recoliには大きく2つの概念があります。
AtomとSelectorです。
Atomは状態を管理します。状態の保持と状態の更新ができます。
SelectorはAtomで管理している状態を加工して取得することができます。

引用 : https://bit.ly/3Si4otw

・2. データストアの作成

認証に関するデータストアを以下のファイル名で作成してみます

src/Recoil/atoms/Auth.ts

import { atom } from 'recoil';

export interface Auth {
  displayName: string | null;
  email: string | null;
  isLogined: boolean;
  isAuthChecked: boolean;
}

const initialAuth: Auth = {
  displayName: null,
  email: null,
  isLogined: false,
  isAuthChecked: false,
};

export const authState = atom({
  key: 'auth',
  default: initialAuth,
})

・3. RecoilRoot の設置

import { RecoilRoot } from 'recoil';
<RecoilRoot>
  <App />
</RecoilRoot>

・4.値の取得

useRecoilState  : 1. 値の取得とSetter取得
useRecoilValue  : 2. 値の取得のみ
useSetRecoilState : 3. Setterのみ(useSetRecoilStateはあくまでSetter関数だけなので、Stateの値自体を参照していない限りComponentにはRe-Renderが走らない)

1. useRecoilState ( 値の取得と更新 )

import { myState } from "../recoil/atoms/myState"; // 自分で作成したRecoilState
import { useRecoilState } from 'recoil'
const [recoilAuth, setRecoilAuth] = useRecoilState(myState);

2. useRecoilValue ( 値の取得のみ )

import { myState } from "../recoil/atoms/myState"; // 自分で作成したRecoilState
import { useRecoilValue } from 'recoil'
  const recoilAuth = useRecoilValue(myState);    // 値のみ

3. useSetRecoilState ( setterのみ )

import { myState } from "../recoil/atoms/myState"; // 自分で作成したRecoilState
import { useSetRecoilState } from 'recoil'
// 関数コンポーネントのトップに記述
const setRecoilAuth = useSetRecoilState(myState);   // setterのみ(Re-Renderしない)
// recoil へ保存
setRecoilAuth({
	displayName: "hogehoge",
	email: "fugafuga@test.local",
	isLogined: true,
})

● Next.js + nookies を使ってRecoilのステートの永続化設定する(Cookie に保存する場合)

https://zenn.dev/someone7140/articles/8a0414a0236142

注意 : parseCookies では http only な Cookie は 取得できません。

● recoil-persist を使ってRecoilのステートの永続化設定する(localStorage, sessionStorage に保存する場合)

recoil-persistはデフォルトだとlocalStorageに保存されますが storageオプションを設定することで任意のStorageを利用することができます。

yarn add recoil-persist

例 : src/recoil/atoms/authState.tsx

import { recoilPersist } from 'recoil-persist'
const { persistAtom } = recoilPersist({
  key: 'recoil-persist',
  storage: sessionStorage,
});
export const authState = atom({
  key: 'authState',
  default: initialAuth,
  effects_UNSTABLE: [persistAtom],  // この行を追加
})

注意 : ログインしているかどうかの情報はにローカルストレージやセッションストレージには保存しないようにしましょう。 (サーバー側に情報を持たせるべきです)

● Recoil の selector (getter , setter )

ざっくり言うとゲッターとセッターです。
次のように使用します

import { selector } from 'recoil'
import { authState } from '../atoms/Auth'

/**
 * ユーザの認証状態を取得するrecoilセレクター
 * @example : const isAuthenticated = useRecoilValue(isAuthenticatedSelector);
 */
export const isAuthenticatedSelector = selector({
  key: 'authSelector',
  get: ({ get }) => {
    const state = get(authState)

    return state.isLogined === true
  }
})

● Recoil を さらに便利にする Atom Effects

https://recoiljs.org/docs/guides/atom-effects/

effects に atom 初期化時に1度だけ実行される Atom Effects を記述することができます。
return で クリーンアップ関数を記述します
何かの値を監視する関数をここに登録しておいて自動的にステート変更させる。といった使い方ができます

export const authState = atom({
  key: 'auth',
  default: authInitialValues,
  effects: [
    () => {
      alert('atom effect 1')

      // クリーンアップ関数
      return () => {
        alert('atom effect cleanup 1')
      }
    }
  ]
})

引数

setSelf: 自分自身の値を更新するための関数
onSet: 値の変更があるたびに引数に入れたコールバック関数を実行する

など、全ての引数は https://recoiljs.org/docs/guides/atom-effects/ を参照

React, Recoilでカスタムフックを作ってRecoilを隠匿する](https://zenn.dev/tokiya_horikawa/articles/89830f78a6dd57)

No.2140
10/12 16:32

edit

Next.jsで作成したサイト に sitemap.xml を追加する

● Next.jsで作成したサイト に sitemap.xml を追加する

引用元 : Next.js に next-sitemap を導入して超手軽にサイトマップ sitemap.xml を生成しよう | fwywd(フュード)powered by キカガク

・1. next-sitemapのインストール

npm install --save-dev next-sitemap

・2. sitemap.config.jsの作成

sitemap.config.js

// config for next-sitemap

module.exports = {
  siteUrl: 'https://YOUR-SITE.com/',
  generateRobotsTxt: true,
  sitemapSize: 7000,
  outDir: './public',
};

・3. ビルドスクリプトにサイトマップを作成するコマンドを追加

package.json

  "scripts": {
    "build": "next build && next-sitemap --config sitemap.config.js",
  },

・4. ビルドの実行

npm run build

/public/sitemap.xml が生成されるので Google Search Console からGoogleに読み込ませます

・5. git管理に含めたくない場合は.gitignoreに記述して除外します

.gitignore

# next-sitemap が自動生成するファイルは除外する
/public/sitemap.xml
/public/robots.txt
No.2139
01/15 20:42

edit

react で スマホ / pc を判別する react-device-detect

● react で スマホ / pc を判別する react-device-detect

npm install react-device-detect
import { isMobile } from "react-device-detect"

{isMobile ? ( <h1>スマホ</h1> ) : ( <h1>pc</h1> ) }
No.2138
02/26 12:49

edit

React で イベント実行関数(メソッド)に引数を渡す。間違えて渡す。

● React で イベント実行関数(メソッド)に引数を渡す。

<a className='btn' onClick={myMethod.bind(this,'aiueo')}>ボタン</a>

または

<a className='btn' onClick={ (e) => {myMethod('aiueo',e)} }>ボタン</a>
// 短く書きたいなら
<a className='btn' onClick={ (e) => myMethod('aiueo',e) }>ボタン</a>

● React で イベント実行関数(メソッド)に引数を間違えて渡す。

<a className='btn' onClick={myMethod('uso800')}>ボタン</a>

このように間違った技術が存在するとどういう動作となるでしょうか?
答えはコンポーネント読み込み時に myMethod('uso800') が実行されてしまう
です。 充分気をつけましょう。

No.2137
01/14 15:44

edit

React で DOM オブジェクトを取得する

● React で DOM オブジェクトを取得する

アンチパターンですが、DOMを取得して attribute を変更してみます

import React from 'react'
const inputRefDrawerCheck = React.createRef();
let inputDOM = inputRefDrawerCheck.current
inputDOM.setAttribute('id', 'sample2');
<input type="checkbox" id="drawer-check" ref={inputRefDrawerCheck} />

IDが 「drawer-check」→「sample2」に変更されます。
このように React.createRef(); から DOM オブジェクトが取得できます。(通常あまりやりませんが)

No.2136
01/14 13:23

edit

Next.js ( pages router ) で メソッドからページ移動する

● Next.js で メソッドからページ移動する

import { useRouter } from 'next/router'
  const router = useRouter()

  const navigatePage = () => {
    alert('トップページに戻ります);
    router.push('/');
  }
<a onClick={navigatePage}>戻る</a>
No.2135
12/19 16:02

edit

useReducer を理解しやすくする

● (useReducerの理解 1). useReducer を理解する前に 配列の reduce メソッドを復習しておきましょう

https://ginpen.com/2018/12/23/array-reduce/

functional programming で map と共に必ず紹介されるメソッドです。
functional programmingの特徴をかなり簡単に紹介すると、
・「元データを更新せず元データを元に新しいデータを作成する(イミュータブル)」
・「関数に関数を渡す」
といったところです。
https://codewords.recurse.com/issues/one/an-introduction-to-functional-programming

・Array.reduce メソッド

const result = array.reduce((前回の値, 現在の値, 現在の値のインデックス, reduceによって操作している配列全て) => {
    return 次の値;
}, 初期値);
Array.reduce は 「単一の値を返します」

初期値について

・初期値を渡さないと、 インデックス=1 (2番目の値)から処理を始めます。
・初期値を渡すと、最初の値から、インデックス=0  から処理を始めます。

例1. (初期値なし)

const myArray = ['hoge', 'fuga', 'piyo'];

myArray.reduce((acc, cur, index, ar) => {
  console.log( `● acc : ${acc} / ● cur : ${cur}  / ● index : ${index}` );
  return 'xxx';
});

結果

● acc : hoge / ● cur : fuga  / ● index : 1 
● acc : xxx / ● cur : piyo  / ● index : 2 

例2. (初期値あり)初期値が acc にセットされるので配列の最初の値から処理されます

const all_result = myArray.reduce((acc, cur, index, ar) => {
  console.log( `● acc : ${acc} / ● cur : ${cur}  / ● index : ${index}` );

  const result = acc + '__' + cur;
  return result;
  
} , 'saisho');

結果

● acc : saisho / ● cur : hoge  / ● index : 0 
● acc : saisho__hoge / ● cur : fuga  / ● index : 1 
● acc : saisho__hoge__fuga / ● cur : piyo  / ● index : 2 

また最終結果 all_result

saisho__hoge__fuga__piyo 

になります。

● (useReducerの理解 2). useReducer を使ってみる

const [state, dispatch] = useReducer(reducer, initialState);

useReducerは以下の引数を受け取ります。

・引数

reducer : stateを更新するための関数。stateとactionを受け取って、新しいstateを返す。
(state, action 共に、数値や配列やオブジェクト、どのような値も受け取ることが可能です。)

initialState : stateの初期値

・戻り値

state : state(コンポーネントの状態)
dispatch : reducerを実行するための呼び出し関数

https://bit.ly/3n3ws5T

・useReducerは、ステートに依存するロジックをステートに非依存な関数オブジェクト(dispatch)で表現することができる点が本質である。
 このことはReact.memoによるパフォーマンス改善につながる。
・useReducerを活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。

● useReducer を使ってオブジェクトの値を更新したのに再描画が起きない場合

React は state の変更の有無の判定に Object.is() を使うとのことなので、現在の state を表す array を直接変更するのではなく別の array を新たに生成して setBookmarks() に渡せば OK です。
No.2131
09/07 16:17

edit

Next.js で 静的サイトとしてエクスポートする(next export)

● 1. まずビルドを実行してエラーがあるかどうかを確認する

npm run build

next バージョンの確認

npx next --version

● 2. サーバーサイドのページがある場合は取り除く

こちらのように /api/ 以下のファイルはサーバーサイドとなりますので取り除きます

Page                                       Size     First Load JS
├ λ /api/hello                             0 B            86.6 kB

● 3. next.config.js 設定の変更

module.exports = {
  reactStrictMode: true,
}

   ↓

module.exports = {
  reactStrictMode: true,
  trailingSlash: true,
}

● 4. エクスポートの実行

npm run build ; npx next export

エクスポートが成功すると out ディレクトリに htmlファイルが生成されるので表示して確認します。 php でサーバを起動する場合は

php -S 0.0.0.0:8000 -t out

http://localhost:8000/ へアクセスして確認します。

● Error: Image Optimization using Next.js' default loader is not compatible with next export. エラーの修正

・1. <Image>タグを戻す

<Image> タグを使用していると 静的サイトエクスポートできないのでエラーとなります。
代わりに <img>タグに戻しましょう

・2. next/image を 削除する

// import Image from 'next/image'

・3. eslint の設定を変更する

あわせて .eslintrc.json の設定も変更します

{
  "extends": "next/core-web-vitals"
}

  ↓

{
  "extends": "next/core-web-vitals",
  "rules": {
    "@next/next/no-img-element": "off"
  }
}
No.2127
12/31 23:58

edit

React で ページの先頭へスムーススクロール

● React で ページの先頭へスムーススクロール

npm install react-scroll
  const scrollToTop = () => {
    scroll.scrollToTop();
  };
<a onClick={scrollToTop}><div>戻る</div></a>
No.2126
12/30 23:20

edit

react swiper を使用する

● swiper の インストール

npm install swiper

pages/_app.js

import "swiper/swiper.scss";

components/SwiperComponent.jsx

import * as React from 'react';
import SwiperCore, { Pagination, Autoplay } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";

SwiperCore.use([Pagination, Autoplay]);

// interface Props {
//   imageData: Array<string>;
//   isAuto: boolean;
//   clickable: boolean
// }

const SwiperComponent = (props) => {
  return (
    <div style={{ zIndex: -9999 }}>
      <Swiper pagination={{ clickable: props.clickable }} autoplay={props.isAuto}>
        {props.imageData.map((imageSrc, i) => {
          return (
            <SwiperSlide key={`${imageSrc}${i}`}>
              <div className="top_banner_background_image" style={{ backgroundImage: `url(${imageSrc})` }}></div>
            </SwiperSlide>
          );
        })}
      </Swiper>
    </div>
  );
};
export default SwiperComponent;

表示させる

          <SwiperComponent
            imageData={[
              "img/top_banner_background_02.png",
              "img/top_banner_background_01.png",
            ]}
            isAuto={true} clickable={false}
          />
No.2125
12/29 15:27

edit

Next.js で 独自の 404エラーページを作成する

● Next.js で 独自の 404エラーページを作成する

pages/404.js を以下のような内容で作成すると独自の404エラーページが表示されます

pages/404.js

import Link from 'next/link'

export default function Custom404() {
  return (
    <div className="container">
      <h1>404 - お探しのページは見つかりませんでした</h1>
      <p>
        こちらからサイトのトップに戻ってみてください<br />
        <Link href="/"><a>サイトトップに戻る</a></Link>
      </p>
    </div>
  )
}
No.2122
12/27 11:26

edit

Next.js サイトに読み込みローディングバー(プログレスバー)を表示させる

npm install nextjs-progressbar

または

yarn add nextjs-progressbar

/pages/_app.js

import NextNprogress from 'nextjs-progressbar'
      <Component {...pageProps} />

       ↓ NextNprogress を追加

      <NextNprogress
        color="#3b7d6b"
        startPosition={0.3}
        stopDelayMs={200}
        height={1}
        showOnShallow={true}
      />

      <Component {...pageProps} />
No.2121
12/26 14:01

edit

Next.js で scss (sass) を使用する(サイト全体css / ページ単位css)

● Next.js で サイト全体に適用するグローバルな scss (sass) を使用する

・ 1. sassのインストール

npm install sass --save-dev

・ 2. scssファイル群の用意

cd styles
mkdir scss
touch scss/style.scss

styles/scss/style.scss をトップのscssとします

・ 3. pages/_app.js から読み込み

style.scss を _app.js から読み込みます

pages/_app.js

_app.js からの読み込みを style.scss に変更します

import '../styles/globals.css'

  ↓

import '../styles/scss/style.scss'

なお、scss内で画像を使用するときは絶対パスを記述して、画像ファイルは /public/ 以下に置きます

● グローバルなCSSを各ページに記述する

次のようにstyleタグに jsx global を付けることで直接 CSS を記述するのと同じように記述できます

<style jsx global>{`
  .my-class {
    background-color: green;
  }
`}</style>

● Next.js で ページ固有の scss (sass) を使用する

拡張子 「 .module.scss 」なファイルを作成する。 バスはどこでも自由ですが拡張子は必ず module.scss または module.css とします
拡張子を module.scss とした場合は自動的にscss 記法が使用できます

例 : styles/components/404.module.scss

.container_404 {
  border: 1px solid red;
  margin: 50px 0;
  h2 {
    color: blue;
  }
}

表示するコンポーネントから読み込む

import css from "../styles/components/404.module.scss"

以下のように記述して読み込みます

<div className={css.container_404}>
No.2120
06/01 11:57

edit

react-datepicker を使用する

● react-datepicker を使用する

https://github.com/Hacker0x01/react-datepicker

yarn add react-datepicker
yarn add @types/react-datepicker
import React, { useState } from 'react';

// datepicker
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
  // datetime-picker
  const [startDate, setStartDate] = useState(new Date());
<DatePicker selected={startDate} onChange={(date:any) => setStartDate(date)} />

オプション

https://qiita.com/k_hoso/items/1bacfb38e449552f2c71

No.2115
01/20 18:03

edit

JSONからTypeScriptの type を作成する

● JSONからTypeScriptの type を作成する

TypeScript で JSON オブジェクトに型情報を付加する|まくろぐ

● JSONからTypeScriptの interface を作成する

https://app.quicktype.io/?l=ts

https://jvilk.com/MakeTypes/

● JSONからTypeScriptの type を作成する(VS Code)

https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype

No.2114
12/16 18:39

edit

React のグリッド、データテーブル

● React のグリッド、データテーブル

● jbetancur / react-data-table-component

デモ : https://codesandbox.io/s/fz295
ドキュメント : https://react-data-table-component.netlify.app/?path=/story/getting-started-intro--page

インストール

yarn add react-data-table-component

使い方


import DataTable from 'react-data-table-component';

const columns = [
    {
        name: 'Title',
        selector: row => row.title,
    },
    {
        name: 'Year',
        selector: row => row.year,
    },
];

const data = [
    {
        id: 1,
        title: 'Beetlejuice',
        year: '1988',
    },
    {
        id: 2,
        title: 'Ghostbusters',
        year: '1984',
    },
]

function MyComponent() {
    return (
        <DataTable
            columns={columns}
            data={data}
        />
    );
};

画像を表示させる

const columns = [
    {
        name: 'Avator',
        selector: (row: any) => <img src={row.avartar} width="36" alt="avator" /> ,
    },
];

● mbrn / material-table

デモ : https://material-table.com/#/

インストール

yarn add material-table @material-ui/core

使い方

import MaterialTable from "material-table";
  return (
    <Layout>

      <main>
        <MaterialTable
          columns={[
            { title: "名前", field: "name" },
            { title: "かな", field: "surname" },
            { title: "birthYear", field: "birthYear", type: "numeric" },
            {
              title: "birthCity",
              field: "birthCity",
              lookup: { 34: "Tokyo", 63: "Osaka" },
            },
          ]}

          options={{
            search: true
          }}

          data={[
            {
              name: "山田",
              surname: "やまだ",
              birthYear: 1987,
              birthCity: 63,
            },
            {
              name: "大橋",
              surname: "おおはし",
              birthYear: 1990,
              birthCity: 34,
            },
            {
              name: "中村",
              surname: "なかむら",
              birthYear: 1990,
              birthCity: 34,
            },
          ]}
          title="Demo Title"
        />
      </main>

    </Layout>
  );
No.2111
12/15 09:23

edit

React Bootstrap を TypeScript で使用する

● React Bootstrap を TypeScript で使用する

yarn add bootstrap 
yarn add @types/bootstrap

yarn add react-bootstrap 
yarn add @types/react-bootstrap

index.tsx

import 'bootstrap/dist/css/bootstrap.min.css';
No.2110
12/09 21:48

edit

No.2108
12/07 09:24

edit

react-router の useRoutes() フックを使うミニマルな形 (TypeScript)

● react-router の useRoutes() フックを使うミニマルな形 (TypeScript)

useRoutes() を使って作成します。Vue-Router に少し近い記述になります。

● react アプリの作成

npx create-react-app my-router-ts-app --template typescript
cd my-router-ts-app

● react-router の追加

yarn add react-router-dom
yarn add @types/react-router-dom

src/App.tsx を以下のようにします

import "./App.css";
import { BrowserRouter } from "react-router-dom";
import Router from "./router";

function App() {
  return (
    <BrowserRouter>
      <div className='App'>
        <h1>Vite + React !</h1>
        <hr />
        <Router></Router>
      </div>
    </BrowserRouter>
  );
}

export default App;

src/router/index.tsx を以下の内容で作成します

import { useRoutes } from "react-router-dom";
import Login from "../views/Login";
import About from "../views/About";

export default function Router() {
  return useRoutes([
    {
      path: "/",
      element: <DefaultLayout />,
      children: [{ path: "about", element: <About /> }],
    },
    {
      path: "/",
      element: <GuestLayout />,
      children: [{ path: "login", element: <Login /> }],
    },
  ]);
}

src/views/About.tsx

import React from "react";
import { Link } from "react-router-dom";

const About: React.FC = () => {
  return (
    <div>
      <h1>About</h1>
      <p>テストテキストテストテキストテストテキスト</p>
      <Link to='/'>戻る</Link>
    </div>
  );
};

export default About;

src/views/Login.tsx

import React from "react";
import { Link } from "react-router-dom";

const Login: React.FC = () => {
  return (
    <div>
      <h1>Login</h1>
      <p>ログイン画面です。</p>
      <Link to='/'>戻る</Link>
    </div>
  );
};

export default Login;

これで、次のURLへアクセスできます。

http://localhost:3000/about
http://localhost:3000/login

● react-router の useRoutes() フックを使わないミニマルな形 (TypeScript)


● 全ページ共通のレイアウトを使用する(Layout コンポーネント)

https://maku.blog/p/dxamw8i/

● 戻るボタン( history.back(); )

import { useNavigate } from "react-router-dom";
const navigate = useNavigate();

<button type='button' onClick={() =>{ navigate(-1) }}>戻る</button>

● 遅延ローディング

https://zukucode.com/2021/06/react-router-razy.html

● Switch-Router

https://dev-yakuza.posstree.com/react/create-react-app/react-router/

● react-router v6 では Switch の代わりに Routes を使いましょう

https://stackoverflow.com/questions/69843615/switch-is-not-exported-from-react-router-dom

● react-router でメソッドを使って画面遷移する

import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/home');

● 認証を伴ったルーティング

function PrivateRoute ({component: Component, authed, ...rest}) {
  return (
    <Route
      {...rest}
      render={(props) => authed === true
        ? <Component {...props} />
        : <Redirect to={{pathname: '/login', state: {from: props.location}}} />}
    />
  )
}
<Route path='/' exact component={Home} />
<Route path='/login' component={Login} />
<PrivateRoute authed={this.state.authed} path='/dashboard' component={Dashboard} />

参考 : https://blog.logrocket.com/complete-guide-authentication-with-react-router-v6/

No.2107
05/31 11:40

edit

TypeScript

Reactの関数コンポーネントで親コンポーネントから子コンポーネントの関数を実行する

● Reactの関数コンポーネントで親コンポーネントから子コンポーネントの関数を実行する

forwardRef , useImperativeHandle を使います

import React, { useImperativeHandle, forwardRef, useRef } from "react";

https://react.dev/reference/react/useImperativeHandle

useImperativeHandle は forwardRef と組み合わせて使います。
useImperativeHandle は コンポーネントにメソッドを生やす Hooks です

● 子コンポーネントで関数 showAlert() を登録

1. メソッドの追加

import { useImperativeHandle, forwardRef } from "react";
    useImperativeHandle(
        ref,
        () => ({
            showAlert() {
                alert("Child Function Called")
            }
        }),
    )

2. refを受け取れるようにする

const MyChildComponent = (props) => {

      ↓

const MyChildComponent = (props, ref) => {
export default MyParentComponent;

      ↓

export default forwardRef(MyParentComponent);

● 親コンポーネントで ref を渡す

import { useRef } from "react";
const childRef = useRef();
<MyChildComponent
    ref={childRef}
/>

後は親コンポーネントの好きなところから関数を呼ぶことができます

<button type="button"
    onClick={ () => { childRef.current.showAlert() }}
>

関数コンポーネントはクラスとどう違うのか? — Overreacted

● useImperativeHandle を async で使用したい

子コンポーネントのuseImperativeHandle を次のようにします。

    useImperativeHandle(
        ref,
        () => (
            {
                showAlert:  async function(){ await showAlert(); } ,
            }
        )
    )

また showAlert() 関数にも async をつけておきます

● TypeScript で useImperativeHandle を使用する例

ForwardRefRenderFunction を使います

import React, { useImperativeHandle, forwardRef } from "react";

interface Props {
  text: string;
}

interface Handler {
  showAlert(): void;
}

const ChildBase: React.ForwardRefRenderFunction<Handler, Props> = (
  props,
  ref
) => {
  // 公開する関数
  useImperativeHandle(ref, () => ({
    showAlert() {
      alert(props.text);
    },
  }));

  return <span>{props.text}</span>;
};

const VFCChild = forwardRef(ChildBase);
export default VFCChild;
No.2093
10/25 16:39

edit

Next.js の ビルド先ディレクトリ「.next」を変更する / ビルド先ディレクトリをシェルから変更する

● Next.js の ビルド先ディレクトリ「.next」を変更する

next.config.js

module.exports = {
  distDir: '.next',
}

● Next.js の ビルド先ディレクトリをシェルから変更する

シェルから環境設定をセットすると process.env で読み取ることができます

シェルコマンド

export NEXTJS_BUILD_DIST=.tmp_next

next.config.js

( NEXTJS_BUILD_DISTが設定してある場合はそのディレクトリをセット。 設定されてない場合はデフォルトの .next をセット )

module.exports = {
  distDir: process.env.NEXTJS_BUILD_DIST ? process.env.NEXTJS_BUILD_DIST : '.next',
}

シェルから環境変数を削除するには次のようにします

export -n NEXTJS_BUILD_DIST
No.2090
10/28 09:15

edit

Next.js を nginx にデプロイする

● Next.js を nginx にデプロイする

・Next.js を使用できるようにする

1. Next.js の準備

npm run build
npm run start

2. Nginxの設定

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

    ↓ / と /_next/ 以下を表示できるようにします。

location @nextserver {
    proxy_pass http://localhost:3000;
    add_header X-Custom-HeaderNextServer "Value for Custom Header @nextserver";
}

location ^~ /_next {
    try_files $uri @nextserver;
    expires 365d;
    add_header Cache-Control 'public';
}

location / {
    try_files $uri $uri.html /index.html @nextserver;
}

3. Nginxのリスタート

nginx -s reload

4. Next.js の public ディレクトリを表示できるようにする。

シンボリックリンクを貼ればOKです

ln -s /PATH/TO/YOUR/APP/public /PATH/TO/YOUR/WEB-SITE/DocumentRoot

以上でokです

Next.js とphpを使用できるようにするには

Next.js とphpを使用できるように下記の仕様とします

「/php/<ファイル名>でアクセスした場合」→  /home/YOUR/SERVER/PATH/DocumentRoot/php/<ファイル名>を返す
「/でアクセスした場合」→  next.jsを返す  
	location /php/ {
	    alias  /home/YOUR/SERVER/PATH/DocumentRoot/php/;
	    index  index.html index.htm;
	}

	location / {
      	# Next.js Server
      	proxy_pass http://localhost:3000;
	}
No.2086
12/08 09:15

edit

Next.js で 任意のdivの高さを 100% にする ( height:100% )

● Next.js で 任意のdivの高さを 100% にする ( height:100% )

const FullHeightPage = () => (
  <div>
    Hello World!
    <style global jsx>{`
      html,
      body,
      body > div:first-child,
      div#__next,
      div#__next > div {
        height: 100%;
      }
    `}</style>
  </div>
)
No.2085
10/22 10:55

edit

Next.js で GraphQL クライアント Apolloクライアントを使用する

● 1. サンプルの「GraphQLクエリ」と「結果」を確認しておく

URL
http://snowtooth.moonhighway.com/

エンドポイントURL

https://countries.trevorblades.com

GraphQLクエリ

query {
  countries{
    code
    name
    emoji
  }
}

こちらの「エンドポイントURL」「GraphQLクエリ」を入力して ▶︎のボタンをクリックするとGraphQLクエリーが実行されます


● 2. apolloクライアントのインストール

開発しているアプリケーションの一番上の階層からnpmでインストールします

npm install @apollo/client graphql

● 3. apolloクライアントクラスの作成

apollo-client.js

import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: 'https://countries.trevorblades.com',            // 今は直接設定していますが .env から参照しましょう。
  cache: new InMemoryCache(),
});

export default client;

● 4. GraphQLから取り込んだデータを表示したいコンポーネントで次のように追加する

/pages/index.js

import { gql } from "@apollo/client";
import client from "../apollo-client";

SSG (Static Site Generation)用の getStaticProps() 関数を作成する
/pages/index.js

// getStaticProps ( for SSG (Static Site Generation) )
//   getStaticPathsは(本番環境)ビルド時に実行されます / (開発環境)リクエスト毎に実行されます
export async function getStaticProps() {
  const { data } = await client.query({
    query: gql`
query {
  countries{
    code
    name
    emoji
  }
}
    `,
  });
  return {
    props: {
      countries: data.countries.slice(0, 4),
    },
 };
}

/pages/index.js の JSXで表示する

export default function Home({ countries }) {
      {countries.map((v) => (
        <div key={v.code} style={{ display:'flex' }}>
          <h1 style={{ lineHeight:'100px' }}>{v.name}</h1>
          <div style={{ fontSize:'100px' }}>
            {v.emoji}
          </div>
        </div>
      ))}

これで該当ページを表示すると次のような表示となります

以上です。


● getStaticProps , getStaticPaths , getServerSideProps について

https://bit.ly/3oCOn4L

getStaticProps(静的生成): ビルド時にデータ取得する
getStaticPaths(静的生成): データに基づきプリレンダリングする動的ルートを特定する
getServerSideProps(サーバーサイドレンダリング): リクエストごとにデータを取得する


● VS Code の拡張機能「Apollo GraphQL」のインストールして開発速度をアップさせる

https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo

[GraphQL] TypeScript+VSCode+Apolloで最高のDXを手に入れよう | DevelopersIO


● GraphQL Playground for Chrome

https://chrome.google.com/webstore/detail/graphql-playground-for-ch/kjhjcgclphafojaeeickcokfbhlegecd/related?hl=ja


● Apollo Client Devtools for Chrome

https://chrome.google.com/webstore/detail/apollo-client-devtools/jdkknkkbebbapilgoeccciglkfbmbnfm/related

Google Chromeの拡張機能 Apollo Client Devtools でできること(引用 : https://bit.ly/3pmP4je

– GraphiQLをその場で実行する(本来であればAPIサーバと別のポートでGraphiQLサーバを立ち上げて、ブラウザでそこにアクセスして利用します)
– JavaScriptから発行されたクエリのログの確認、クエリの編集・再発行
– apollo-link-stateでキャッシュに書き込んだ値の確認(apolloはクエリのレスポンスを勝手にキャッシュしてくれるのですが、その内容も確認できます)

初めて Apollo Client を使うことになったらキャッシュについて知るべきこと - WASD TECH BLOG

・取得するフィールドに id は必ず含める
・更新処理のときは Mutation のレスポンスでオブジェクトのキャッシュを更新する
・作成、削除処理のときは refetchQueries などを使い配列のキャッシュを更新する
・画面表示のたびに最新のデータを表示したければ fetchPolicy: "cache-and-network" を使う

fetchPolicy の種類

https://www.apollographql.com/docs/react/data/queries/

Name Description
cache-first

Apollo Client first executes the query against the cache. If all requested data is present in the cache, that data is returned. Otherwise, Apollo Client executes the query against your GraphQL server and returns that data after caching it.

Prioritizes minimizing the number of network requests sent by your application.

This is the default fetch policy.

cache-only

Apollo Client executes the query only against the cache. It never queries your server in this case.

A cache-only query throws an error if the cache does not contain data for all requested fields.

cache-and-network

Apollo Client executes the full query against both the cache and your GraphQL server. The query automatically updates if the result of the server-side query modifies cached fields.

Provides a fast response while also helping to keep cached data consistent with server data.

network-only

Apollo Client executes the full query against your GraphQL server, without first checking the cache. The query's result is stored in the cache.

Prioritizes consistency with server data, but can't provide a near-instantaneous response when cached data is available.

no-cache

Similar to network-only, except the query's result is not stored in the cache.

standby

Uses the same logic as cache-first, except this query does not automatically update when underlying field values change. You can still manually update this query with refetch and updateQueries.

参考 : ApolloのFetchPoliciesを理解する - Qiita

添付ファイル1
No.2084
12/09 16:10

edit

添付ファイル

ReactのJSX内で string の style を扱う

● ReactのJSX内で string の style を扱う

helpers/stringHelper.js

export const getStyleObjectFromString = str => {
  if (!str) { return {}; }
  const style = {};
  str.split(";").forEach(el => {
    const [property, value] = el.split(":");
    if (!property) return;

    const formattedProperty = formatStringToCamelCase(property.trim());
    style[formattedProperty] = value.trim();
  });
  return style;
};

コンポーネントで使用する

import { getStyleObjectFromString } from "helpers/stringHelper";
      return (
          <div
            style={getStyleObjectFromString("color:red; margin:20px; font-weight:bold;")}
          />
      )
No.2083
10/26 13:14

edit

Next.js で YAMLファイルを使用する

● Next.js で YAMLファイルを使用する

js-yaml-loaderのインストール

npm install js-yaml-loader

next.config.js に以下を設定

module.exports = {
 webpack: function (config) {
   config.module.rules.push(
     {
       test: /\.ya?ml$/,
       use: 'js-yaml-loader',
     },
   )
   return config
 }
}

使用する

import useryml from 'user.yml';
  console.log( '● useryml' );
  console.log( useryml );
No.2082
10/19 22:23

edit

VS Code の「F5」キーで Next.js のデバッグを行う

● VS Code の「F5」キーで Next.js のデバッグを行う

Next.JSのVS Code を使ったデバッグは次の2ステップのみでとても簡単に行うことができます

・1. ローカルサーバーを起動する

VS Codeの
「エクスプローラー」→「NPMスクリプト」 →「dev」の横の ▶︎ をクリックしてローカルサーバーを起動する。

・2. 「F5」をクリックしてデバッガーを起動する

最初に launch.json があるかどうかのチェックが行われ、まだない場合は作成を促されるので作成します。 launch.json を以下の内容で保存します

{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Next.js: debug server-side",
        "type": "node-terminal",
        "request": "launch",
        "command": "npm run dev"
      },
      {
        "name": "Next.js: debug client-side",
        "type": "pwa-chrome",
        "request": "launch",
        "url": "http://localhost:3000"
      },
      {
        "name": "Next.js: debug full stack",
        "type": "node-terminal",
        "request": "launch",
        "command": "npm run dev",
        "console": "integratedTerminal",
        "serverReadyAction": {
          "pattern": "started server on .+, url: (https?://.+)",
          "uriFormat": "%s",
          "action": "debugWithChrome"
        }
      }
    ]
  }

この設定ファイルでは

Next.js: debug client-side
Next.js: debug server-side
Next.js: debug full stack

と言う3つのデバッグを定義していますので「Next.js: debug client-side」を選択肢で起動します。(クライアントアプリをデバッグしたい場合)

これだけでokです。
後は「F5」でデバッグを起動「shift+F5」デバッグを終了 のお決まりのキーボード操作をしてください

No.2081
11/22 09:53

edit

Next.js で import の相対パスのルートディレクトリを変更(固定)してルートからの絶対パスで記述する

● Next.js で import の相対パスのルートディレクトリを変更(固定)してルートからの絶対パスで記述する

jsconfig.json (プロジェクトトップディレクトリにこのファイルがない場合は新規で作成します)

{
  "compilerOptions": {
    // import時のパスのルートを設定
    "baseUrl": "."
  },
  "include": ["**/*.js", "**/*.jsx"]
}

VS Codeの「F12」てのファイル移動も有効です

No.2080
11/02 09:04

edit

React Hook Formでフォームを作成する ( + Yup )

● 1. React Hook Formのインストール

npm install react-hook-form
または
yarn add  react-hook-form

● 2. React Hook Formを使用する

import { useForm } from 'react-hook-form';
// React Hook Form
const {register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data:any) => {
    alert('form送信されました'); 
    console.log(data);
}

useForm() には様々なオプションを渡すことができます

https://react-hook-form.com/api/useform/

useForm({
  mode: 'onSubmit',  // onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'
  reValidateMode: 'onChange',
  defaultValues: {},
  resolver: undefined,
  context: undefined,
  criteriaMode: "firstError",
  shouldFocusError: true,
  shouldUnregister: false,
  shouldUseNativeValidation: false,
  delayError: undefined
})

useForm() して返ってくる handleSubmitに (バリデーションOKの時の関数,バリデーションNGの時の関数) を渡します

handleSubmit(SubmitHandler, SubmitErrorHandler)

● フォーム の部品を設置する

フォーム の部品の作り方には2つ方法があります
引用 : react-hook-formでregisterとControllerのどちらを使うか - mrsekut-p

・useForm の registerを使って、<input {...register('hoge')} />とする (input , text , MUIの TextField に使用できます )
・useForm のcontrolと <Controller name='hoge' control={フォーム部品コンポーネント }> を使う (上記以外の複雑なコンポーネントはこちらを使用します)

例1: 会社名を入力するフォームに「入力必須」「入力文字数4文字以上」のバリデーションを設定します

return (
    <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register('company', { required: true, minLength: 4 })} placeholder="株式会社○○" />
        {errors.company?.type === "required" && <div className="err_message" id="company_err">会社名は必須です</div>}
        {errors.company?.type === "minLength" && <div className="err_message" id="company_err">会社名は4文字以上を入力してください</div>}

        <input type="submit" />
    </form>
);

例2: 次のように「チェック内容」と「エラーメッセージ」をまとめて記述することもできます

    <input type="email" placeholder="user@server.xxx"
        {...register("email", {
            required: "入力必須です",
            pattern: {
            value: /\S+@\S+\.\S+/,
            message: "メールアドレスが不完全です"
            }
        })}
    />
    {errors.email && <div className="err_message">{errors.email.message}</div>}

● 3. フォーム入力値のバリデーションの記述と種類

https://react-hook-form.com/jp/api#register

● 4. コンポーネントは何を使用するか?

以下のコンポーネントがおすすめです。

・ shadcn/ui

https://ui.shadcn.com/docs/components/input

・ MUI

https://mui.com/material-ui/react-text-field/

・ chakra-ui

https://chakra-ui.com/docs/components

・ radix

https://www.radix-ui.com/

● 4. React Hook Form + Yup を使用する

バリデーションの記述をYupでまとめるには次のように記述します

npm install @hookform/resolvers yup
または
yarn add @hookform/resolvers yup
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
  const validationSchema = Yup.object().shape({
    firstName: Yup.string()
      .required('First Name は必須です'),
    lastName: Yup.string()
      .required('Last Name は必須です'),
  });

  const formOptions = { resolver: yupResolver(validationSchema) };

  const { register, handleSubmit, reset, formState } = useForm(formOptions);
  const { errors } = formState;
<form onSubmit={handleSubmit(onSubmit)}>
  <input type="text" {...register('firstName')} className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
  <div className="invalid-feedback">{errors.firstName?.message}</div>
</form>

name="firstName" は記述しなくてokです。

● SWR(Axios)で非同期で取得したデータをフォームのデフォルト値として流し込む

・ valuesメソッドが使えるようになったようです

https://github.com/react-hook-form/react-hook-form/pull/9261

外部からフォームの値を変更するには以下の2つの方法を使用します。
( なお、useState は使用できません )
( MUI を使用している場合でも問題なくこちらの方法で値を流し込むことができます。(ただしバージョンが古いとうまく動作しないのでできるだけ新しいバージョンにアップデートしましょう。 過去にreact-hook-form@7.3.0 ではうまく動作しませんでした。)

・A. Resetメソッドを使う方法

次の二箇所にデータを流し込む命令をセットします

// ● SWR
const fetcher = (url: string) => axios(url)
  .then((res) => {
    reset(res.data);  // 1. axiosでデータ取得時にデータをフォームに反映(Re-render)
    return res.data
  });
const { data, error, mutate } = useSWR(`http://localhost:8000/api/news/1`, fetcher);

// ● useForm
const formOptions = {
  defaultValues: data,  // 2. SWRのキャッシュがデータをすでに取得している場合はキャッシュからフォームに反映
};
const { control, register, handleSubmit, reset, formState: { errors } } = useForm(formOptions);

・B. setValueメソッドを使う方法

  // ● React Hook Form
  const { register, handleSubmit, setValue, formState } = useForm(formOptions);

setValueを使用します

 // ● SWR
  const fetcher = (url: string) => axios(url)
    .then((res) => {
      // 最初に1度だけフォームに値をセット
      const data = res.data;
      Object.keys(data).forEach(function (k) {
          setValue(k, data[k]);
      });

      return res.data
    });

  const { data, error, mutate } = useSWR(`http://localhost:8000/api/news/${params.id}`, fetcher);
  // swrによるfetchエラー時
  if (error) return <div>failed to load</div>

● setValue() がうまく動作しない場合は

{...register('hoge')} の記述が抜けている可能性がありますチェックしましょう。

<input type="text" id="hoge" {...register('hoge')} />

● Yupでカスタムバリデーション

自作関数を使う場合このように記述できます

function myPasswordCheck(){
 return false
}
.test(
  "mypassword",
  "パスワードが間違っています",
  myPasswordCheck
)

● Yupでバリデーションの実行順序を指定するのは無理?

https://github.com/jquense/yup/issues/503

● Yupで動的にバリデーションエラーメッセージを変更する

createError() を返せばokです。createErrorファンクション自体は渡してあげる必要があります。

    return createError({
      message: `パスワードは半角英字、数字、記号を組み合わせた 8文字以上で入力してください (${password})`,
    });

● useFormState

https://react-hook-form.com/api/useformstate
https://qiita.com/bluebill1049/items/f838bae7f3ed29e81fff

● @hookform/devtools

yarn add @hookform/devtools
import { DevTool } from '@hookform/devtools';

jsx

<DevTool control={control} placement="top-left" />

<form onSubmit={handleSubmit(onSubmit, onError)}>
.............
</form>

● その他参考になるサイト

公式サンプル
https://github.com/react-hook-form/react-hook-form/tree/master/examples

https://bit.ly/3oSjgm9

React Hook Form 7 - Form Validation Example | Jason Watmore's Blog

jsonによるスキーマ定義をyupに変換する

https://github.com/kristianmandrup/schema-to-yup

yarn add schema-to-yup

● React Hook Form をフォームの外のボタンのメソッドから submit する

onClick = { () => {
    handleSubmit(onSubmit)()
}}

● React Hook Form をフォームの外のボタンのメソッドから submit する (throttleで連打防止)

https://github.com/alibaba/hooks

import { useThrottleFn } from 'ahooks'

  const { run: throttledHandleSubmit } = useThrottleFn(
    () => {
      handleSubmit(onSubmit)()
    },
    { wait: 2000 }
  )
onClick = { throttledHandleSubmit }

● watchでフォームの値を監視する

https://zenn.dev/hirof1990/scraps/2cbf610f439283

No.2068
08/03 14:07

edit

React で登場する ... (3点リーダー)(ドット3つ)の使い方

● React で登場する ... (3点リーダー)(ドット3つ)(JSX Spread Attributes)の使い方

これは Spread Attributes(スプレッド構文) といって ES6 Javascript での記法です。Spread syntax (...)
意味としては以下のコードと同じ意味合いとなります。

● Spread syntax (...) の例

const obj = {
  id:1,
  name: 'John',
  email: 'john@test.local',
}
console.log( obj );
console.log( {...obj} );

結果

{ id: 1, name: 'John', email: 'john@test.local' }
{ id: 1, name: 'John', email: 'john@test.local' }
const props = { foo: "foo", bar: "bar" };
render() {
  return <Child foo={props.foo} bar={props.bar} />
}

       ↓

const props = { foo: "foo", bar: "bar" };
render() {
  return <Child {...props} />
}

● なぜ JSX Spread Attributes を利用するのか?

子コンポーネントに渡したい値に変更があった場合に変更箇所が少なくて済みます。

props に hogee を追加する場合でも提示する箇所のみの変更でokです。

const props = { foo: "foo", bar: "bar", hoge:"hoge" };
render() {
  return <Child {...props} />
}
No.2067
07/01 16:02

edit

Next.jsで改行コードを改行タグに変換(nl2br)する

● Next.jsで改行コードを改行タグに変換(nl2br)する

npm install react-nl2br -S
const nl2br = require('react-nl2br');

jsxで以下のように記述します
my_textに入っている文字列の改行コードをbrタグに変換します

return(
    <div>
        { nl2br(my_text) }
    </div>
);
No.2065
10/08 09:33

edit

Next.js で日付をフォーマットする

● moment.js の次世代のパッケージ date-fns をインストールする

次世代のパッケージ date-fns をインストールします

npm install date-fns

● 使用する

import { parseISO, format } from 'date-fns'
import ja from 'date-fns/locale/ja'
console.log( format(new Date(), 'yyyy-MM-dd (EEEE) HH:mm:ss', {locale:ja}) );

結果

2021-10-08 (金曜日) 10:09:21

● jsxで使用する

// date-fns
function Date({ dateString }) {
 return <time dateTime={dateString}>{format(parseISO(dateString), 'yyyy.MM.dd (EEEE)', {locale:ja} )}</time>
};
<Date dateString={my_date} />
No.2064
11/01 09:59

edit

Next.js で font awesomeなどのアイコンフォントを使用する

● React Icons インストール

npm install react-icons --save

● コンポーネント内で呼び出して使用する

Mycompoenent.js

import { FaGithub } from "react-icons/fa"

function MyComponent() {
  return (
    <div>
        <FaGithub />
    </div>
  )
}

● 使用したいアイコンを検索する

https://react-icons.github.io/react-icons/search?q=cart

No.2063
10/07 15:04

edit

Next.js で Axios , fetcher の非同期通信を便利にする SWR を使用する

● SWR を使用する

・1. swrとaxiosのインストール

yarn add swr
yarn add axios @types/axios

または

npm i swr -S

・2. fetcherの定義

import useSWR from 'swr'

fetchを使用する場合 の fetcher の定義方法

const fetcher = (url) => fetch( url )
  .then(res => res.json());

axiosを使用する場合 の fetcher の定義方法**

const fetcher = (url: string) => axios(url)
  .then((res) => {
    return res.data
  });

・3. NewsIndexコンポーネントの作成

function NewsIndex() {
  const { data, error } = useSWR('https://YOUR/SERVER/PATH/api/news/', fetcher)
    // swrによるfetchエラー時
  if (error) return <div>failed to load</div>
  // swrによるfetchロード中
  if (!data) return <div>now loading...</div>
  return (
    <ul>
    {data.map((v) => 
      <li key={v.id}>{v.id} : {v.name}</li>
    )}
  </ul>
  );
}

・4. NewsIndexコンポーネントの呼び出し

<div>
  <NewsIndex/>
</div>

これで URL「https://YOUR/SERVER/PATH/api/news/」が返す json (中身はコレクション)の id と name を一覧で表示します。

● SWRの自動再検証(自動再読込)について

デフォルトでは自動的に revalidate(再読込)処理が入っています。

「ページがフォーカスした時」 「タブを切り替えた時」 「設定したポーリング間隔」で再読み込みされます。

100msecとフォーカス時に自動再読込

  const options = {
    revalidateOnFocus,
    refreshInterval: 100,
  };
  const { data, error, mutate } = useSWR(url, fetcher, options);
revalidateIfStale = true: automatic revalidation on mount even if there is stale data (details)
revalidateOnMount: enable or disable automatic revalidation when component is mounted
revalidateOnFocus = true: automatically revalidate when window gets focused (details)
revalidateOnReconnect = true: automatically revalidate when the browser regains a network

公式 : https://swr.vercel.app/docs/options
参考 : https://bit.ly/3Aicc4w

● fetch と axios どちらを使用するか?

axios

const options = {
  url: 'http://localhost/test.htm',
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json;charset=UTF-8'
  },
  data: {
    a: 10,
    b: 20
  }
};

axios(options)
  .then(response => {
    console.log(response.status);
  });

fetch

const url = 'http://localhost/test.htm';
const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json;charset=UTF-8'
  },
  body: JSON.stringify({
    a: 10,
    b: 20
  })
};

fetch(url, options)
  .then(response => {
    console.log(response.status);
  });

● データのPOST(送信)


● 参考

zeit製のswrがすごい - Qiita

引用 : https://bit.ly/3DkNoL0

【JS】Fetch API のResponse には気をつけて。axios とは違うぞ。。。 - 7839

No.2062
02/22 22:58

edit

React に Tailwind CSS を導入する

● React + Tailwind CSS のアプリを新規作成する

npx create-next-app next-tailwind -e with-tailwindcss

● Tailwind CSS のインストール

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

● Tailwind CSS の設定ファイルの作成

npx tailwindcss init -p

自動で tailwind.config.js , postcss.config.js の2ファイルが生成されます

tailwind.config.js

module.exports = {
  mode: 'jit',
  purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}
No.2056
10/01 15:34

edit

React hooksの概要 / useState / useEffect / useContext / useReducer / useMemo / useCallback / useRef

● React の Hooks まとめ

● 1. State Hooks
 ・useState
 ・useReducer

● 2. Context Hooks
 ・useContext

● 3. Ref Hooks
 ・useRef
 ・useImperativeHandle

● 4. Effect Hooks 
 ・useEffect

● 5. Performance Hooks
 ・useMemo
 ・useCallback

● 6. Other Hooks 
 ・useDebugValue
 ・useId
 ・useSyncExternalStore
 ・useActionState

https://react.dev/reference/react/hooks#other-hooks

● 1. React hooks / useState()

useState() とは

・「状態を持つ変数」と「更新する関数」を管理するReactフックです。
・「状態を持つ変数」の値が変わると useState を宣言したコンポーネントで再レンダリングが起こります。
・(jsxの中でその変数が使われていてもいなくても再レンダリングがおこります)

useStateの使い方

import { useState } from "react";
const [email, setEmail] = useState<string>("");
// const [変数, 変数を更新するための関数(setter アクセサ)] = useState(状態の初期値);
// (例)変数 email / 更新する関数 setEmail() を宣言する
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          placeholder="Email..."
        />

オブジェクトや配列に対して、useStateをどう使うか

const [member, setMember] = useState({ name: "", part: "" });

引用 : https://bit.ly/3nG3WHa

useStateで型指定

const [isLogined, setIsLogined] = useState<boolean>(true);

TypeScriptで中身がオブジェクトであるuseStateの初期値にnullが使えない

TypeScriptで

const [startDate, setStartDate] = useState(new Date());

の初期値を null にしたい場合は、以下のように記述します

  const [startDate, setStartDate] = useState <Date|null>(null);

useStateで更新したstateは即座に更新されるわけではない https://tyotto-good.com/blog/usestate-pitfalls

前の値を保持する時にuseRefを使います。 https://qiita.com/cheez921/items/9a5659f4b94662b4fd1e

非同期 async で使いたい時は useAsyncEffect() 関数を用意します https://github.com/jeswr/useAsyncEffect/blob/main/lib/index.ts

React.useState()の初期値は一度しかセットされない https://zenn.dev/lilac/articles/9e025186343058


● 2. React hooks / useEffect()

useEffect() とは

useEffectとは、関数コンポーネントで副作用を実行するためのhookです。
useEffectで関数コンポーネントにライフサイクルを持たせることができます
useEffectを使うと、useEffectに渡された関数はレンダーの結果が画面に反映された後(マウント時)に1回だけ動作します。( = componentDidMount() )
またクリーンアップ処理を返すとアンマウント時にも実行できます。( =componentWillUnmount()  )

useEffect の宣言方法

// 第1引数に「実行させたい副作用関数」を記述
// 第2引数に「副作用関数の実行タイミングを制御する依存データ」を記述
useEffect(() => {
  // 実行したい処理
  return () => {
    // クリーンアップの処理
  }
}, [input])

引用 : https://bit.ly/3SVp3ne
https://bit.ly/3Mn4Kwq

第2引数が指定されている場合は、マウント時以降は第2引数に渡された値が更新された時 に実行されます

初回レンダリング完了時1回だけ実行する

useEffect(() => {
    console.log('useEffectが実行されました');
},[]);
// 第2引数の配列を空にして記述すると初回レンダリング完了時(マウント時)のみ1回だけ実行されます。(実行を1回だけに限定できます)

第2引数を省略するとコンポーネントがレンダリングされるたび毎回実行されます。 (非常に危険です)

初回レンダリング完了時と任意の変数が変化したとき実行する

  const [count, setCount] = useState(0);
  useEffect(() => {
    alert('変数 count が変更されましたよ');
  }, [count]);  // 第二引数の配列に任意の変数を指定

マウント解除時に実行するにはクリーンアップ関数を返せばokです

const FunctionalComponent = () => {
 React.useEffect(() => {

   // クリーンアップ関数
   return () => {
     console.log("Bye");
   };

 }, []);
 return <h1>Bye, World</h1>;
};

初回は実行されないuseEffectのカスタムフックを作る(React)

https://zenn.dev/catnose99/scraps/30c623ba72d6b5

「useEffectを使用しないでコンポーネント直下に処理を記述」と「useEffectの第2引数を指定しない」の違いは?

以下のようなタイミングの違いがあります

・「useEffectを使用しないでコンポーネント直下に処理を記述」する場合

NoUseEffectComponent.tsx

import React, { useState } from "react"
const NoUseEffectComponent: React.FC = () => {
  console.log("=====(NoUseEffectComponent.tsx) function top =====")
  const [count, setCount] = useState(0)
  console.log("=====(NoUseEffectComponent.tsx) called =====")
  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        ({count})NoUseEffectComponent
      </button>
      {console.log("=====(NoUseEffectComponent.tsx) render last =====")}
    </>
  )
}
export default NoUseEffectComponent

・「useEffectの第2引数を指定しない」

UseEffectComponentNoSecondArgument.tsx

import React, { useEffect, useState } from "react"
const UseEffectComponentNoSecondArgument: React.FC = () => {
  console.log("=====(UseEffectComponentNoSecondArgument.tsx) function top =====")
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log("=====(UseEffectComponentNoSecondArgument.tsx) called =====")
  })
  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        ({count})UseEffectComponentNoSecondArgument
      </button>
      {console.log("=====(UseEffectComponentNoSecondArgument.tsx) render last =====")}
    </>
  )
}
export default UseEffectComponentNoSecondArgument

それぞれ実行すると次のようなタイミングとなります

useEffectを使用していないので render より前に実行される

=====(NoUseEffectComponent.tsx) function top =====
=====(NoUseEffectComponent.tsx) called =====
=====(NoUseEffectComponent.tsx) render last =====

useEffectを使用しているので render の後に実行される

=====(UseEffectComponentNoSecondArgument.tsx) function top =====
=====(UseEffectComponentNoSecondArgument.tsx) render last =====
=====(UseEffectComponentNoSecondArgument.tsx) called =====

useLayoutEffect → useEffect タイミングについて

useLayoutEffect

すべてのDOM変異後、ペイントフェーズの前に同期的に発火します。これを使用して、DOMからレイアウト(スタイルまたはレイアウト情報)を読み取り、レイアウトに基づいてカスタムDOM変異のブロックを実行します。

useEffect

レンダリングが画面にコミットされた後、つまりレイアウトとペイントフェーズの後に実行されます。視覚的な更新のブロックを避けるために、可能な限りこれを使用してください。

https://bit.ly/46jGGDZ


● 3. React hooks / useContext()

useContext() とは

Contextは、propsのバケツリレーを回避するために使用します。
グローバルなステートを管理するのに使用する Redux, Recoil とどちらを使うかについては設計段階で検討しましょう。
ReduxはMiddlewareを間に挟むことができるので、Middlewareを使いたい場合はReduxを使用します
Reduxライクで人気な zustand もおすすめです

以下の4つの要素から作成されます

・React.createContext関数でステートを作成する
・<Context.Provider> を用いて値を渡す
・<Context.Consumer> を用いて値を受け取る(re-renderの範囲を限定しやすい)
・React.useContext を用いると、Contect.Consumer と同じ役割をさせることができます(コンポーネント全体で再レンダリングが起きるのでre-render範囲を限定したい場合は、さらに子コンポーネントとして切り出す事。)

React hooksの概要 useContext による Provider|プログラムメモ


● 4. React hooks / useReducer()

useReducer() とは

引用 : https://bit.ly/3uzQaJb

useReducer で setState 関連のロジックを閉じ込める
deleteItem メソッドは、配列のうち該当する index の item を削除するメソッドであるが、こういったロジックをどこに書くかをかなり悩んできた。結論としては useReducer 内にロジックを保持するパターンが、一番疎結合である。

引用: https://bit.ly/3BeyRQw

useReducerというAPIも登場しています。 useReducerはReduxにおけるReducerのような振る舞いをします。 

引用: https://bit.ly/2Yb49ZK

useReducer が生きるのは、複数の関連したステート、つまりオブジェクトをステートとして扱うときです。

useReducerの使い方

import { useReducer } from "react";
const [state, dispatch] = useReducer(reducer, initialState);
[ステート, 発火関数 ] = useReducer(関数[, 初期値])

なお reducer は自作する必要があります。

const reducer = (state, action) => {
    
    if(action === 'INCREMENT'){
        return {count: state.count + 1}
    }else{
        return {count: state.count - 1}
    }
}

引用 : https://bit.ly/357icDb

Reduxと全く同じ使い方ですので、Reduxをさらっておくことをおすすめします。

useReducer は useState と似ている。
useState では「数値」、「文字列」、「論理値」を扱うことができるがそれを拡張して
useReducerでは「配列」、「オブジェクト」を扱えるようにする


● 5 , 6. React hooks / useMemo / useCallback

memo , useMemo , useCallback は コンポーネントのレンダリング最適化を考えるときに登場します

重たいコンポーネントの計測方法はこちらを参考にすると良いでしょう ↓ 。

【React】重い処理のあるコンポーネントはパフォーマンスチューニングまで忘れずに

・React.Memo , useMemo , useCallback の違い

以下のようにメモ化する対象が変わります。

memo → コンポーネント全体をメモ化する。
useCallback → 関数をメモ化する。(子コンポーネントに渡す関数など。)
useMemo → 変数やchildrenをメモ化する。

「メモ化」とは

コンポーネントの出力を「記憶」し、同じ入力が与えられた場合に再計算を省略するものです。キャッシュですね。

参考 : https://bit.ly/40RnZp5

Reactで再レンダリングが起きる条件

・stateが更新されたコンポーネントは再レンダリング
・propsが更新されたコンポーネントは再レンダリング
・再レンダリングされたコンポーネント配下の子コンポーネントは再レンダリングされる

引用 : https://bit.ly/34YCuyH

 親コンポーネントのレンダリングによる再レンダリングを制御する方法には以下の2つがあります。

コンポーネントをメモ化する
コンポジション(props.children)を使って子コンポーネントを渡す

memo() とは

使い方

MyChild.jsx

const MyChild = () => {
  console.log('🤙 MyChildのレンダリングが開始されました');
  return (
    <div>MyChild</div>
  );
}
export default MyChild;

 ↓ このように変更することでメモ化されます

MyChild.jsx

import { memo } from "react";
const MyChild = memo( () => {
  console.log('🤙 MyChildのレンダリングが開始されました');
  return (
    <div>MyChild</div>
  );
})
export default MyChild;

全体を memo() で囲っておきます。このようにすることで親コンポーネントに変更があった場合に MyChild は再レンダリングはされなくなります。

useMemo() とは

useMemoとは変数に対して memo化を行うものです。
useMemoは、以前行われた計算の結果をキャッシュし、useMemoが属するコンポーネントが再レンダリングしても、useMemoの第2引数(deps)が更新されていない場合は計算をスキップして以前の結果を使うという機能を提供します。

引用 : https://bit.ly/3tpOgw4

useCallback() とは

再レンダリングを抑えるための手法
useCallbackはパフォーマンス向上のためのフックで、メモ化したコールバック関数を返します。
useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算します。

引用 https://blog.uhy.ooo/entry/2021-02-23/usecallback-custom-hooks/

const App: React.VFC = () => {
  const handleClick = useCallback((e: React.MouseEvent) => {
    console.log("clicked!");
  }, []);

  return (
    // memo化されたコンポーネント
    <SuperHeavyButton onClick={handleClick} />
  );

こちらのように、SuperHeavyButtonをmemo化 + props を useCallback する事で、App コンポーネントが再レンダリングされた際にもSuperHeavyButtonはすでに生成されたものが再利用されます

・値が MyData | undefined のように 初期値 undefined な値を使って新たに変数を生成する時に使用する。

例えば ReactQuery の カスタムフックで以下のように使用してる場合に

const { data } = useMySampleQuery() // 型は  MyData | undefined
const dataWithGetter = withGetter( data ) // undefined を受け付けない場合

 ↓ useMemo を使って次のようにすることができます

const { data } = useMySampleQuery() // 型は  MyData | undefined
const dataWithGetter = withGetter( data ) // undefined を受け付けない場合

const dataWithGetter = useMemo(() => {
  return data ? withGetter( data ) : undefined;
}, [data]);

children にも useMemoは有効です
【React】メモ化したコンポーネントに children を渡すと効果がなくなる


● 7. React hooks / useRef

useRef() とは

2つの使い方があります。

1. DOMにアクセスするための ref として使用する

関数コンポーネントでは、Classコンポーネント時のref属性の代わりに、useRefを使って要素への参照を行います。

なお、propsで受け取ったrefをさらに子コンポーネントに渡す場合は forwardRef でなくてもokです。

const Child = React.forwardRef((props, ref) => {
    return (
        <div ref={ref}>DOM</div>
    );
});

const Component = () => {
    const el = useRef(null);

    useEffect(() => {
        console.log(el.current);
    }, []);

    return (
        <Child ref={el} />
    );
};

2. 再レンダリングを発生させないステートとして使用する

useStateを利用している場合はstateの変更される度にコンポーネントの再レンダリングが発生しますが、
useRefは値が変更になっても、コンポーネントの再レンダリングは発生しません。
コンポーネントの再レンダリングはしたくないけど、内部に保持している値だけを更新したい場合は、
保持したい値をuseStateではなく、useRefを利用するのが良いです。
useState と比較したとき useRef の重要な特徴は3つです。

更新による再レンダリングが発生しない
値が同期的に更新される
返却されるRefオブジェクトは同一である

引用 : https://bit.ly/3C9oMGv

こちらでは preact の useSignal との比較が記述されています
https://www.builder.io/blog/usesignal-is-the-future-of-web-frameworks

引用: https://bit.ly/3zXdqC2

refを「ref」propとして渡さないでください。これはReactで予約されている属性名であり、エラーが発生します。
代わりに、forwardRefを使用します

● preact の Signalsを react で使いたい場合はこちらを参考に

https://github.com/preactjs/signals

● その他 hooks について読んでおくと良い記事

https://zenn.dev/kobayang/articles/9145de86b20ba6

React.memo と useCallbackで state の変化に伴う{個別,共通}コンポーネントの再描画を抑制する - DEV Community

No.2055
08/14 17:19

edit

Next.js + Firebase で Googleログインのテスト

● Next.js + Firebase で Googleログインのテスト


● Next.js アプリの初期化と実行

1-1. アプリの初期化

npx create-next-app

(アプリ名を聞かれるので 「my-first-next-app」 のように入力します。)

1-2. アプリの実行

cd my-first-next-app
npm install
npm run dev

http://localhost:3000/ へ アクセスできることを確認します


● Firebaseの設定

Firebaseのインストール

yarn add firebase @types/firebase

2-1. Firebaseの新規登録

https://console.firebase.google.com/u/0/
へアクセスして「+プロジェクトの追加」をクリックします

(プロジェクト名を聞かれるので「my-first-next-app-firebase」 のように入力します。)

「続行」をクリックしてプロジェクトを作成します。

2-2. 新規登録したFirebaseのプロジェクトの設定

「ウェブ」アイコンをクリックして「ウェブアプリへの Firebase の追加」画面へ移動します。

(ニックネームを聞かれるので「my-first-next-app-apelido」 のように入力します。)

登録が完了すると firebaseConfig が表示されるのでコピーしておきます。

const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
};

コピーした後コンソールに進みます

2-3. FirebaseでGoogle認証を有効にする

一番左のメニューの「Authentication」をクリックし「始める」をクリックします。

ログインプロバイダーにGoogleを追加して有効化します。

2-4. ログインテスト用アカウントを作成しておく

「Authentication」 →「Users」からログインテスト用アカウントを作成しておきます


● Next.js アプリへFirebaseの追加

3-1. Firebaseの新規登録

npm install firebase

3-2. Firebaseアプリ初期化コンポーネントの作成

FirebaseApp.js

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey              : "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain          : "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  projectId           : "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  storageBucket       : "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  messagingSenderId   : "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  appId               : "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
};

const FirebaseApp = initializeApp(firebaseConfig);

export default FirebaseApp

3-3. Firebaseログインサンプル画面の作成

pages/login.js

import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import FirebaseApp from '../FirebaseApp';
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";

export default function Home() {

  const doLogin = () => {
    const auth = getAuth();

    // Firebaseに登録したID,PASSWORD
    const email = 'test@user.com';
    const password = 'XXXXXXXXXX';

    signInWithEmailAndPassword(auth, email, password)
      .then((userCredential) => {
        const user = userCredential.user;
        alert( 'ログインok!' );
        console.log( '● user' );
        console.log( user );
      })
      .catch((error) => {
        const errorCode = error.code;
        const errorMessage = error.message;
      });
  }

  return (
    <div className={styles.container}>
      <h1>Googleログイン</h1>
      <button onClick={doLogin}>googleでログインする</button>
    </div>
  )
}

3-4. Firebaseログインの実行

http://localhost:3000/login
にアクセスしてログインボタンをクリックすると「ログインok!」のアラートが表示されます。

● ログイン状態の永続性

import {
  getAuth,
  setPersistence,
  browserLocalPersistence,
  browserSessionPersistence,
  inMemoryPersistence
} from "firebase/auth";

const auth = getAuth()

await setPersistence(auth, browserLocalPersistence);

以下の4パターンが存在します

browserLocalPersistence (LOCAL)
browserSessionPersistence (SESSION)
indexedDBLocalPersistence (LOCAL)
inMemoryPersistence (NONE)

認証状態の永続性  |  Firebase

● 現在ログイン中かどうかを調べる

firebase.auth().currentUser

firebase v9

export async function currentUser() {
  if (firebaseAuth.currentUser) return firebaseAuth.currentUser

  return new Promise<User | null>((resolve) => {
    firebaseAuth.onAuthStateChanged((user) => {
      if (user) return resolve(user)
      else return resolve(null)
    })
  })
}
await currentUser();

に現在ログイン中のユーザ情報が帰ってきますのでそこを調べます。

● そもそも Firebase Auth の情報の保存先

ブラウザのIndexedDB
No.2052
01/30 08:59

edit

プロセスマネージャー pm2で next.js を動作させる

● 1. pm2 のインストール

-g オプションをつけてグローバルにインストールします

npm install pm2@latest -g

バージョンを確認します

pm2 --version
5.3.0

● 2-A. ワンライナーでpm2からnext.jsを起動する

npm run start コマンドを pm2 から実行します。

cd <nextjsアプリのディレクトリ >
pm2 start --name "my-next-app" npm -- start 

npm run start :staging コマンドといった任意のコマンドを pm2 から実行する場合はこちら。

pm2 start --name "my-next-app" npm -- run start:staging

● 2-B. 設定ファイルを作成して pm2からnext.jsアプリを起動する

・pm2の設定ファイルを作成する

設定ファイル自動生成コマンド( simple )

pm2 init simple 

設定ファイル自動生成コマンド( 詳細な設定 )

pm2 init

ecosystem.config.js が自動生成されますので編集します。

ecosystem.config.jsだとわかりにくい場合は /etc/pm2.d/pm2.config.js などに作成するのもいいでしょう。

cd /etc
mkdir pm2.d
chown kusanagi pm2.d
chgrp kusanagi pm2.d
module.exports = {
  apps: [
    {
      name: 'MyAppName',
      script: '/path/to/folder/server.js',
      env: {
        HOSTNAME: "0.0.0.0",
        PORT: 3000,
      },
    }
  ]
}
注意: script に指定した ファイルを node で起動するので、npm run start は指定できません。
next.js の場合だと standaloneモードで buildして server.js を指定するといいでしょう

script: '/path/to/folder/server.js',

設定ファイルの書き方 : https://pm2.keymetrics.io/docs/usage/application-declaration/

以下のコマンドでプロセスを起動します

pm2 start ecosystem.config.js

ファイル名を変更している場合はフルパスで指定します。

pm2 start /etc/pm2.d/pm2.config.js

● pm2のコマンド

pm2のログを表示する

pm2 log

プロセスの状態を見る(簡易)

pm2 ls

プロセスの状態を見る(詳細)

pm2 ls -m

全てのプロセスの停止 / 削除

pm2 stop all
pm2 delete all

「nextjs」という名前のアプリを停止する

pm2 stop nextjs

「nextjs」という名前のアプリをプロセスリストから削除する

pm2 delete nextjs

「nextjs」という名前のアプリのプロセス返す(クラスターにて複数プロセスが立ち上がっている場合は全てのプロセス ID を返す)

pm2 pid nextjs

「nextjs」という名前のアプリをリスタートする

reload を使用するとゼロダウンタイムでリスタートしようとします。(必ずそうなるわけではありませんが。こちらのコマンドがおすすめです)

pm2 reload app_name

restart コマンドはreloadよりダウンタイムが発生しやすいコマンドです。

pm2 restart app_name

● pm2 を サーバーマシン起動時に自動実行するように設定する

pm2 を自動起動させる( centos )

pm2 startup

実行後に表示されるコマンドをルート権限(または sudo できるユーザー)から実行します

● pm2自動起動時の起動プロセスを保存

pm2 を自動起動させる

pm2 save

● GRACEFUL RESTART / START / SHUTDOWN

https://pm2.keymetrics.io/docs/usage/signals-clean-restart/#cleaning-states-and-jobs
https://pm2.keymetrics.io/docs/usage/signals-clean-restart/

● クラスター化 ( 2core以上のcpuの場合にクラスター化することができます)

https://pm2.keymetrics.io/docs/usage/cluster-mode/

module.exports = {
  apps : [{
    script    : "api.js",
    // クラスター化 
    exec_mode : "cluster" ,
    instances : "max",
  }]
}

参考: https://kazuhira-r.hatenablog.com/entry/2022/01/02/151132

● pm2ログのローテーション

pm2-logrotate のインストール

pm2 install pm2-logrotate  

pm2-logrotate の設定表示

pm2 get pm2-logrotate
No.1907
09/24 23:10

edit

next.js