npm init vite@latest msw-ts-app
cd msw-ts-app
npm install
npm i -D msw
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;
mkdir src/models
vi src/models/User.ts
以下の内容で保存
export type User = {
id: number;
username: string;
age: number;
};
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;
vi src/mocks/browser.ts
browser.ts を以下の内容で保存する
import { setupWorker } from 'msw';
import handlers from 'mocks/handlers';
const worker = setupWorker(...handlers);
export default worker;
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>
);
次の準備されているコマンドを実行するだけでOKです
npx msw init public/ --save
これで準備が整いました。
npm run dev
http://localhost:5174/ へアクセスして、ブラウザの console を確認する
[MSW] Mocking enabled.
と表示されていれば起動は成功しています。
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;
useTransition
useDeferredValue
useId
useSyncExternalStore
useInsertionEffect
解説はこちらがよくまとまっています
https://rightcode.co.jp/blog/information-technology/react18-hooks-syain
const ChildComponent = () => {
const id = new Date().getTime();
return (
<div>
<p>{id}</p>
</div>
);
};
const schema = z.object({
name: z.string().optional(), // nameフィールドはstringかundefined
});
const schema = z.object({
name: z.string().nullable(), // nameフィールドはstringかnull
});
const schema = z.object({
name: z.string().nullish(), // nameフィールドはstringかnullかundefined
});
その他スキーマについては 忘れそうなzodスキーマメモ - Qiita
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 )
以下がミニマルな形です。
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>
)
}
React Hook Form で 出てくる register関数 は以下のようなデータが返ってきます。
{ ...register("myName") }
↓
{
name: 'myName',
onChange: ƒ,
onBlur: ƒ,
ref: ƒ
}
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,
},
です。
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;
};
};
src/graphql/queries/UsersFindAll.graphql
query UsersFindAll {
usersFindAll {
id
email
name
authProvider
authId
createdAt
updatedAt
}
}
自動生成の実行
graphql-codegen --config codegen.ts
const { data } = useUsersFindAllQuery<UsersFindAllQuery>({
endpoint: "http://localhost:4000/graphql",
fetchParams: {
headers: {
"content-type": "application/json",
"Authorization": `Bearer ${token}`,
},
},
});
const { data } = useUsersFindAllQuery();
キャッシュ時間を無限にして取得する場合。
const { data } = useUsersFindAllQuery(
{},
{ staleTime: Infinity, cacheTime: Infinity },
);
npm install react-hook-form zod
npm install @hookform/resolvers
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>;
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 }) => {
const formOptions = {
resolver: zodResolver(mysourceCreateSchema),
};
const { register, formState, handleSubmit } =
useForm<MysourceCreateSchema>(formOptions);
const { errors } = formState;
const onSubmit: SubmitHandler<MysourceCreateSchema> = (data) => {
console.log("● data");
console.log(data);
};
return (
<form 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は、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の型を厳密に制限することで、プログラムの信頼性を高めることができます。
参考 : 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>
// 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 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)
型 UserData を指定する
const { error, loading, data } = useQuery<UserData>(QUERY_VIEWER)
パッケージのインストール
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 も自動で追加されています
const config: CodegenConfig = {
...........................
hooks: { afterAllFileWrite: ['prettier --write'] }
}
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を紹介する
https://github.com/vite-pwa/vite-plugin-pwa
(Reactに 限りません。viteの プラグインなので、Vueなどviteを使用しているアプリで使えます)
npm i vite-plugin-pwa -D
すでに存在する 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.ts の plugins に以下を追加します
例: Vue3 の場合
export default defineConfig({
// VitePWA を追加する
plugins: [vue(), VitePWA(pwaOptions)],
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
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"
],
"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
React+TSプロジェクトで便利だったLint/Format設定紹介
npm i -D prettier eslint-config-prettier
vi .prettierrc.json
{
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 80,
"singleQuote": false,
"jsxSingleQuote": false,
"arrowParens": "always",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"semi": true,
"endOfLine": "lf"
}
vi .eslintrc.cjs
.eslintrc.cjs
extends に prettier を追加
{
"extends": [
"prettier"
]
}
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);
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);
何も設定していない状態だと、以下のエラーが 返ってきます。
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
yarn add @tanstack/react-query
yarn add axios
yarn add @types/axios
( nextjs の場合 ) pages/_app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default function App({ Component, pageProps }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
( 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>,
);
コンポーネント 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;
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 };
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
}, []);
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),
});
FlutterでFirestoreのサブコレクションからコレクショングループを使って横断的にドキュメントを取得する
以下のようなデータがある場合、 /news サブコレクションを横断的に検索したい場合があります。
/users(コレクション)
├── user-0001 <ドキュメント>
│ └── news(コレクション)
│ ├── news-0001 <ドキュメント>
│ └── news-0001 <ドキュメント>
│
└── user-0002 <ドキュメント>
└── news(コレクション)
├── news-0001 <ドキュメント>
└── news-0001 <ドキュメント>
const searchQuery = query(collectionGroup(firebaseDB, 'news')).withConverter(firestoreConverter)
const querySnapshot = await getDocs(searchQuery).catch(e => {
console.error(e)
})
const searchQuery = query(collectionGroup(firebaseDB, 'news'), where('ID', ' ==', 'XXXXXXXXXXX')).withConverter(
firestoreConverter
)
const querySnapshot = await getDocs(searchQuery).catch(e => {
console.error(e)
})
npm i -D @swc/jest
jest.config.jsに以下を追加する(jest.config.ts ではなく .js で記述します。)
transform: {
'^.+\\.tsx?$': '@swc/jest'
},
以上です
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>
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>
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()
}
},
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
/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)
/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)が不要な場合は取り除きましょう
データ取得方法は以下の3種類
・getDoc() 「単一のドキュメントを取得」
・getDocs() 「コレクション(複数のドキュメント)を取得」
・onSnapshot() 「コレクション(複数のドキュメント)をサブスクライブし、中のドキュメントのどれかに変更がある場合自動的に全てのコレクションを自動で再読込(購読件数分の再リクエストが発生します)」
の3種類です。
公式 : https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja
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() メソッドに「コレクションリファレンス」を突っ込みます
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()))
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>
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-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
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
先に react や nextjs アプリを作成した後で、次の2ファイルを追加します。
ディレクトリ .devcontainer を作成して、以下のファイルを作成します
<アプリの dev container 名>は好きな名前をつけます
.devcontainer/devcontainer.json
{
"name": "<アプリの dev container 名>",
"dockerComposeFile": ["../docker-compose.yml"],
"service": "app",
"workspaceFolder": "/workspace",
"settings": {},
"extensions": []
}
<コンテナ名>は好きな名前をつけます
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"
"scripts": {
"dev": "next dev",
↓
"scripts": {
"dev": "next dev -p 3011",
以上です!
npx create-remix@latest
いろいろ質問されるので選択していきます。
? What type of app do you want to create? (Use arrow keys)
テンプレートを選択するにはこちらを選択します
A pre-configured stack ready for production
https://github.com/topics/remix-stack
例 : franck-boucher / mantine-stack をインストールする場合
npx create-remix --template franck-boucher/mantine-stack
npm install dayjs
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
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().utc().format('YYYY-MM-DD HH:mm:ss'))
受け取った時刻を 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
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
(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
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/
afterEach(() => {
cleanup();
})
を実行して、
render(<App />);
を毎回初期化する必要があるようです。
速度についてはテストケースが少ない場合はさほど違いはありませんがvitestの方が若干早いです
https://github.com/minwe/jetbrains-react
https://github.com/Drapegnik/env/tree/master/jetbrains/templates
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>;
}
type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;
type ReactChild = ReactElement | string | number;
例2: isAdmin == true の場合に 私は管理者ですと表示します。
{isAdmin && <div>私は管理者です</div>}
例2: 未ログインの場合に class="hidden" とします。
className={! isLogin && 'hidden'}>
<div>
{isLogin
? 'ログイン済み'
: '未ログイン'
}
</div>
ログインしつつ、管理者の場合に「管理者です」を表示する
{isLogin ? ( isAdmin && <div>管理者です</div> )
: (null)
}
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>
{Object.keys(myObject).map(k => {
const v = myObject[k]
return (
<div> {v.name} </div>
)
})}
{console.log( 'hoge' )}
{console.log( 'fuga' )}
{(function () {
console.log('hoge')
console.log('fuga')
return null
})()}
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だと伝えたい - パンダのプログラミングブログ
next.config.js
module.exports = {
webpack: (config, { dev }) => {
if (dev) {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 200,
};
}
return config;
},
};
npx create-react-app sample-ts-app-lint --template typescript
cd sample-ts-app-lint
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D eslint-plugin-react eslint-plugin-react-hooks
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 --debug .
eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore .
DEBUG=eslint:* eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore .
(Macの場合です。 /User でgrep をかけています)
DEBUG=eslint:* eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore . 2>&1 | grep /User
eslint --print-config src/index.ts
package.jsonにも記述しておきます
"scripts": {
"lint:js": "eslint --ext .js,.ts,.jsx,.tsx --ignore-path .gitignore . ",
},
↑ yarn run lint:js で起動します
yarn add -D stylelint stylelint-config-prettier stylelint-config-standard
stylelint.config.js
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
rules: {},
}
yarn stylelint **/*.{scss,css} --ignore-path .gitignore
package.jsonにも記述しておきます
"lint:css": "stylelint **/*.{scss,css} --ignore-path .gitignore",
yarn run lint:css で実行できるようになります
huskyで消耗したくない人はこちら
husky v5 で消耗している人には simple-git-hooks がお薦め - Qiita
yarn add -D husky lint-staged npm-run-all
"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"
},
yarn lint-staged
動作が思ってたものと違う場合はこちらで実行して確認します
yarn lint-staged --debug
yarn lint-staged --help
npx husky-init && yarn
.husky/pre-commit が自動生成されます。
1. eslint . でエラーが出る状態にして
2. git aad -A
3. git commit
としてエラーが出ることを確認する
.vscode/settings.json
{
"tslint.enable": false,
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
],
}
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",
},
yarn add @mui/material @emotion/react @emotion/styled
yarn add @mui/material @mui/styled-engine-sc styled-components
エラーが出る場合は以下もインストールします
yarn add @emotion/react @emotion/styled
全てのアイコンをビルドすることになるので、ビルド時間を考えると個別に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;
(インストールには yarn 必須のようです)
yarn create frourio-app
(アプリ名は後ほどブラウザ画面から入力するのでここではこのまま入力します)
ブラウザ画面が立ち上がって構成セットを選択してインストールを実行します
vscode からフォルダを開いて、「NPMスクリプト」のメニューから「dev」を起動します
ちなみにpackage.json は以下のようになっています
"scripts": {
"dev": "npm run migrate:dev && run-p dev:*",
"dev:client": "next dev -p 8001",
"dev:server": "npm run dev --prefix server",
"dev:aspida": "aspida --watch",
..................
server/prisma/schema.prisma に記述を追加して、
// ● 追加
model Post {
id Int @id @default(autoincrement())
name String
content_name String?
created_at DateTime @default(now())
updated_at DateTime
}
マイグレーションの実行
cd server/prisma/
npx prisma migrate dev --name <マイグレーションにつける名前>
例:
npx prisma migrate dev --name add_model_post
実行後
1. server/prisma/migrations/<実行日時>_add_model_post/migration.sql というファイルが新規作成されます
2. postテーブルがdbに追加されます
Prisma Clientコードを自動で作成
npx prisma generate
実行後
1. server/node_modules/.prisma/client/index.d.ts に post の型が自動生成されます
server/node_modules/.prisma/client/index.d.ts
/**
* Model post
*/
export type post = {
id: number
name: string
content_name: string | null
created_at: Date
updated_at: Date
}
例えばエンドポイントが /post/ なら、対応する定義は server/api/post/index.ts に書きます。
cd app/server/api
mkdir posts
ディレクトリを作成すると作成したディレクトリ以下に3ファイルが自動生成されます
server/api/post/$relay.ts
server/api/post/controller.ts
server/api/post/index.ts
frourioの自動生成処理
server/api/ にディレクトリが追加されたらファイル群を自動生成
server/api/ に変更があったら $server.ts を自動生成
APIのURLをディレクトリとファイルの階層で表現する
パス変数を含むパス /tasks/{taskId}/ は、server/api/tasks/_taskId/index.ts のようになります。
_ から始まるパス変数のデフォルトの型は number | string です。明示的に指定する場合は変数名の後に @number などをつけ、_taskId@number と指定します。
server/api/posts/index.ts
import { defineController } from './$relay'
import type { Post } from '$prisma/client'
export default defineController(() => ({
get: () => {
return { status: 200, body: indexPost() }
},
}))
const indexPost = (): Post[] => {
return [
{
id: 1,
name: 'string',
content_name: 'string',
created_at: new Date,
updated_at: new Date,
}
]
}
(あとで修正しますが一旦これで作成します)
アクセスして確認します。
server/api/$api.ts に サーバーのポートが記述されているのでそれを参考にアクセスします
http://localhost:10928/api/posts
TypeScript エラーが出ている場合は一度サーバーをストップして再起動すると直ることがあります
pages/post/index.tsx
import useAspidaSWR from '@aspida/swr'
import { apiClient } from '~/utils/apiClient'
const Index = () => {
const { data } = useAspidaSWR(
apiClient.posts, {}
);
return (
<>
<h1>post/index</h1>
{data &&
<ul>
{data.map((v, i) => {
return <li key={v.id}>{v.id} : {v.name}</li>;
})}
</ul>
}
</>
)
}
export default Index
アクセスして確認します。
http://localhost:8000/post
Prismaの findMany メソッドを使ってデータ一覧を取得するように修正します
import { defineController } from './$relay'
import type { Post } from '$prisma/client'
import { PrismaClient } from '@prisma/client'
import { depend } from 'velona'
export default defineController(() => ({
get: async () => {
return { status: 200, body: await getIndexPost() }
},
}))
const prisma = new PrismaClient()
export const getIndexPost = depend(
{ prisma: prisma as { post: { findMany(): Promise<Post[]> } } },
() => {
return prisma.post.findMany()
}
)
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>
)
Reduxよりシンプルに扱えるステート管理ツールです。
ただし、ブラウザリロードするとリセットされてしまうので永続化は別途設定する必要があります。
yarn add recoil @types/recoil
知っておくといい概念
Recoliには大きく2つの概念があります。
AtomとSelectorです。
Atomは状態を管理します。状態の保持と状態の更新ができます。
SelectorはAtomで管理している状態を加工して取得することができます。
認証に関するデータストアを以下のファイル名で作成してみます
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,
})
import { RecoilRoot } from 'recoil';
<RecoilRoot>
<App />
</RecoilRoot>
useRecoilState : 1. 値の取得とSetter取得
useRecoilValue : 2. 値の取得のみ
useSetRecoilState : 3. Setterのみ(useSetRecoilStateはあくまでSetter関数だけなので、Stateの値自体を参照していない限りComponentにはRe-Renderが走らない)
import { myState } from "../recoil/atoms/myState"; // 自分で作成したRecoilState
import { useRecoilState } from 'recoil'
const [recoilAuth, setRecoilAuth] = useRecoilState(myState);
import { myState } from "../recoil/atoms/myState"; // 自分で作成したRecoilState
import { useRecoilValue } from 'recoil'
const recoilAuth = useRecoilValue(myState); // 値のみ
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,
})
https://zenn.dev/someone7140/articles/8a0414a0236142
注意 : parseCookies では http only な Cookie は 取得できません。
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], // この行を追加
})
注意 : ログインしているかどうかの情報はにローカルストレージやセッションストレージには保存しないようにしましょう。 (サーバー側に情報を持たせるべきです)
ざっくり言うとゲッターとセッターです。
次のように使用します
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
}
})
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)
引用元 : Next.js に next-sitemap を導入して超手軽にサイトマップ sitemap.xml を生成しよう | fwywd(フュード)powered by キカガク
npm install --save-dev next-sitemap
sitemap.config.js
// config for next-sitemap
module.exports = {
siteUrl: 'https://YOUR-SITE.com/',
generateRobotsTxt: true,
sitemapSize: 7000,
outDir: './public',
};
package.json
"scripts": {
"build": "next build && next-sitemap --config sitemap.config.js",
},
npm run build
/public/sitemap.xml が生成されるので Google Search Console からGoogleに読み込ませます
.gitignore
# next-sitemap が自動生成するファイルは除外する
/public/sitemap.xml
/public/robots.txt
npm install react-device-detect
{isMobile ? (
<h1>スマホ</h1>
) : (
<p>pc</p>
)}
<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>
<a className='btn' onClick={myMethod('uso800')}>ボタン</a>
このように間違った技術が存在するとどういう動作となるでしょうか?
答えはコンポーネント読み込み時に myMethod('uso800') が実行されてしまう
です。
充分気をつけましょう。
アンチパターンですが、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 オブジェクトが取得できます。(通常あまりやりませんが)
import { useRouter } from 'next/router'
const router = useRouter()
const navigatePage = () => {
alert('トップページに戻ります);
router.push('/');
}
<a onClick={navigatePage}>戻る</a>
.env.local
REACT_APP_FIREBASE_API_KEY="XXXXXXXXXXXXXXXXX"
REACT_APP_FIREBASE_AUTH_DOMAIN="XXXXXXXXXXXXXXXXX"
REACT_APP_FIREBASE_PROJECT_ID="XXXXXXXXXXXXXXXXX"
REACT_APP_FIREBASE_STORAGE_BUCKET="XXXXXXXXXXXXXXXXX"
REACT_APP_FIREBASE_MESSAGE_SENDER_ID="XXXXXXXXXXXXXXXXX"
REACT_APP_FIREBASE_APP_ID="XXXXXXXXXXXXXXXXX"
REACT_APP_FIREBASE_MEASUREMENT_ID="XXXXXXXXXXXXXXXXX"
src/FirebaseApp.tsx
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider } from "firebase/auth";
const firebaseConfig = {
apiKey : process.env.REACT_APP_FIREBASE_API_KEY,
authDomain : process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId : process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket : process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId : process.env.REACT_APP_FIREBASE_MESSAGE_SENDER_ID,
appId : process.env.REACT_APP_FIREBASE_SENDER_ID,
};
initializeApp(firebaseConfig);
export const auth = getAuth();
export const provider = new GoogleAuthProvider();
src/App.tsx
import { auth, provider } from './FirebaseApp';
import { signInWithPopup, GoogleAuthProvider } from "firebase/auth";
function App() {
const doLoginWithGoogle = () => {
signInWithPopup(auth, provider)
.then((result) => {
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential?.accessToken;
const user = result.user;
console.log('● token');
console.log(token);
console.log('● user');
console.log(user);
}).catch((error) => {
console.log('● error');
console.log(error);
});
}
return (
<div>
<h1>Googleログイン</h1>
<button onClick={doLoginWithGoogle}>googleでログインする</button>
{/* <button onClick={doLoginWithGoogle}>googleでログインする</button> */}
</div>
)
}
export default App;
https://ginpen.com/2018/12/23/array-reduce/
functional programming で map と共に必ず紹介されるメソッドです。
functional programmingの特徴をかなり簡単に紹介すると、
・「元データを更新せず元データを元に新しいデータを作成する(イミュータブル)」
・「関数に関数を渡す」
といったところです。
https://codewords.recurse.com/issues/one/an-introduction-to-functional-programming
const result = array.reduce((前回の値, 現在の値, 現在の値のインデックス, reduceによって操作している配列全て) => {
return 次の値;
}, 初期値);
Array.reduce は 「単一の値を返します」
初期値について
・初期値を渡さないと、 インデックス=1 (2番目の値)から処理を始めます。
・初期値を渡すと、最初の値から、インデックス=0 から処理を始めます。
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
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
になります。
const [state, dispatch] = useReducer(reducer, initialState);
useReducerは以下の引数を受け取ります。
・引数
reducer : stateを更新するための関数。stateとactionを受け取って、新しいstateを返す。
(state, action 共に、数値や配列やオブジェクト、どのような値も受け取ることが可能です。)
initialState : stateの初期値
・戻り値
state : state(コンポーネントの状態)
dispatch : reducerを実行するための呼び出し関数
・useReducerは、ステートに依存するロジックをステートに非依存な関数オブジェクト(dispatch)で表現することができる点が本質である。
このことはReact.memoによるパフォーマンス改善につながる。
・useReducerを活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。
React は state の変更の有無の判定に Object.is() を使うとのことなので、現在の state を表す array を直接変更するのではなく別の array を新たに生成して setBookmarks() に渡せば OK です。
npm run build
next バージョンの確認
npx next --version
こちらのように /api/ 以下のファイルはサーバーサイドとなりますので取り除きます
Page Size First Load JS
├ λ /api/hello 0 B 86.6 kB
module.exports = {
reactStrictMode: true,
}
↓
module.exports = {
reactStrictMode: true,
trailingSlash: true,
}
npm run build ; npx next export
エクスポートが成功すると out ディレクトリに htmlファイルが生成されるので表示して確認します。 php でサーバを起動する場合は
php -S 0.0.0.0:8000 -t out
で http://localhost:8000/ へアクセスして確認します。
next export
. エラーの修正<Image> タグを使用していると 静的サイトエクスポートできないのでエラーとなります。
代わりに <img>タグに戻しましょう
// import Image from 'next/image'
あわせて .eslintrc.json の設定も変更します
{
"extends": "next/core-web-vitals"
}
↓
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}
npm install react-scroll
const scrollToTop = () => {
scroll.scrollToTop();
};
<a onClick={scrollToTop}><div>戻る</div></a>
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}
/>
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>
)
}
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} />
npm install sass --save-dev
cd styles
mkdir scss
touch scss/style.scss
styles/scss/style.scss をトップのscssとします
style.scss を _app.js から読み込みます
pages/_app.js
_app.js からの読み込みを style.scss に変更します
import '../styles/globals.css'
↓
import '../styles/scss/style.scss'
なお、scss内で画像を使用するときは絶対パスを記述して、画像ファイルは /public/ 以下に置きます
次のようにstyleタグに jsx global を付けることで直接 CSS を記述するのと同じように記述できます
<style jsx global>{`
.my-class {
background-color: green;
}
`}</style>
拡張子 「 .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}>
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)} />
TypeScript で JSON オブジェクトに型情報を付加する|まくろぐ
https://app.quicktype.io/?l=ts
https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype
デモ : 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" /> ,
},
];
デモ : 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>
);
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';
useRoutes() を使って作成します。Vue-Router に少し近い記述になります。
npx create-react-app my-router-ts-app --template typescript
cd my-router-ts-app
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
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
https://dev-yakuza.posstree.com/react/create-react-app/react-router/
https://stackoverflow.com/questions/69843615/switch-is-not-exported-from-react-router-dom
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/
forwardRef , useImperativeHandle を使います
import React, { useImperativeHandle, forwardRef, useRef } from "react";
https://ja.reactjs.org/docs/hooks-reference.html#useimperativehandle
useImperativeHandle は forwardRef と組み合わせて使います。
useImperativeHandle は コンポーネントにメソッドを生やす Hooks です
import { useImperativeHandle, forwardRef } from "react";
useImperativeHandle(
ref,
() => ({
showAlert() {
alert("Child Function Called")
}
}),
)
const MyChildComponent = (props) => {
↓
const MyChildComponent = (props, ref) => {
export default MyParentComponent;
↓
export default forwardRef(MyParentComponent);
import { useRef } from "react";
const childRef = useRef();
<MyChildComponent
ref={childRef}
/>
後は親コンポーネントの好きなところから関数を呼ぶことができます
<button type="button"
onClick={ () => { childRef.current.showAlert() }}
>
関数コンポーネントはクラスとどう違うのか? — Overreacted
子コンポーネントのuseImperativeHandle を次のようにします。
useImperativeHandle(
ref,
() => (
{
showAlert: async function(){ await showAlert(); } ,
}
)
)
また showAlert() 関数にも async をつけておきます
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;
next.config.js
module.exports = {
distDir: '.next',
}
シェルから環境設定をセットすると 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
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;
}
const FullHeightPage = () => (
<div>
Hello World!
<style global jsx>{`
html,
body,
body > div:first-child,
div#__next,
div#__next > div {
height: 100%;
}
`}</style>
</div>
)
URL
http://snowtooth.moonhighway.com/
エンドポイントURL
https://countries.trevorblades.com
GraphQLクエリ
query {
countries{
code
name
emoji
}
}
こちらの「エンドポイントURL」「GraphQLクエリ」を入力して ▶︎のボタンをクリックするとGraphQLクエリーが実行されます
開発しているアプリケーションの一番上の階層からnpmでインストールします
npm install @apollo/client graphql
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;
/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://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo
[GraphQL] TypeScript+VSCode+Apolloで最高のDXを手に入れよう | DevelopersIO
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" を使う
https://www.apollographql.com/docs/react/data/queries/
Name | Description |
---|---|
|
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. |
|
Apollo Client executes the query only against the cache. It never queries your server in this case. A |
|
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. |
|
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. |
|
Similar to |
|
Uses the same logic as |
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;")}
/>
)
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 );
Next.JSのVS Code を使ったデバッグは次の2ステップのみでとても簡単に行うことができます
VS Codeの
「エクスプローラー」→「NPMスクリプト」 →「dev」の横の ▶︎ をクリックしてローカルサーバーを起動する。
最初に 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」デバッグを終了
のお決まりのキーボード操作をしてください
jsconfig.json (プロジェクトトップディレクトリにこのファイルがない場合は新規で作成します)
{
"compilerOptions": {
// import時のパスのルートを設定
"baseUrl": "."
},
"include": ["**/*.js", "**/*.jsx"]
}
VS Codeの「F12」てのファイル移動も有効です
npm install react-hook-form
または
yarn add 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={フォーム部品コンポーネント }> を使う (上記以外の複雑なコンポーネントはこちらを使用します)
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>
);
<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>}
https://react-hook-form.com/jp/api#register
以下のコンポーネントがおすすめです。
https://ui.shadcn.com/docs/components/input
https://mui.com/material-ui/react-text-field/
https://chakra-ui.com/docs/components
バリデーションの記述を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です。
https://github.com/react-hook-form/react-hook-form/pull/9261
外部からフォームの値を変更するには以下の2つの方法を使用します。
( なお、useState は使用できません )
( MUI を使用している場合でも問題なくこちらの方法で値を流し込むことができます。(ただしバージョンが古いとうまく動作しないのでできるだけ新しいバージョンにアップデートしましょう。
過去にreact-hook-form@7.3.0 ではうまく動作しませんでした。)
次の二箇所にデータを流し込む命令をセットします
// ● 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);
// ● 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>
{...register('hoge')} の記述が抜けている可能性がありますチェックしましょう。
<input type="text" id="hoge" {...register('hoge')} />
自作関数を使う場合このように記述できます
function myPasswordCheck(){
return false
}
.test(
"mypassword",
"パスワードが間違っています",
myPasswordCheck
)
https://github.com/jquense/yup/issues/503
createError() を返せばokです。createErrorファンクション自体は渡してあげる必要があります。
return createError({
message: `パスワードは半角英字、数字、記号を組み合わせた 8文字以上で入力してください (${password})`,
});
https://react-hook-form.com/api/useformstate
https://qiita.com/bluebill1049/items/f838bae7f3ed29e81fff
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
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
onClick = { () => {
handleSubmit(onSubmit)()
}}
https://github.com/alibaba/hooks
import { useThrottleFn } from 'ahooks'
const { run: throttledHandleSubmit } = useThrottleFn(
() => {
handleSubmit(onSubmit)()
},
{ wait: 2000 }
)
onClick = { throttledHandleSubmit }
これは Spread Attributes(スプレッド構文) といって ES6 Javascript での記法です。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} />
}
子コンポーネントに渡したい値に変更があった場合に変更箇所が少なくて済みます。
props に hogee を追加する場合でも提示する箇所のみの変更でokです。
const props = { foo: "foo", bar: "bar", hoge:"hoge" };
render() {
return <Child {...props} />
}
npm install react-nl2br -S
const nl2br = require('react-nl2br');
jsxで以下のように記述します
my_textに入っている文字列の改行コードをbrタグに変換します
return(
<div>
{ nl2br(my_text) }
</div>
);
次世代のパッケージ 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
// date-fns
function Date({ dateString }) {
return <time dateTime={dateString}>{format(parseISO(dateString), 'yyyy.MM.dd (EEEE)', {locale:ja} )}</time>
};
<Date dateString={my_date} />
npm install react-icons --save
Mycompoenent.js
import { FaGithub } from "react-icons/fa"
function MyComponent() {
return (
<div>
<FaGithub />
</div>
)
}
yarn add swr
yarn add axios @types/axios
または
npm i swr -S
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
});
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>
);
}
<div>
<NewsIndex/>
</div>
これで URL「https://YOUR/SERVER/PATH/api/news/」が返す json (中身はコレクション)の id と name を一覧で表示します。
デフォルトでは自動的に 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
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);
});
npx create-next-app next-tailwind -e with-tailwindcss
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
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: [],
}
「状態を持つ変数」と「更新する関数」を管理するReactフックです。
「状態を持つ変数」の値が変わると useState を宣言したコンポーネントで再レンダリングが起こります。
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..."
/>
const [member, setMember] = useState({ name: "", part: "" });
const [isLogined, setIsLogined] = useState<boolean>(true);
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
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>;
};
https://zenn.dev/catnose99/scraps/30c623ba72d6b5
以下のようなタイミングの違いがあります
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
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 =====
Contextは、propsのバケツリレーを回避するために使用します。
グローバルなステートを管理するのに使用する Redux, Recoil とどちらを使うかについては設計段階で検討しましょう。
ReduxはMiddlewareを間に挟むことができるので、Middlewareを使いたい場合はReduxを使用します
Recoilでいいという意見もあります
以下の4つの要素から作成されます
・React.createContext関数でステートを作成する
・<Context.Provider> を用いて値を渡す
・<Context.Consumer> を用いて値を受け取る(再レンダリングが起きないので、パフォーマンスを気にする場合はこちら)
・React.useContext を用いると、Contect.Consumer と同じ役割をさせることができます(再レンダリングが起きやすい)
値を更新する関数も createContext で同じオブジェクトに入れています。(分けても良い)
type MyContextValue = {
myvalue: number // 値
setMyvalue: VoidFunction // 値を更新する関数
}
export const MyContext = createContext<MyContextValue>({} as MyContextValue) // デフォルト値を {} とする。
const [myvalue, setMyvalue] = useState<number>(0)
const setMyvalueFunction = () => {
setMyvalue(myvalue + 1)
}
const values = {
myvalue: myvalue,
setMyvalue: setMyvalueFunction
}
return(
<MyContext.Provider value={values}>
...(ここに値を受け取りたいコンポーネントを記述する)
</MyContext.Provider>
)
( ↓ の React.useContext を使う方がソースが見やすくなりますが、再レンダリングが起きないのが特徴です。)
const SampleSub1 = () => {
return (
<>
<MyContext.Consumer>
{value => {
return (
<>
<h1>Hello SampleSub1!</h1>
<h3>{value.myvalue}</h3>
</>
)
}}
</MyContext.Consumer>
</>
)
}
こちらの方が記述がシンプルになるのでおすすめです。
const SampleSub2 = () => {
const { myvalue } = useContext(MyContext)
return (
<>
<h1>Hello SampleSub2!</h1>
<h3>{myvalue}</h3>
</>
)
}
useReducer で setState 関連のロジックを閉じ込める
deleteItem メソッドは、配列のうち該当する index の item を削除するメソッドであるが、こういったロジックをどこに書くかをかなり悩んできた。結論としては useReducer 内にロジックを保持するパターンが、一番疎結合である。
useReducerというAPIも登場しています。 useReducerはReduxにおけるReducerのような振る舞いをします。
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}
}
}
Reduxと全く同じ使い方ですので、Reduxをさらっておくことをおすすめします。
useReducer は useState と似ている。
useState では「数値」、「文字列」、「論理値」を扱うことができるがそれを拡張して
useReducerでは「配列」、「オブジェクト」を扱えるようにする
useMemo useCallback は コンポーネントのレンダリング最適化や無限ループ防止を考えるときに登場します
useMemoは関数の戻り値をメモ化します。
useCallbackは関数そのものをメモ化します。
useCallbackはuseMemoの亜種で、関数に特化しています
ファンクションの場合にuseCallbackを、それ以外の値の場合にuseMemoを使用します。
Reactで再レンダリングが起きる条件
・stateが更新されたコンポーネントは再レンダリング
・propsが更新されたコンポーネントは再レンダリング
・再レンダリングされたコンポーネント配下の子コンポーネントは再レンダリングされる
コンポーネントをメモ化する
コンポジション(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)が更新されていない場合は計算をスキップして以前の結果を使うという機能を提供します。
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はすでに生成されたものが再利用されます
2つの使い方があります。
関数コンポーネントでは、Classコンポーネント時のref属性の代わりに、useRefを使って要素への参照を行います。
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} />
);
};
useStateを利用している場合はstateの変更される度にコンポーネントの再レンダリングが発生しますが、
useRefは値が変更になっても、コンポーネントの再レンダリングは発生しません。
コンポーネントの再レンダリングはしたくないけど、内部に保持している値だけを更新したい場合は、保持したい値をuseStateではなく、useRefを利用するのが良さそうです。
useState と比較したとき useRef の重要な特徴は3つです。
更新による再レンダリングが発生しない
値が同期的に更新される
返却されるRefオブジェクトは同一である
こちらでは preact の useSignal との比較が記述されています
https://www.builder.io/blog/usesignal-is-the-future-of-web-frameworks
refを「ref」propとして渡さないでください。これはReactで予約されている属性名であり、エラーが発生します。
代わりに、forwardRefを使用します
https://zenn.dev/kobayang/articles/9145de86b20ba6
React.memo と useCallbackで state の変化に伴う{個別,共通}コンポーネントの再描画を抑制する - DEV Community
npx create-next-app
(アプリ名を聞かれるので 「my-first-next-app」 のように入力します。)
cd my-first-next-app
npm install
npm run dev
http://localhost:3000/ へ アクセスできることを確認します
Firebaseのインストール
yarn add firebase @types/firebase
https://console.firebase.google.com/u/0/
へアクセスして「+プロジェクトの追加」をクリックします
(プロジェクト名を聞かれるので「my-first-next-app-firebase」 のように入力します。)
「続行」をクリックしてプロジェクトを作成します。
「ウェブ」アイコンをクリックして「ウェブアプリへの Firebase の追加」画面へ移動します。
(ニックネームを聞かれるので「my-first-next-app-apelido」 のように入力します。)
登録が完了すると firebaseConfig が表示されるのでコピーしておきます。
const firebaseConfig = {
apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
};
コピーした後コンソールに進みます
一番左のメニューの「Authentication」をクリックし「始める」をクリックします。
ログインプロバイダーにGoogleを追加して有効化します。
「Authentication」 →「Users」からログインテスト用アカウントを作成しておきます
npm install 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
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>
)
}
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 Authentication これだけは覚えておけ!テストに出るぞ! - Qiita
firebase.auth().currentUser
firebase v9
await currentUser();
に現在ログイン中のユーザ情報が帰ってきますのでそこを調べます。
ブラウザのIndexedDB
-g オプションをつけてグローバルにインストールします
npm install pm2@latest -g
バージョンを確認します
pm2 --version
5.1.2
cd <nextjsアプリのディレクトリ >
pm2 start npm --name "my-next-app" -- start
プロセスの状態を見る
pm2 ls
「nextjs」という名前のアプリを停止する
pm2 stop nextjs
「nextjs」という名前のアプリをプロセスリストからする
pm2 delete nextjs
「nextjs」という名前のアプリをリスタートする
pm2 restart app_name
pm2 を自動起動させる( centos )
pm2 startup
pm2 を自動起動させる
pm2 save