1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

自分的React環境構築メモ

Posted at

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 ...
};

.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

エラー回避

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>
    )
};

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?