React
環境構築
node.jsのインストール
$ brew install anyenv
$ echo 'eval "$(anyenv init -)"' >> ~/.bash_profile $ exec $SHELL -l
$ anyenv install nodenv
$ exec $SHELL -l
$ mkdir -p $(anyenv root)/plugins
$ git clone https://github.com/znz/anyenv-update.git $(anyenv root)/plugins/anyenv-update
$ mkdir -p "$(nodenv root)"/plugins
$ git clone https://github.com/nodenv/nodenv-default-packages.git "$(nodenv root)/plugins/nodenv-default-packages"
$ touch $(nodenv root)/default-packages
default-packagesの中身
yarn typescript ts-node
typesync
$ nodenv install -l
$ nodenv install 14.4.0 # 最新バージョン
$ nodenv global 14.4.0 # 最新バージョン
Create React App
$ npx create-react-app hello-world --template typescript
$ cd hello-world
$ yarn start
ESLint導入
$ npm ls eslint # ESLintのバージョン確認
$ yarn upgrade-interactive --latest
"@types/node": "14系",
"@types/react": "16.9系",
"@types/react-dom": "16.9系",
Enter
$ yarn eslint --init
? How would you like to use ESLint?
❯To check syntax, find problems, and enforce code style
? What type of modules does your project use? JavaScript modules (import/export) ❯JavaScript modules (import/export)
? Which framework does your project use? ❯ React
? Does your project use TypeScript? ❯ Yes
? Where does your code run? ❯ Browser
? How would you like to define a style for your project? ❯Use a popular style guide
? Which style guide do you want to follow? ❯Airbnb: https://github.com/airbnb/javascript
? What format do you want your config file to be in? ❯ JavaScript
eslint-plugin-react@^7.20.0 \ @typescript-eslint/eslint-plugin@latest \
eslint-config-airbnb@latest \
eslint@^5.16.0 || ^6.8.0 || ^7.2.0 \ eslint-plugin-import@^2.21.2 eslint-plugin-jsx-a11y@^6.3.0 \ eslint-plugin-react-hooks@^4 || ^3 || ^2.3.0 || ^1.7.0 \ @typescript-eslint/parser@latest
? Would you like to install them now with npm?
❯No
ここでエラーが出ても良い
拡張ルールセットとプラグインのインストール
$ yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin \
eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import \
eslint-plugin-jsx-a11y eslint-config-airbnb
$ typesync # package.jsonを見て足りない型定義パッケージがあれば追加
$ yarn
.eslintrc.jsの設定
module.exports = {
env: {
browser: true,
es2020: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
'airbnb/hooks',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2020,
project: './tsconfig.eslint.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: [
'@typescript-eslint',
'import',
'jsx-a11y',
'react',
'react-hooks',
],
root: true,
rules: {
'lines-between-class-members': [
'error',
'always',
{
exceptAfterSingleLine: true,
},
],
// should be rewritten as `['error', { allowAsStatement: true }]` in ESLint 7 or later
// SEE: https://github.com/typescript-eslint/typescript-eslint/issues/1184
'no-void': 'off',
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: '*',
next: 'return',
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
argsIgnorePattern: '_',
ignoreRestSiblings: false,
varsIgnorePattern: '_',
},
],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'react/jsx-filename-extension': [
'error',
{
extensions: ['.jsx', '.tsx'],
},
],
'react/jsx-props-no-spreading': [
'error',
{
html: 'enforce',
custom: 'enforce',
explicitSpread: 'ignore',
},
],
// note you must disable the base rule as it can report incorrect errors
// https://stackoverflow.com/questions/63818415/react-was-used-before-it-was-defined#answer-64024916
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error"],
},
overrides: [
{
files: ['*.tsx'],
rules: {
'react/prop-types': 'off',
},
},
],
settings: {
'import/resolver': {
node: {
paths: ['src'],
},
},
},
};
.eslintignoreの設定
build/
public/
**/coverage/
**/node_modules/
**/*.min.js
*.config.js
.eslintrc.js
# https://wonwon-eater.com/ts-eslint-import-error/
tsconfig.eslint.jsonの設定
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
package.jsonに追記
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'",
"postinstall": "typesync"
},
Prettierの導入
$ yarn add -D prettier eslint-plugin-prettier eslint-config-prettier
$ yarn
.eslintrc.jsに追記
extends: [
'plugin:react/recommended',
'airbnb',
'airbnb/hooks',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:prettier/recommended',
'prettier',
'prettier/@typescript-eslint',
'prettier/react',
'prettier/standard',
],
plugins: [
'@typescript-eslint',
'import',
'jsx-a11y',
'prettier',
'react',
'react-hooks',
],
.prettierrcの設定
{
"bracketSpacing": true,
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": 'all',
"useTabs": false,
}
$ npx eslint-config-prettier .eslintrc.js
stylelintの導入
$ yarn add -D stylelint stylelint-config-standard stylelint-order stylelint-config-recess-order
.stylelintrc.jsの設定
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-recess-order',
],
plugins: [
'stylelint-order',
],
ignoreFiles: [
'**/node_modules/**',
],
rules: {
'string-quotes': 'single',
},
};
VSCode Code > Preferences > Settings >『Open Settings (JSON)』
settings.json
{
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true,
},
"editor.formatOnSave": false,
"eslint.enable": true,
"eslint.packageManager": "yarn",
}
package.jsonの設定
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "npm run lint:es && npm run lint:style",
"lint:fix": "npm run lint:es:fix && npm run lint:style:fix",
"lint:es": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"lint:es:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'",
"lint:style": "stylelint 'src/**/*.css'",
"lint:style:fix": "stylelint --fix 'src/**/*.css'",
"lint:conflict": "npx eslint-config-prettier .eslintrc.js",
"postinstall": "typesync"
},
その他の設定
$ yarn -D add eslint-plugin-prefer-arrow
plugins: [
'@typescript-eslint',
'import',
'jsx-a11y',
'prefer-arrow',
'prettier',
'react',
'react-hooks',
],
...
rules: {
...
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
'react/jsx-filename-extension': [
'error',
{
extensions: ['.jsx', '.tsx'],
},
],
...
}
$ yarn add -D husky lint-staged
package.jsonの設定
" devDependencies": {
...
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix"
],
"src/**/*.css": [
"stylelint --fix"
]
}
ChromeにReact Developer Toolsを追加
local:3000で有効にする↓
https://qiita.com/obr-note/items/395a842343bc06c1efb6
ライブラリ
Semantic UI React
導入
$ yarn add semantic-ui-react semantic-ui-css
src/index.tsxに追記
import 'semantic-ui-css/semantic.min.css'
React Hook Form
導入
$ yarn add react-hook-form
React Router(5系)
導入
$ yarn add react-router react-router-dom
$ yarn
src/index.tsxを設定
追記
import { BrowserRouter } from 'react-router-dom';
編集
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root'),
);
src/App.tsxにルーティングを設定
import { Redirect, Route, Switch } from 'react-router';
const App: React.FC = () => (
<Switch>
<Route exact path="/" component={ホーム} />
<Route path="/item/:itemId" component={各データページ} />
<Redirect push to="/" />
</Switch>
);
移動/戻る/進む ボタン
import { Link, useHistory } from 'react-router-dom';
const Item: FC = () => {
const history = useHistory();
return (
<>
// aタグ不可
<Link to="/">トップページへ</Link>
// historyを使う
<button type="button" onClick={() => history.goBack()}>
戻る
</button>
<button type="button" onClick={() => history.goForward()}>
進む
</button>
<button type="button" onClick={() => history.push('/')}>
トップページへ
</button>
</>
);
};
Redux
Reduxの導入
$ yarn add redux react-redux
$ yarn
Redux Toolkitの導入
$ yarn add @reduxjs/toolkit
$ yarn
feature/counter.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export type CounterState = {
count: number;
};
const initialState: CounterState = { count: 0 };
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
added: (state, action: PayloadAction<number>) => ({
...state,
count: state.count + action.payload,
}),
decremented: (state) => ({ ...state, count: state.count - 1 }),
incremented: (state) => ({ ...state, count: state.count + 1 }),
},
});
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import 'semantic-ui-css/semantic.min.css';
import { BrowserRouter } from 'react-router-dom';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { counterSlice } from './features/counter';
const store = configureStore({ reducer: counterSlice.reducer });
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
reportWebVitals();
containers/pages/SomePage
import React, { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { counterSlice, CounterState } from 'features/counter';
import SomePage from 'components/pages/SomePage';
const EnhancedSomePage: FC = () => {
const count = useSelector<CounterState, number>((state) => state.count);
const dispatch = useDispatch();
const { added, decremented, incremented } = counterSlice.actions;
return (
<SomePage
count={count}
add={(amount: number) => dispatch(added(amount))}
decrement={() => dispatch(decremented())}
increment={() => dispatch(incremented())}
/>
);
};
export default EnhancedSomePage;
ChromeへRedux DevToolsを追加
※createStoreに設定が必要。Redux ToolkitのconfigureStoreにはデフォルトで設定されている。
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja
Redux Toolkitにおけるreducerの書き方
※Redux Toolkitにはデフォルトでimmerが適用されている
https://immerjs.github.io/immer/docs/introduction
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { uuid } from 'uuidv4';
type Task = {
id: string;
title: string;
deadline?: Date;
createdAt: Date;
};
export type TodoState = {
todoList: { [id: string]: Task };
doneList: { [id: string]: Task };
};
export const todoSlice = createSlice({
name: 'todo',
initialState: { todoList: {}, doneList: {} } as TodoState,
reducers: {
taskCreated: ( state, action: PayloadAction<Pick<Task, 'title' | 'deadline'>>, )=>{
const id = uuid();
const createdAt = new Date();
state.todoList[id] = { ...action.payload, id, createdAt };
},
taskDone: (state, action: PayloadAction<string>) => {
const id = action.payload;
const task = state.todoList[id];
if (task) {
state.doneList[id] = { ...task };
delete state.todoList[id];
}
},
taskUpdated: (state, action: PayloadAction<Omit<Task, 'createdAt'>>) => {
const { id, ...data } = action.payload;
const task = state.todoList[id];
if (task) state.todoList[id] = { ...task, ...data };
},
},
});
uuidv4
重複しない数値をランダムに作成
導入
$ yarn add uuidv4
Firebase
https://firebase.google.com/?hl=ja
1.コンソールへ移動
2.プロジェクトを追加
3.ウェブアプリを追加
4.scriptをコピー(※重要 どこかに残しておく)
ライブラリ追加
$ yarn add firebase
App.tsx
import firebase from 'firebase';
const App: React.FC = () => {
const { env } = process;
const firebaseConfig = {
apiKey: env.REACT_APP_API_KEY,
authDomain: env.REACT_APP_AUTH_DOMAIN,
databaseURL: env.REACT_APP_DATABASE_URL,
projectId: env.REACT_APP_PROJECT_ID,
storageBucket: env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: env.REACT_APP_MESSAGING_SENDER_ID,
appId: env.REACT_APP_APP_ID,
measurementId: env.REACT_APP_MEASUREMENT_ID,
};
firebase.initializeApp(firebaseConfig);
return ...
};
REACT_APP_API_KEY=XXXXXXXXXX
REACT_APP_AUTH_DOMAIN=XXXXXXXXXX
REACT_APP_DATABASE_URL=XXXXXXXXXX
REACT_APP_PROJECT_ID=XXXXXXXXXX
REACT_APP_STORAGE_BUCKET=XXXXXXXXXX
REACT_APP_MESSAGING_SENDER_ID=XXXXXXXXXX
REACT_APP_APP_ID=XXXXXXXXXX
REACT_APP_MEASUREMENT_ID=XXXXXXXXXX
SWR
エラー回避
Warning: findDOMNode is deprecated in StrictMode.
src/index.tsx
修正前
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
修正後
ReactDOM.render(<App />, document.getElementById('root'));
Do not use Array index in keys react/no-array-index-key
linterエラー。keyはオブジェクト固有のidに設定する。
修正前
{Array.map((item, index) => (
<Item key={index} text={item.value} />
))}
修正後
{Array.map((item) => (
<Item key={item.id} text={item.value} />
))}
A form label must be associated with a control.eslint(jsx-a11y/label-has-associated-control)
linterエラー。labelはinputと紐付ける。
修正前
<label>Last Name</label>
<input placeholder="Last Name"
修正後
<label htmlFor="lastName">
Last Name
<input placeholder="Last Name" id="lastName" />
</label>
Type '() => void' is not assignable to type 'void'
TypeScriptの型エラー。型を合わせる。
修正前
const Home: FC<{ addFunction: void }> = ({ addFunction }) => (
...
);
const EnhancedHome: FC = () => {
const addFunction = () => {
...
};
return (
<>
// エラー発生箇所
<Home addFunction={addFunction} />
</>
);
};
修正後
const Home: FC<{ addFunction: () => void }> = ({ addFunction }) => (
...
);
以下同
Type '() => void' is not assignable to type 'FC<{}>'. Type 'void' is not assignable to type 'ReactElement | null'
TypeScriptの型エラー。型を合わせる。
修正前
const Home: FC<{ addFunction: FC }> = ({ addFunction }) => (
...
);
const EnhancedHome: FC = () => {
const addFunction = () => {
...
};
return (
<>
// エラー発生箇所
<Home addFunction={addFunction} />
</>
);
};
修正後
const Home: FC<{ addFunction: () => void }> = ({ addFunction }) => (
...
);
以下同
onClickでイベントが発生しないno-unused-expressions
関数の中に関数を書いてしまっている。関数を子のコンポーネントに継承したときは入れ子に注意。
修正前
const greet = () => console.log('Hello!!');
...
<button type="button" onClick={() => greet}>
push
</button>
修正後
const greet = () => console.log('Hello!!');
...
<button type="button" onClick={greet}>
push
</button>
prefer-destructuring
linterエラー。分割代入にする。
Type 'CustomType | undefined' is not assignable to type 'CustomType'
Array.prototype.find()の処理後などundefindが混じる場合はif文で型ガードする
if ( typeof TargetValue !== 'undefined' ){
// CustomTypeの場合の処理
} else {
// undefindの場合の処理
}
【今回の例】
ルーティングで/itmes/:itemIdとしたとき、配列から:itemIdに一致した要素のみ取得する
itemIdが一致する要素がない場合、undefindが返ってきている
import React, { FC } from 'react';
import { useSelector } from 'react-redux';
import { useParams, Navigate } from 'react-router-dom';
import ItemDetails from '../../components/pages/ItemDetails';
import { TodoState, TodoItemState } from '../../reducer';
const EnhancedItemDetails: FC = () => {
const { itemId } = useParams();
const content = useSelector<TodoState, TodoItemState[]>(
(state) => state.content,
);
const contentItem = content.find((element) => element.id === Number(itemId));
if (typeof contentItem !== 'undefined') {
return <ItemDetails item={contentItem} />;
}
return <Navigate to="/" replace />;
};
export default EnhancedItemDetails;
Warning: Functions are not valid as a React child.
JSXに関数を返してしまっているエラー。関数の場合は最後に()をつけないと実行されないので注意。
修正前
<div>{somefunction}</div>
修正後
<div>{somefunction()}</div>
Firebase net::ERR_CONNECTION_TIMED_OUT
単純にWifiが繋がっていなかった(物理的にインターネットと繋がっていなかった)だけ
実装例
ファイル構成
src/
components/
molecules/
something.tsx
organisms/
something.tsx
pages/
something.tsx
templates/
something.tsx
containers/
molecules/
something.tsx
organisms/
something.tsx
pages/
something.tsx
templates/
something.tsx
data/
someData.ts
features/
someSlice.ts
hooks/
useSomething.ts
App.tsx
index.tsx
テキストフォーム
テキストフォームの入力をconsoleに出力する例。
const App: FC = () => {
const [input, setInput] = useState<{ firstName: string; lastName: string }>({
firstName: '',
lastName: '',
});
const onChangeFunc = (event: React.ChangeEvent<HTMLInputElement>) => {
let { firstName, lastName } = input;
if (event.target.id === 'firstName') {
firstName = event.target.value;
} else if (event.target.id === 'lastName') {
lastName = event.target.value;
}
setInput({ firstName, lastName });
};
const onSubmitFunc = () => {
event.preventDefault();
console.log(input);
};
return (
<Form onSubmit={onSubmitFunc}>
<Form.Field>
<label htmlFor="firstName">
First Name
<input
placeholder="First Name"
id="firstName"
onChange={onChangeFunc}
/>
</label>
</Form.Field>
<Form.Field>
<label htmlFor="lastName">
Last Name
<input
placeholder="Last Name"
id="lastName"
onChange={onChangeFunc}
/>
</label>
</Form.Field>
<Button type="submit">Submit</Button>
</Form>
)
};