自分的React環境構築メモ

# React

## 環境構築
### node.jsのインストール
```zsh
$ 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
```
```zsh
$ nodenv install -l
$ nodenv install 14.4.0 # 最新バージョン
$ nodenv global 14.4.0 # 最新バージョン
```

### Create React App
```zsh
$ npx create-react-app hello-world --template typescript
$ cd hello-world
$ yarn start
```

### ESLint導入
```zsh
$ npm ls eslint # ESLintのバージョン確認
$ yarn upgrade-interactive --latest
"@types/node": "14系",
"@types/react": "16.9系",
"@types/react-dom": "16.9系",
Enter
```
```zsh
$ 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
```
ここでエラーが出ても良い

拡張ルールセットとプラグインのインストール

```zsh
$ 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の設定

```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の設定

```json
{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.js",
    "src/**/*.jsx",
    "src/**/*.ts",
    "src/**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

```

package.jsonに追記

```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に追記

```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,
}

```

```zsh
$ npx eslint-config-prettier .eslintrc.js
```

### stylelintの導入
```
$ yarn add -D stylelint stylelint-config-standard stylelint-order stylelint-config-recess-order
```

.stylelintrc.jsの設定

```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

```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の設定

```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の設定

```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を追加
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ja

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
```
https://react-hook-form.com/jp

### 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 ...
};
```
.env
https://create-react-app.dev/docs/adding-custom-environment-variables/#adding-development-environment-variables-in-env

```
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
https://swr.vercel.app/

## エラー回避
### 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<any, any> | 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>
    )
};

```