Open4

Vite + React + TypeScript環境構築

AVERUAVERU

ESLint、Stylelint、Prettier導入

長いのでアコーディオン化

.eslintrc.json
.eslintrc.json
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": ["airbnb", "airbnb-typescript", "airbnb/hooks", "standard-with-typescript"],
  "overrides": [],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": ["react"],
  "rules": {
    //関数や変数が定義される前に使われているとエラーになるデフォルトの機能をoff
    "no-use-before-define": "off",
    //typescript側のno-use-before-defineを使うようにする
    "@typescript-eslint/no-use-before-define": ["error"],
    "@typescript-eslint/consistent-type-definitions": ["error", "interface"], // or type
    //named exportがエラーになるので使えるようにoff
    "import/prefer-default-export": "off",
    //TypeScriptでチェックしているから不要
    "react/prop-types": "off",
    "react/require-default-props": "off",
    "react/no-unknown-property": ["error", { "ignore": ["css"] }],
    "react/react-in-jsx-scope": "off",
    "react/jsx-one-expression-per-line": "off",
    "react/function-component-definition": [
      "error",
      {
        "namedComponents": "arrow-function",
        "unnamedComponents": "arrow-function"
      }
    ],
    // devDependenciesのimportを許可
    "import/no-extraneous-dependencies": [
      "error",
      {
        "devDependencies": [
          "vite.config.ts"
        ]
      }
    ],
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ],
    // importの自動整列
    "import/order": [
      "error",
      {
        // グループ毎の並び順
        "groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"],
        // グループ毎に改行
        "newlines-between": "always",
        "pathGroupsExcludedImportTypes": ["builtin"],
        // アルファベット順・大文字小文字を区別しない
        "alphabetize": { "order": "asc", "caseInsensitive": true },
        "pathGroups": [
          {
            "pattern": "{react,react-dom/**,react-router-dom}",
            "group": "builtin",
            "position": "before"
          },
          {
            "pattern": "@src/**",
            "group": "parent",
            "position": "before"
          }
        ]
      }
    ],
    // 型情報しか使ってないimportをimport typeに強制
    "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports" }],
    //jsx形式のファイル拡張子をjsxもしくはtsxに限定
    "react/jsx-filename-extension": [
      "error",
      {
        "extensions": [".jsx", ".tsx"]
      }
    ]
  }
}
.stylelintrc.json
.stylelintrc.json
{
  "extends": ["stylelint-config-standard-scss", "stylelint-config-prettier-scss", "stylelint-config-recess-order"],
  "plugins": ["stylelint-order"],
  "ignoreFiles": ["**/node_modules/**"],
  "rules": {
    "order/properties-alphabetical-order": null
  }
}
.prettierrc
package.json
{
  "tabWidth": 2,
  "trailingComma": "none",
  "semi": false,
  "singleQuote": true,
  "printWidth": 80
}

npm scriptsの追加

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
+   "format:fix": "prettier --write --ignore-path .gitignore './**/*.{js,ts,tsx}'",
+   "format:check": "prettier --check --ignore-path .gitignore './**/*.{js,ts,tsx}'",
+   "lint": "eslint --ignore-path .gitignore './**/*.{js,ts,tsx}'",
+   "lintfix:eslint": "eslint --fix --ignore-path .gitignore './**/*.{js,ts,tsx}'",
+   "lint:stylelint": "stylelint '**/*.{css,sass,scss}'",
+   "lintfix:stylelint": "stylelint --fix '**/*.{css,sass,scss}'"
  },
},

editorconfig導入

.editorconfig
.editorconfig

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
AVERUAVERU

React Router v6 チュートリアル

https://reactrouter.com/en/main/start/tutorial

試しにTypeScriptに書き換えながらやってみる

あまりTypeScriptに触れてないのでこの機会に触ってみる
先に一部動くようになったときのコミットを貼る

https://github.com/averu/vite-react-typescript-airbnb-format/commit/fa0ac9b97671573fdfe4f03ada7030e456e8e2dc

必要なもののインストール

yarn add react-router-dom localforage match-sorter sort-by

ディレクトリ構成

bulletproof-reactを参考に下記のようなイメージで考えておく

src
├─ assets
├─ components
├─ features
├─ routes
├─ types
└─ utils

ルーティング設定

createBrowserRouterでRouterを設定

https://reactrouter.com/en/main/routers/create-browser-router

children内に書くことで簡単にルーティング設定ができる

src/routes/main.tsx
const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      { index: true, element: <Index /> },
      {
        path: 'contacts/:contactId',
        element: <Contact />,
        loader: contactLoader
      },
      {
        path: 'contacts/:contactId/edit',
        element: <EditContact />,
        loader: contactLoader,
        action: editAction
      },
      {
        path: 'contacts/:contactId/destroy',
        action: destroyAction,
        errorElement: <div>Oops! There was an error.</div>
      }
    ]
  }
])

loader, actionでデータ取得および更新

loaderでデータ取得

https://reactrouter.com/en/main/route/loader

Contact画面でloaderを追加し、useLoaderDataでデータ取得

src/routes/contact.tsx
export const loader = async ({ params }: any): Promise<ProfileType | null> => {
  return await getContact(params.contactId)
}

const Contact: FC = () => {
  const contact = useLoaderData() as ProfileType
  return (
    ...
  )
}

ルーティング設定で使用するloaderを追加

src/main.tsx
import Contact, { loader as contactLoader } from './routes/contact'
const router = createBrowserRouter([
  {
      ...
    children: [
      {
        path: 'contacts/:contactId',
        element: <Contact />,
+       loader: contactLoader,
        action: favoriteAction
      },
      ...
    ]
  }
])

actionでデータ更新

https://reactrouter.com/en/main/route/action

Edit画面でactionを追加し更新処理を実装する

src/routes/edit.tsx
export const action = async ({ request, params }: ActionFunctionArgs): Promise<Response> => {
  const formData = await request.formData()
  const updates = Object.fromEntries(formData)
  const contactId = params.contactId ?? ''
  await updateContact(contactId, updates)
  return redirect(`/contacts/${contactId}`)
}

ルーティング設定で使用するactionを追加

src/main.tsx
import EditContact, { action as editAction } from './routes/edit'
const router = createBrowserRouter([
  {
      ...
    children: [
      {
        path: 'contacts/:contactId/edit',
        element: <EditContact />,
        loader: contactLoader,
+       action: editAction
      },
      ...
    ]
  }
])

useFetcherで画面遷移せず処理する

https://reactrouter.com/en/main/hooks/use-fetcher

useFetcher.Formを使用しbuttonが押されたときにactionが動く

src/components/favorite.tsx
import { useFetcher } from 'react-router-dom'

export const action = async ({
  request,
  params
}: ActionFunctionArgs): Promise<ProfileType> => {
  const formData = await request.formData()
  const contactId = params.contactId ?? ''
  return await updateContact(contactId, {
    favorite: formData.get('favorite') === 'true'
  })
}

const Favorite: FC<{ contact: ProfileType }> = ({ contact }) => {
  const fetcher = useFetcher()
  return (
    <fetcher.Form method="post">
      <button
        type="submit"
        name="favorite"
        value={favorite === true ? 'false' : 'true'}
        aria-label={
          favorite === true ? 'Remove from favorites' : 'Add to favorites'
        }
      >
        {favorite === true ? '★' : '☆'}
      </button>
    </fetcher.Form>
  )
}

ルーティング設定で使用するactionを追加

src/routes/main.tsx
const router = createBrowserRouter([
  {
    ...
    children: [
      { index: true, element: <Index /> },
      {
        path: 'contacts/:contactId',
        element: <Contact />,
        loader: contactLoader,
+       action: favoriteAction
      },
    ...
    ]
  }
])