Open4
Vite + React + TypeScript環境構築
リポジトリ
参考
目指すアウトプット
自分なりの開発環境を模索してみる
開発環境
構築環境
- Windows 11
- WSL2(Ubuntu)
- Node.js v16.19.0
- yarn 1.22.19
- VSCode
機能
- Vite
- React
- TypeScript
- ESLint
- airbnb
- Prettiter
- Stylelint
- vscode/setting.json
- editorconfig
Vite + React + TypeScript導入
yarn create vite vite-react-typescript-airbnb-format
--template react-ts
cd vite-react-typescript-airbnb-format
yarn
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
React Router v6 チュートリアル
試しにTypeScriptに書き換えながらやってみる
あまりTypeScriptに触れてないのでこの機会に触ってみる
先に一部動くようになったときのコミットを貼る
必要なもののインストール
yarn add react-router-dom localforage match-sorter sort-by
ディレクトリ構成
bulletproof-reactを参考に下記のようなイメージで考えておく
src
├─ assets
├─ components
├─ features
├─ routes
├─ types
└─ utils
ルーティング設定
createBrowserRouterで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でデータ取得
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でデータ更新
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で画面遷移せず処理する
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
},
...
]
}
])