20210504のReactに関する記事は16件です。

React/Material-UIで簡単なダッシュボード、

@material-ui/iconsも使ってみた。 Dashboard.js import React, { useState } from "react"; import { AppBar, Toolbar, IconButton, Typography, Drawer, List, ListItem, ListItemIcon, ListItemText, } from "@material-ui/core"; import MenuIcon from "@material-ui/icons/Menu"; import TimelineIcon from "@material-ui/icons/Timeline"; import TableChartIcon from "@material-ui/icons/TableChart"; import MapIcon from "@material-ui/icons/Map"; const Dashboard = () => { const [state, setstate] = useState(false); const handleMenuIcon = () => { setstate(!state); console.log(state); }; return ( <div> <AppBar position="static"> <Toolbar> <IconButton onClick={handleMenuIcon}> <MenuIcon /> </IconButton> <Typography>Dashboard</Typography> </Toolbar> </AppBar> <Drawer open={state} onClose={handleMenuIcon}> <List> <ListItem> <ListItemIcon> <TimelineIcon /> <ListItemText primary="AAA" /> </ListItemIcon> </ListItem> <ListItem> <ListItemIcon> <TableChartIcon /> <ListItemText primary="BBB" /> </ListItemIcon> </ListItem> <ListItem> <ListItemIcon> <MapIcon /> <ListItemText primary="CCC" /> </ListItemIcon> </ListItem> </List> </Drawer> </div> ); }; export default Dashboard;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/Material-UIでappbar+drawer

MyDashboard.js import React, { useState } from "react"; import { AppBar, Drawer, IconButton, List, ListItem, Toolbar, Typography, } from "@material-ui/core"; import MenuIcon from "@material-ui/icons/Menu"; const MyDashboard = () => { const [state, setstate] = useState(false); const handleClick = () => { setstate(!state); }; console.log(state); return ( <div> <AppBar position="static"> <Toolbar> <IconButton onClick={handleClick}> <MenuIcon /> </IconButton> <Typography>My Dashboard</Typography> </Toolbar> </AppBar> <Drawer open={state} onClose={handleClick}> <List> <ListItem>AAAAA</ListItem> <ListItem>BBBBB</ListItem> <ListItem>CCCCC</ListItem> </List> </Drawer> </div> ); }; export default MyDashboard;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/Material-UIでdrawer

MyDrawer.js import React, { useState } from "react"; import { Button, Drawer, List, ListItem } from "@material-ui/core"; const MyDrawer = () => { const [state, setstate] = useState(false); const flag = () => { setstate(!state); console.log(state); }; return ( <div> <Button variant="contained" color="primary" onClick={flag}> Open Drawer </Button> <Drawer open={state} onClose={flag}> <List> <ListItem>AAA</ListItem> <ListItem>BBB</ListItem> <ListItem>CCC</ListItem> </List> </Drawer> </div> ); }; export default MyDrawer;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel + React(TypeScript)の開発環境を構築する方法 ~DB接続まで~

はじめに この記事では、LaravelとReact(TypeScript)の開発環境構築と、DB接続までできるようになります。 React(TypeScript)環境構築の部分については、下記の動画で紹介されている手順とほぼ同じですが、メモも兼ねてまとめさせていただきました。 LaravelにReact.jsとTypeScriptをインストール:React.js + LaravelでSPA開発講座#08 開発環境 Windows10 XAMPP php version 7.4.18 laravel version 8.40.0 事前準備 以下の項目のインストールが済んでいない場合、下記のサイトを参考にインストールを完了してください。 「php」のインストール 「node.js」のインストール 「composer」と「XAMPP」のインストール 環境構築 Laravelのインストール 任意のフォルダを作成&フォルダの階層まで移動し、VScodeのターミナルで以下のコマンドを実行 $ composer create-project laravel/laravel example-app "8.*" 作成されたプロジェクトの階層に移動、インストールされたlaravelのバージョンを確認  $ cd example-app $ php artisan -V Laravel Framework 8.40.0 と表示されていればOKです。 ReactとTypeScript のセットアップ LaravelのプロジェクトでReact(TypeScript)を扱えるようにします。 TypeScriptとSCSSでビルドするように、webpack.mix.jsを以下の様に書き換え ※webpack.mix.jsはexample-appディレクトリ直下に配置されています webpack.mix.js const mix = require('laravel-mix'); /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel applications. By default, we are compiling the CSS | file for the application as well as bundling up all the JS files. | */ mix.ts('resources/ts/index.tsx', 'public/js') .sass('resources/sass/app.scss', 'public/css'); ターミナルでnpm install実行 $ npm install ビルドを実行 $ npm run prod Reactのインストール $ npm i -D react react-dom @types/react @types/react-dom 再度npm install $ npm install tsconfig.jsonファイル を作成 ※example-appディレクトリ直下に作成される $ ./node_modules/.bin/tsc --init tsconfig.jsonファイルの内容を下記に書き換え tsconfig.json { "compilerOptions": { "target": "es5", "module": "commonjs", "jsx": "react", "strict": true, "esModuleInterop": true } } reourcesディレクトリ直下にtsディレクトリを新しく作成、作成したtsディレクトリ直下にindex.tsxファイルを作成 index.tsxに下記の内容を記載 index.tsx import React from 'react' import ReactDOM from 'react-dom' const App = () => { return ( <h1>Laravel SPA</h1> ) } ReactDOM.render( <App />, document.getElementById('app') ) resourcesディレクトリ直下のcssディレクトリとapp.cssフォルダの名前と拡張子を変更 変更前 変更後 再度ビルドを実行 $ npm run prod viewディレクトリ直下のwelcome.blade.phpの内容を下記の様に変更 welcome.blade.php <!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> <link rel="stylesheet" href="/css/app.css"> </head> <body> <div id="app"></div> </body> <script src="/js/index.js"></script> </html> VScodeの「Split Terminal」で2つのターミナルを起動し、それぞれ下記のコマンドを実行する $ php artisan serve $ npm run watch ブラウザが起動し、下記の画面が表示されたらOK DB接続 データベース作成 「XAMPP」を起動し、ApatcheとMySQLの「Start」を押下 MySQLの「Admin」を押下する。ブラウザが起動しAdmin画面が表示されたら、「データベース」タブを押下 任意のデータベース名を入力し、「作成」ボタンを押下 ※ここではexample_appと入力 データベースが作成され、テーブルが無い趣旨のメッセージが表示される envファイル変更&マイグレーション example-appディレクトリ直下にある、.envファイルの「DB_DATABSE=laravel」を接続したいデータベース名に変更 ※ここではexample_appと入力 ターミナルで下記のコマンドを実行 $ php artisan migrate adminを再度確認し、下記の様にテーブルが作成できていればOK 以上が環境構築からDB接続までの流れになります。次はこの環境でaxiosを利用したCRUD処理の記事を書ければなと思います。 参考文献 LaravelにReact.jsとTypeScriptをインストール:React.js + LaravelでSPA開発講座#08 【Windows編】今すぐ簡単にできる!PHPのダウンロード方法 Node.jsの開発環境を用意しよう!(Windows) Windows Laravelの環境構築の手順書
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-appは外部ディスクで実行してはいけないという戒め

Reactのローカル環境を構築して、いざアプリを作成しようとした際にエラーでハマった。 結論 create-react-appコマンドはローカルディスク上で実行しましょう。 エラー内容は以下。 $ npx create-react-app my-app Creating a new React app in /Users/XXX/Box/Programming/JavaScript/my-app. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts with cra-template... npm ERR! cb() never called! npm ERR! This is an error with npm itself. Please report this error at: npm ERR! <https://github.com/npm/cli/issues> cb()という関数が呼ばれてないというエラーが発生している模様。 アタッチしているBOXドライブ上にあるフォルダ配下でアプリを作成しようとした結果、上記のエラーが発生した。 ローカルディレクトリのダウンロードフォルダ内で同様のコマンドを実行したところ問題なくアプリ作成ができたので、BOXドライブ上とローカルディレクトリ上で出来ることが違うっぽい・・・? BOXドライブはPCにアタッチされてるから特に問題なく使用できると思ったんだけど、そうではないらしい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/Material-UI/axios/chart.js&react-chartjs-2で簡単なダッシュボード

Reactプロジェクトの作成 $ npx create-react-app ~/Desktop/iris-dashboard $ cd ~/Desktop/iris-dashboard $ cp -irp ~/Desktop/iris.json ~/Desktop/iris-dashboard/public $ npm install @material-ui/core $ npm install axios $ npm install --save react-chartjs-2 chart.js $ npm start 実装 ~/Desktop/iris-dashboard/src/components/IrisDashboard.js import React, { useState, useEffect, createContext } from "react"; import { AppBar, Toolbar, Typography, Container, Grid, InputLabel, Select, MenuItem, FormControl, } from "@material-ui/core"; import axios from "axios"; import IrisScatter from "./IrisScatter"; export const IrisContext = createContext([]); const IrisDashboard = () => { const [iris, setIris] = useState([]); const [xaxis, setXaxis] = useState("SepalLength"); const [yaxis, setYaxis] = useState("PetalLength"); useEffect(() => { axios.get("./iris.json").then((r) => { setIris(r.data); }); }, []); console.log(iris); const handleXaxisChange = (e) => { setXaxis(e.target.value); }; console.log(xaxis); const handleYaxisChange = (e) => { setYaxis(e.target.value); }; console.log(yaxis); return ( <div> <IrisContext.Provider value={iris}> <AppBar position="static"> <Toolbar> <Typography>Iris Dashboard</Typography> </Toolbar> </AppBar> <Container> <FormControl> <InputLabel id="xaxisLabel">xaxis</InputLabel> <Select labelId="xaxisLabel" value={xaxis} onChange={handleXaxisChange} > <MenuItem value="SepalLength">SepalLength</MenuItem> <MenuItem value="SepalWidth">SepalWidth</MenuItem> <MenuItem value="PetalLength">PetalLength</MenuItem> <MenuItem value="PetalWidth">PetalWidth</MenuItem> </Select> </FormControl> <FormControl> <InputLabel id="yaxisLabel">yaxis</InputLabel> <Select labelId="yaxisLabel" value={yaxis} onChange={handleYaxisChange} > <MenuItem value="SepalLength">SepalLength</MenuItem> <MenuItem value="SepalWidth">SepalWidth</MenuItem> <MenuItem value="PetalLength">PetalLength</MenuItem> <MenuItem value="PetalWidth">PetalWidth</MenuItem> </Select> </FormControl> <Grid container> <Grid item xs={12}> <IrisScatter xaxis={xaxis} yaxis={yaxis} /> </Grid> </Grid> </Container> </IrisContext.Provider> </div> ); }; export default IrisDashboard; ~/Desktop/iris-dashboard/src/components/IrisScatter.js import React, { useContext } from "react"; import { Scatter } from "react-chartjs-2"; import { IrisContext } from "./IrisDashboard"; const IrisScatter = (props) => { console.log(props.xaxis); console.log(props.yaxis); const iris = useContext(IrisContext); console.log(iris); const data = { datasets: [ { label: "Iris-setosa", data: iris .filter((x) => { return x.Class == "Iris-setosa"; }) .map((x) => { return [x[props.xaxis], x[props.yaxis]]; }), borderColor: "rgba(255, 0, 0, 1)", backgroundColor: "rgba(255, 0, 0, 0.5)", pointRadius: 8, }, { label: "Iris-versicolor", data: iris .filter((x) => { return x.Class == "Iris-versicolor"; }) .map((x) => { return [x[props.xaxis], x[props.yaxis]]; }), borderColor: "rgba(0, 255, 0, 1)", backgroundColor: "rgba(0, 255, 0, 0.5)", pointRadius: 8, }, { label: "Iris-virginica", data: iris .filter((x) => { return x.Class == "Iris-virginica"; }) .map((x) => { return [x[props.xaxis], x[props.yaxis]]; }), borderColor: "rgba(0, 0, 255, 1)", backgroundColor: "rgba(0, 0, 255, 0.5)", pointRadius: 8, }, ], }; console.log(data); return <Scatter data={data} />; }; export default IrisScatter; ~/Desktop/iris-dashboard/src/App.js import logo from "./logo.svg"; import "./App.css"; import IrisDashboard from "./components/IrisDashboard"; function App() { return ( <div className="App"> <IrisDashboard /> </div> ); } export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/Material-UIで簡単なUI

Material-UIのインストール $ npm install @material-ui/core Material-UIでUI作成 ~/Desktop/sample-app/src/components/MySample.js import React from "react"; import { AppBar, Container, Toolbar, Typography, Grid, } from "@material-ui/core"; const MySample = () => { return ( <div> <AppBar position="static"> <Toolbar> <Typography>Iris Dashboard</Typography> </Toolbar> </AppBar> <Container> <Grid container> <Grid item xs={12}> AAA </Grid> <Grid item xs={6}> AAA </Grid> <Grid item xs={6}> BBB </Grid> <Grid item xs={4}> AAA </Grid> <Grid item xs={4}> BBB </Grid> <Grid item xs={4}> CCC </Grid> </Grid> </Container> </div> ); }; export default MySample; ~/Desktop/sample-app/src/App.js import logo from "./logo.svg"; import "./App.css"; import MySample1 from "./components/MySample"; function App() { return ( <div className="App"> <MySample /> </div> ); } export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/chart.js&react-chartjs-2で散布図

chart.js&react-chartjs-2をインストール $ npm install --save react-chartjs-2 chart.js chart.sj&react-chartjs-2で散布図 ~/Desktop/sample-app/src/components/MySample.js import React, { useState, useEffect } from "react"; import axios from "axios"; import { Scatter } from "react-chartjs-2"; const MySample = () => { const [iris, setIris] = useState([]); useEffect(() => { axios.get("http://localhost:3000/iris.json").then((r) => { setIris(r.data); }); }, []); console.log(iris); const xaxis = "SepalLength"; const yaxis = "PetalLength"; const data = { datasets: [ { label: "Iris-setosa", data: iris .filter((x) => { return x.Class == "Iris-setosa"; }) .map((x) => { return [x[xaxis], x[yaxis]]; }), borderColor: "rgba(255, 0, 0, 1)", backgroundColor: "rgba(255, 0, 0, 0.5)", pointRadius: 8, }, { label: "Iris-versicolor", data: iris .filter((x) => { return x.Class == "Iris-versicolor"; }) .map((x) => { return [x[xaxis], x[yaxis]]; }), borderColor: "rgba(0, 255, 0, 1)", backgroundColor: "rgba(0, 255, 0, 0.5)", pointRadius: 8, }, { label: "Iris-virginica", data: iris .filter((x) => { return x.Class == "Iris-virginica"; }) .map((x) => { return [x[xaxis], x[yaxis]]; }), borderColor: "rgba(0, 0, 255, 1)", backgroundColor: "rgba(0, 0, 255, 0.5)", pointRadius: 8, }, ], }; console.log(data); return <Scatter data={data} />; }; export default MySample; ~/Desktop/sample-app/src/App.js import logo from "./logo.svg"; import "./App.css"; import MySample from "./components/MySample"; function App() { return ( <div className="App"> <MySample /> </div> ); } export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/axiosでget+useState

~/Desktop/sample-app/src/components/MySample.js import React, { useState, useEffect } from "react"; import axios from "axios"; const MySample = () => { const [iris, setIris] = useState([]); useEffect(() => { axios.get("http://localhost:3000/iris.json").then((r) => { setIris(r.data); }); }, []); console.log(iris); return <div></div>; }; export default MySample;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/axiosでget

Reactプロジェクトの作成 $ npx creaste-react-app ~/Desktop/sample-app $ cd ~/Desktop/sample-app $ npm start axiosのインストール $ npm install axios jsonファイルをpublicに配置 $ cp -irp ~/Desktop/iris.json ~/Desktop/sample-app/public http://localhost:3000/iris.jsonで確認できたらOK。 axiosでget&確認 ~/Desktop/sample-app/src/components/MySample.js import React, { useEffect } from "react"; import axios from "axios"; const MySample = () => { useEffect(() => { axios.get("http://localhost:3000/iris.json").then((r) => { console.log(r.data); }); }, []); return <div></div>; }; export default MySample; ~/Desktop/sample-app/src/App.js import logo from "./logo.svg"; import "./App.css"; import MySample from "./components/MySample"; function App() { return ( <div className="App"> <MySample /> </div> ); } export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReduxでuseSelectorとuseDispatchを短いコードで覚える | React チュートリアル

ソース : ReduxでuseSelectorとuseDispatchを短いコードで覚える | React チュートリアル | off.tokyo   React Reduxは、既存のconnect()高次コンポーネントの代替としてHooksのセットを提供しています。   これらのHookは、connect()でコンポーネントをラップすることなく、Reduxストアへの接続やアクションのディスパッチを可能にします。   このガイドでは、React-Redux HooksであるuseSelectorとuseDispatchをアプリケーションに実装する方法を説明します。   カウンターとユーザーのログイン状態を表示する完成形があることを考えてみてください。   カウンターを記録するための状態と、ログインしたユーザーを記録するための状態の2つに分かれています。   それぞれの状態を処理するために、別々のファイルを用意します。   actions and reducersフォルダの作成     アクション   まずは counterActions.js でカウンターのアクションを定義してみましょう。   インクリメントとデクリメントという2つのメソッドが必要です。   この2つのメソッドは、オブジェクトとしてエクスポートします。   counterActions.js const increment = () => { return { type: "INCREMENT" } } const decrement = () => { return { type: "DECREMENT" } } export default { increment, decrement }   同様に、userActions.jsで現在のユーザーに対するアクションを定義しましょう。   ここにもsetUserとlogOutという2つのメソッドがあり、オブジェクトとしてエクスポートされます。   userActions.js const setUser = (userObj) => { return { type: "SET_USER", payload: userObj } } const logOut = () => { return { type: "LOG_OUT" } } export default { setUser, logOut }   これらの2つのファイルを1つの場所、つまりactionフォルダ内のindex.jsファイルにインポートします。   変数allActionsを作成し、インポートしたアクションを含むオブジェクトを設定します。   index.js import counterActions from './counterActions' import userActions from './userActions' const allActions = { counterActions, userActions } export default allActions   レデューサ   アクションのファイル構造と同様に、reducersフォルダを作成して、ユーザー用とカウンター用のリデューサを格納します。   まずは、カウンターのリデューサであるcounter.jsから始めましょう。 レデューサ関数は、2つの引数、stateとactionを受け取りまして、stateは、必ずしもオブジェクトに設定する必要はありません。   今回の場合、stateのデフォルト値は整数に設定されています。   先に定義したように、アクションは2つのキー、タイプとオプションでペイロードを含むことができるオブジェクトを返します。   アクションのタイプに応じて、stateの値が変更されるわけですが、アプリが壊れないように、存在しないアクションタイプが呼び出された場合には、デフォルトケースが必要であることを覚えておいてください。   counter.js const counter = (state = 1, action) => { switch(action.type){ case "INCREMENT": return state + 1 case "DECREMENT": return state - 1 default: return state } } export default counter   カレントユーザーのリデューサであるcurrentUser.jsでは、キーuserとloggedInを含む空のオブジェクトがステートに設定されます。   counterとcurrentUserでは、返される値が異なることに注目してください。   counterリデューサは、初期値が整数であるため、整数を返します、current userのリデューサの場合は、常にオブジェクトが返されます。   currentUser.js const currentUser = (state = {}, action) => { switch(action.type){ case "SET_USER": return { ...state, user: action.payload, loggedIn: true } case "LOG_OUT": return { ...state, user: {}, loggedIn: false } default: return state } } export default currentUser;   これらのレデューサーを1つにまとめる必要があります。   reducers/index.jsの下で、reducerのファイルとcombineReducersをインポートしましょう。   import {combineReducers} from 'redux'   index.js import currentUser from './currentUser' import counter from './counter' import {combineReducers} from 'redux' const rootReducer = combineReducers({ currentUser, counter }) export default rootReducer   Combine reducersは,その名のとおり,別々のリデューサファイルを1つにまとめます。   引数は1つで、レデューサーファイルを格納したオブジェクトを受け取ります。   アクションとリデューサの設定が完了したので、アプリへのReduxの実装に移りましょう。   Reduxの実装   srcフォルダの下にあるindex.jsでは、以下のようにインポートします。     import {Provider} from 'react-redux'; import {createStore} from 'redux' import rootReducer from './reducers'     ReduxのストアはcreateStoreというメソッドで作成されます。   このメソッドは2つの引数を取ります。   rootReducerはレデューサーを組み合わせたファイルで、Reduxのdevtools拡張機能です。   index.js   import {createStore} from 'redux' import rootReducer from './reducers' import {Provider} from 'react-redux' const store = createStore( rootReducer, window.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION() ) ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));   最後に、Appコンポーネントをreact-reduxのProviderコンポーネントでラップします。   Providerコンポーネントは1つのpropであるstoreを受け取り、   createStoreからのstoreに設定されます。   useSelector/useDispatchの実装   react-reduxから次のフック、useSelectorとuseDispatchをインポートします。   以前は、react-reduxからconnect()をインポートして、ステートをpropsにマッピングしたり、   ディスパッチをpropsにマッピングするために、コンポーネントをそれでラップする必要がありました。   useSelector   map state to props に相当するのが useSelector です。   useSelectorは、stateの必要な部分を返す関数の引数を受け取ります。   今回の例では、stateからcounterとcurrentUserというキーが定義されています。   これらは先ほどreducersを組み合わせる際に定義しました。   const counter = useSelector(state => state.counter) // 1 const currentUser = useSelector(state => state.currentUser) // {}   そのため、変数counterとcurrentUserには、それぞれのリデューサから定義された状態が設定されます。   useDispatch   map dispatchのpropsへの対応に相当するのが、useDispatchです。   useDispatchを起動して、変数dispatchに格納します。   dispatchはactionフォルダからインポートされたallActionsと連動します。   例えば、useEffectは次のようなアクション、allActions.userActions.setUser(user)でディスパッチを呼び出します。   ユーザーは次のように定義されます。   const user = {name: "Rei"}   allActionsは、userActionsとcounterActionsをキーとしたオブジェクトです。   userActions.jsで定義されているsetUser関数の再確認をしましょう。   const setUser = (userObj) => { return { type: "SET_USER", payload: userObj } }   setUserは、タイプとペイロードを持つオブジェクトを返します。   Dispatchはこのオブジェクトを受け取り、アクションタイプにマッチしたreducerを探します。   この例では、reducersフォルダ内のcurrentUser.jsがそれにあたります。   case "SET_USER": return { ...state, user: action.payload, loggedIn: true }   App.js import React, {useEffect} from 'react'; import {useSelector, useDispatch} from 'react-redux' import './App.css'; import allActions from './actions' const App = () => { const counter = useSelector(state => state.counter) const currentUser = useSelector(state => state.currentUser) const dispatch = useDispatch() const user = {name: "Rei"} useEffect(() => { dispatch(allActions.userActions.setUser(user)) }, []) return ( <div className="App"> { currentUser.loggedIn ? <> <h1>Hello, {currentUser.user.name}</h1> <button onClick={() => dispatch(allActions.userActions.logOut())}>Logout</button> </> : <> <h1>Login</h1> <button onClick={() => dispatch(allActions.userActions.setUser(user))}>Login as Rei</button> </> } <h1>Counter: {counter}</h1> <button onClick={() => dispatch(allActions.counterActions.increment())}>Increase Counter</button> <button onClick={() => dispatch(allActions.counterActions.decrement())}>Decrease Counter</button> </div> ); } export default App;   これで完成です。   React-ReduxのフックであるuseSelectorとuseDispatchがReact Appに実装されています。   connect()を使った場合と比べて、コードがすっきりと整理されています。   お読みいただきありがとうございました。   この記事は英語の記事を要点だけ絞って分かりやすく翻訳したものです、全文で詳しく英語で読みたい方は下記へどうぞ。  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

laravel6 x react でnpm run devできない

laravel6からreactのビルドを行うためnpm run devをしたいが以下のエラーが出て詰まった こんな感じ(実際は違う) [webpack-cli] Running multiple commands at the same time is not possible [webpack-cli] Found commands: 'bundle', 'Akinori\sample_app\node_modules\laravel-mix\setup\webpack.config.js' [webpack-cli] Run 'webpack --help' to see available commands and options npm ERR! code ELIFECYCLE npm ERR! errno 2 npm ERR! @ development: mix npm ERR! Exit status 2 npm ERR! npm ERR! Failed at the @ development script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! C:\Users*** ***\AppData\Roaming\npm-cache_logs21-01-14T17_00_01_305Z-debug.log npm ERR! code ELIFECYCLE npm ERR! errno 2 npm ERR! @ dev: npm run development npm ERR! Exit status 2 npm ERR! npm ERR! Failed at the @ dev script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! C:\Users*** ***\AppData\Roaming\npm-cache_logs21-01-14T17_00_01_347Z-debug.log 対処したこと .node.jsのアンインストールと最新版のインストール(あまり効果があったように感じない) .webpack.mix.jsを mix.js('resources/js/app.js', 'public/js') .react() .sass('resources/sass/app.scss', 'public/css', [ // ]); に書き換え dekita!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[簡単]モダン構成なSPAで作るチュートリアル④(API取得してReactで一覧表示)

SPAで作るタスク管理アプリのチュートリアル - seederで作ったDBのデータをAPI取得してReactで一覧表示 今回は第④弾でReactで一覧テーブル作成する部分をやっていくで ①環境構築(Docker/Laravel/React.js/Material-UI) ②React側でルーティング設定 ③API取得してReactで一覧テーブル作成 ④API取得してReactで一覧表示 ⑤新規登録機能 ⑥編集・削除機能 今回の章から本格的にバックエンドとフロントエンドをAPI連携してデータ取得する部分を実装していくで。 APIの連携の部分だけをやりたい人もおるやろうからこの章のスタート地点としてのブランチを用意しといたから、この章から始める人はAPIっていうブランチがあるから、それを使ってな。 ③章までやってきた人はそのままのブランチで大丈夫やで! データ表示のためのDB周りの準備(テーブルとテストデータ作成) postsテーブルを作成するためのマイグレーションファイルを作成する // migrationファイルを作成 $ docker-compose exec app php artisan make:migration create_posts_table --create=posts 上記のコマンドを実行すると_create_posts_tableと名のつくファイルが生成されるのでup()の部分を更新する _create_posts_table.php //ファイルを開くとfunction up()が存在するので書きを記載する public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('name'); $table->text('content'); $table->timestamps(); }); } migrateionを実行する $ docker-compose exec app php artisan migrate Migrating: 2021_05_03_081009_create_posts_table Migrated: 2021_05_03_081009_create_posts_table (31.42ms) migratedされたらDBを確認 DBクライアントで確認する(mysqlコマンドで確認してもOKやで) postsテーブルが生成されて id, name, content, create_at, updated_atカラムが生成されていればOKやで。 postsテーブルのテストデータを作成するためのseederファイルを作成するで $ docker-compose exec app php artisan make:seeder PostsTableSeeder Seeder created successfully. 上記実行するとdatabase/seeders/PostsTableSeeder.phpが作成されるので開くと既存コードがあるので function run()に下記を記載する(3レコード分を記載するで) PostsTableSeeder.php public function run() { //書きを記載する \DB::table('posts')->insert([ [ 'name' => 'モーリー', 'content' => '肩トレ', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ], [ 'name' => 'ドンキーコング', 'content' => 'バナナ食う', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ] [ 'name' => 'データベース太郎', 'content' => 'データ投入', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ] ]); } 記載したらdatabase/seeders/DatabaseSeeder.phpに実行するための設定をするで function run()に下記を記載する DatabaseSeeder.php public function run() { $this->call([ PostsTableSeeder::class, //追記する ]); } 追記したら、seederを実行するで //dump-autoload $ docker-compose exec app composer dump-autoload // seedを実行する $ docker-compose exec app php artisan db:seed Seeding: Database\Seeders\PostsTableSeeder Seeded: Database\Seeders\PostsTableSeeder (28.15ms) Database seeding completed successfully. データベース確認する 3レコード分を登録出来ていることを確認。 確認出来たらコミットしておこうな add . git commit -m "postsテーブル作成とテストデータ投入" Postのバックエンドを構築する mvcに沿ってバックエンドを作っていくな。 この辺が分からん人は個別にLaravelの勉強とかMVCフレームワークをなにか一個やっとくとええと思うわ。 Postモデルを作成 $ docker-compose exec app php artisan make:model Post 今回のプロダクトはメモとしてCRUDするだけやからモデル部分は実装なしやな。 Postコントローラーを作成 今回はApiでやり取りするから/Apiの下に作る $ docker-compose exec app php artisan make:controller Api/PostController ①コントローラーを作成したら作成したファイルにindex()を作成する ②(Postモデルをuseしておく) PostController.php <?php namespace App\Http\Controllers\Api;//追記 use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Post; //Postをモデルをuseする class PostController extends Controller { // postの一覧を表示する public function index() { $posts = Post::all(); return response()->json($posts, 200); } } Post::all()でpostの一覧を取得してJOSNを返す ルーティングを記載する こちらもapiなので、routes/api.php api.php Route::group(['middleware' => 'api'], function(){ Route::get('posts', 'App\Http\Controllers\Api\PostController@index'); }); //下記は使わないので削除しておく //Route::middleware('auth:api')->get('/user', function (Request //$request) { // return $request->user(); //}); ここまで出来たら ルーティング コントローラー モデル の流れが出来たからAPIのテストして見るな Postmanを使ってAPIテストしてみる APIの開発現場で、よく使うツールやから使えるようにしといたらええと思うわ。 まぁcurlとかで、確認してもええと思うけど 設定に対して投げるURIを確認する artisan route:listで現在の設定を確認できるで % docker-compose exec app php artisan route:list +--------+----------+-----------+------+-----------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-----------+------+-----------------------------------------------+------------+ | | GET|HEAD | api/posts | | App\Http\Controllers\Api\PostController@index | api | | | GET|HEAD | api/user | | Closure | api | | | | | | | auth:api | | | GET|HEAD | {any} | | Closure | web | +--------+----------+-----------+------+-----------------------------------------------+------------+ App\Http\Controllers\Api\PostController@indexを叩くには http://localhost/api/postsをgetで叩けばいいことが分かるね。 Postmanでhttp://localhost/api/posts叩く JSON形式でDBに設定した3レコードがjson形式で返ってきたな。 React側でこのデータを取得すればええことが分かるね。 次のところでやっていくからここまで出来たら コミットしておこうな $ git add . $ git commit -m "laravel側でindex()でpost一覧の取得を実装" React側でデータを取得する 固定データを表示してるHome.jsにデータをを表示する部分をやっていくで。 既存コードにデータ取得用のコードを追記する Home.js import React, { useState, useEffect } from 'react'; //1行目にuseStateを変更する import axios from 'axios'; //追記する useStyle();している下に ステートの定義(posts)とaxiosの定義をする Home.js //定義したスタイルを利用するための設定 const classes = useStyles(); //既存コードなのでこの下に書く //postsの状態を管理する const [posts, setPosts] = useState([]);   //バックエンドからpostsの一覧を取得する処理 axios .get('/api/posts') .then(response => { setPosts(response.data); //バックエンドから返ってきたデータでpostsを更新する console.log(response.data); //取得データ確認用のconsole.log() }) .catch(() => { console.log('通信に失敗しました'); }); return ( ステートとはフロント側で状態を管理するために定義するもの。 初期値としてから配列を定義しておいてバックエンドから返ってきたresponse.dataで更新する。 axiosの説明記事 https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9 ビルドする $ make npm-dev 画面リロードするとデータが取得できている事がわかる。 3つデータが取れているが繰り返しリクエストが送られてしまっているため、useEffectを利用して対処する(詳しくは記事参照) 先程のコードに対して、axiosの処理部分を関数化してuseEffect()の中で呼び出すことで、画面についた際の一回だけリクエストするようにする Home.js import React, { useState, useEffect } from 'react'; //useEffectする //画面に到着したらgetPostsDataを呼ぶ useEffect(() => { getPostsData(); },[]) //一覧情報を取得しステートpostsにセットする const getPostsData = () => { axios .get('/api/posts') .then(response => { setPosts(response.data); }) .catch(() => { console.log('通信に失敗しました'); }); } ビルドする $ make npm-dev 上記の様に、データが取れたらOK。画面についた時点では空の状態でその後リクエストが投げられてデータが取得出来ていることが分かるね 次はデータを一覧表示するためにrowsにpostsを繋ぎこむで その前にコミットしておこうな $ git add . $ git commit -m "DBのpostsデータをフロント側で取得しステート(posts)にセット" バックエンドから取得したデータを一覧表につなぎこむ Home.js(旧コード) let rows = [ { name: "モーリー", content: "肩トレ", editBtn: <Button color="secondary" variant="contained">編集</Button>, deleteBtn: <Button color="primary" variant="contained">完了</Button>, },{ name: "ドンキーコング", content: "バナナ補給", editBtn: <Button color="secondary" variant="contained">編集</Button>, deleteBtn: <Button color="primary" variant="contained">完了</Button>, }, ]; getPostDataの下にpostsを整形したデータを入れるrowsを定義する Home.js(新コード) const getPostsData = () => { axios .get('/api/posts') .then(response => { setPosts(response.data); }) .catch(() => { console.log('通信に失敗しました'); }); } //空配列として定義する let rows = []; //postsの要素ごとにrowsで使える形式に変換する posts.map((post) => rows.push({ name: post.name, content: post.content, editBtn: <Button color="secondary" variant="contained">編集</Button>, deleteBtn: <Button color="primary" variant="contained">完了</Button>, }) ); ちょっと何してるかわかりにくいかもしれんけど空配列rowsにmap中のpostのデータを整形して配列の要素として追加してるな。 バックエンド側から取得したデータ(posts)を フロントエンド側で使う形に整形する変数(rows)に加工し 表示 という流れはよく使うからマスターしとこうな ビルドする $ make npm-dev 表示確認 一番下にデータ投入という行が追加されてたら、実装成功やで 纏め フロントエンドからAPI叩いてデータ取得 データを整形して表示という部分をやっていったな。 次の⑤ではデータの新規登録をやっていくで github乗っけとくわな。 https://github.com/morry48/LaravelReactTaskApp ほな、LGTMよろしゅーやで
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails × Carrierwave で作成した画像アップロードAPIをReact側から叩くためのチュートリアル

概要 バックエンド: Rails(APIモード) フロントエンド: React(もしくはVue) 最近、こんな感じで役割分担しつつ開発する人が増えていると思います。(特に未経験からのエンジニア転職を目指している人の多くは大体そんな感じでポートフォリオを作られている印象) 自分自身も上記の構成が好きでシコシコ個人開発してたりするわけですが、画像アップロード機能を実装しようと思った際に少し手こずったのでメモ書きとして残しておきます。 ※「チュートリアル」と銘打ってはいるものの、細かいコードの説明などはあまりありません。一応、手順通りに進めれば同じものは作れるはずなのでまずは自分の手を動かした後に各コードを読み込んでみてください。 完成イメージ 画像プレビュー機能なども付けてそれっぽく仕上げてみました。 使用技術 Rails6(APIモード) React TypeScript バックエンド まず最初にバックエンド側から作っていきましょう。 ディレクトリ&各種ファイルを作成 $ mkdir mkdir rails-react-carrierwave-backend && cd rails-react-carrierwave-backend $ touch Dockerfile $ touch docker-compose.yml $ touch entrypoint.sh $ touch Gemfile $ touch Gemfile.lock ./Dockerfile FROM ruby:3.0 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH COPY Gemfile $APP_PATH/Gemfile COPY Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install COPY . $APP_PATH COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] ./docker-compose.yml version: "3" services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 3306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - "3001:3000" depends_on: - db volumes: mysql-data: ./entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" ./Gemfile # frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails", "~> 6" /Gemfile.lock # 空欄でOK rails new おなじみのコマンドでプロジェクトを作成します。 $ docker-compose run api rails new . --force --no-deps -d mysql --api Gemfileが更新されたので再ビルド。 $ docker-compose build 「.config/database.yml」を編集 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> host: <%= ENV["DATABASE_HOST"] %> データベースを作成 $ docker-compose run api rails db:create 動作確認 $ docker-compose up -d localhost:3001 にアクセスしていつもの画面が表示されればOK。 APIを作成 carrierwaveをインストール。 ./Gemfile gem 'carrierwave' Gemfileを更新したので再ビルド。 $ docker-compose build アップローダーを作成。 $ docker-compose run api rails g uploader Image すると「./app/uploaders/image_uploader.rb」が自動生成されるので次のように変更します。 .app/uploaders/image_uploader.rb class ImageUploader < CarrierWave::Uploader::Base storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # 受け付け可能なファイルの拡張子を指定 def extension_allowlist %w(jpg jpeg png) end end また、「./config/initializers/」配下にcarrierwave設定用のファイルを作成。 $ touch config/initializers/carrierwave.rb ./config/initializers/carrierwave.rb CarrierWave.configure do |config| config.asset_host = "http://localhost:3001" config.storage = :file config.cache_storage = :file end Postモデルを作成。 $ docker-compose run api rails g model Post content:text image:string $ docker-compose run api rails db:migrate 先ほど作成したアップローダーをマウントします。(ついでにバリデーションもやっておきましょう。) ./app/models/post.rb class Post < ApplicationRecord mount_uploader :image, ImageUploader validates :content, presence: true, length: { maximum: 140 } end コントローラーを作成。 $ docker-compose run api rails g controller api::v1::posts ./app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < ApplicationController before_action :set_post, only: %i[destroy] def index render json: { posts: Post.all.order("created_at DESC") } end def create post = Post.new(post_params) post.save end def destroy post = Post.find(params[:id]) post.destroy end private def set_post @post = Post.find(params[:id]) end def post_params params.permit(:content, :image) end end ルーティングを記述。 ./config/routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :posts, only: %i[index create destroy] end end end これで準備は完了です。 あとは動作確認のためルートディレクトリに適当な画像を「sample.jpg」という名前配置し、次のcurlコマンドを実行しましょう。 $ curl -F "content=test" -F "image=@sample.jpg" http://localhost:3001/api/v1/posts 特にエラーっぽいレスポンスが返ってこなければ上手くいっているはずなので、次のcurlコマンドで確認します。 $ curl -X GET http://localhost:3001/api/v1/posts { "posts": [ { "id": 1, "content": "test", "image": { "url": "http://localhost:3001/uploads/post/image/1/sample.jpg" }, "created_at": "2021-05-03T17:36:33.147Z", "updated_at": "2021-05-03T17:36:33.147Z" } ] } こんな感じで画像のパスが保存されていればOK。 CORSの設定 これでAPIは完成ですが、今の状態のままReact側から呼び出そうとするとセキュリティ的な問題でエラーが生じます。 そこで、CORSの設定を行わなければなりません。 参照: CORS とは? RailsにはCORSの設定を簡単に行えるgemが存在するのでインストールしましょう。 rb./Gemfile gem 'rack-cors' APIモードで作成している場合、すでにGemfile内に記載されているのでそちらのコメントアウトを外せばOKです。 Gemfileを更新したので再度ビルド。 $ docker-compose build あとは「./config/initializers/」配下にある設定ファイルをいじくり外部からアクセス可能なようにしておきます。 ./config/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "localhost:3000" # React側はポート番号3000で作るので「localhost:3000」を指定 resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end これで設定は完了。 フロントエンド フロントエンド側は別リポジトリで作成します。(その方がコードの見通しも良くなるので) create-react-app $ mkdir rails-react-carrierwave-frontend && cd rails-react-carrierwave-frontend $ npx create-react-app . --template typescript 不要なファイルを整理 $ rm src/App.css src/App.test.tsx src/logo.svg src/reportWebVitals.ts src/setupTests.ts 「./src/index.tsx」と「./src/App.tsx」を次のように変更します。 ./src/index.tsx import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./App" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) ./src/App.tsx import React from "react" const App: React.FC = () => { return ( <h1>Hello World!</h1> ) } export default App 一旦、動作確認してみましょう。 $ yarn start localhost:3000 にアクセスして「Hello World!」と返ってくればOK。 各種ディレクトリ・ファイルを準備 $ mkdir components $ mkdir components/post $ mkdir interfaces $ mkdir lib $ mkdir lib/api $ touch components/post/PostForm.tsx $ touch components/post/PostItem.tsx $ touch components/post/PostList.tsx $ touch interfaces/index.ts $ touch lib/api/client.ts $ touch lib/api/posts.ts $ mv components interfaces lib src 最終的に次のような構成になっていればOK。 rails-react-carrierwave-frontend ├── node_modules ├── public │   ├── favicon.ico │   ├── index.html │   ├── logo192.png │   ├── logo512.png │   ├── manifest.json │   └── robots.txt ├── src │   ├── components │   │   └── post │   │   ├── PostForm.tsx │   │   ├── PostItem.tsx │   │   └── PostList.tsx │   ├── interfaces │   │   └── index.ts │   ├── lib │   │   └── api │   │   ├── client.ts │   │   └── posts.ts │   ├── App.tsx │   ├── index.css │   ├── index.tsx │   ├── react-app-env.d.ts ├── .gitignore ├── package.json ├── README.md ├── tsconfig.json └── yarn.lock 各種ライブラリをインストール $ yarn add @material-ui/core@next @material-ui/icons@next @material-ui/lab@next @material-ui/styled-engine @emotion/react @emotion/styled axios react-router-dom @types/react-router-dom material-ui UIを整える用のライブラリ。(ついこの前最新バージョンv5が発表されたので試しにそちらを使用) emotion material-uiの最新バージョンを使う際に追加で必要なライブラリ。 axios APIリクエスト用のライブラリ。 react-router-dom ルーティング設定用のライブラリ。 型定義 ./src/interfaces/index.ts export interface Post { id: string content: string image?: { url: string } } export interface PostApiJson { posts: Post[] } APIクライアントを作成 ./src/lib/api/client.ts import axios, { AxiosInstance, AxiosResponse } from "axios" let client: AxiosInstance export default client = axios.create({ baseURL: "http://localhost:3001/api/v1", headers: { "Content-Type": "multipart/form-data" // 画像ファイルを取り扱うのでform-dataで送信 } }) client.interceptors.response.use( (response: AxiosResponse): AxiosResponse => { const data = response.data return { ...response.data, data } } ) ./src/lib/api/posts.ts import { AxiosPromise } from "axios" import client from "./client" import { PostApiJson } from "../../interfaces/index" // post取得 export const getPosts = (): AxiosPromise<PostApiJson> => { return client.get("/posts") } // post作成 export const createPost = (data: FormData): AxiosPromise => { return client.post("/posts", data) } // post削除 export const deletePost = (id: string): AxiosPromise => { return client.delete(`/posts/${id}`) } ビュー部分を作成 ./src/components/PostList.tsx import React, { useEffect, useState } from "react" import { Container, Grid } from "@material-ui/core" import { makeStyles } from "@material-ui/core/styles" import PostForm from "./PostForm" import PostItem from "./PostItem" import { getPosts } from "../../lib/api/posts" import { Post } from "../../interfaces/index" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) const PostList: React.FC = () => { const classes = useStyles() const [posts, setPosts] = useState<Post[]>([]) const handleGetPosts = async () => { const { data } = await getPosts() setPosts(data.posts) } useEffect(() => { handleGetPosts() }, []) return ( <Container maxWidth="lg" className={classes.container}> <Grid container direction="row" justifyContent="center"> <Grid item> <PostForm handleGetPosts={handleGetPosts} /> { posts?.map((post: Post) => { return ( <PostItem key={post.id} post={post} handleGetPosts={handleGetPosts} /> )} )} </Grid> </Grid> </Container> ) } export default PostList ./src/components/post/PostItem.tsx import React, { useState } from "react" import { makeStyles } from "@material-ui/core/styles" import Card from "@material-ui/core/Card" import CardHeader from "@material-ui/core/CardHeader" import CardMedia from "@material-ui/core/CardMedia" import CardContent from "@material-ui/core/CardContent" import CardActions from "@material-ui/core/CardActions" import Avatar from "@material-ui/core/Avatar" import IconButton from "@material-ui/core/IconButton" import Typography from "@material-ui/core/Typography" import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder" import FavoriteIcon from "@material-ui/icons/Favorite" import ShareIcon from "@material-ui/icons/Share" import DeleteIcon from "@material-ui/icons/Delete" import MoreVertIcon from "@material-ui/icons/MoreVert" import { Post } from "../../interfaces/index" import { deletePost } from "../../lib/api/posts" const useStyles = makeStyles(() => ({ card: { width: 320, marginTop: "2rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)" } }, delete: { marginLeft: "auto" } })) interface PostItemProps { post: Post handleGetPosts: Function } const PostItem = ({ post, handleGetPosts }: PostItemProps) => { const classes = useStyles() const [like, setLike] = useState<boolean>(false) const handleDeletePost = async (id: string) => { await deletePost(id) .then(() => { handleGetPosts() }) } return ( <> <Card className={classes.card}> <CardHeader avatar={ <Avatar> U </Avatar> } action={ <IconButton> <MoreVertIcon /> </IconButton> } title="User Name" /> { post.image?.url ? <CardMedia component="img" src={post.image.url} alt="post image" /> : null } <CardContent> <Typography variant="body2" color="textSecondary" component="span"> { post.content.split("\n").map((content: string, index: number) => { return ( <p key={index}>{content}</p> ) }) } </Typography> </CardContent> <CardActions disableSpacing> <IconButton onClick={() => like ? setLike(false) : setLike(true)}> { like ? <FavoriteIcon /> : <FavoriteBorderIcon /> } </IconButton> <IconButton> <ShareIcon /> </IconButton> <div className={classes.delete}> <IconButton onClick={() => handleDeletePost(post.id)} > <DeleteIcon /> </IconButton> </div> </CardActions> </Card> </> ) } export default PostItem ./src/components/post/PostForm.tsx import React, { useCallback, useState } from "react" import { experimentalStyled as styled } from '@material-ui/core/styles'; import { makeStyles, Theme } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import Button from "@material-ui/core/Button" import Box from "@material-ui/core/Box" import IconButton from "@material-ui/core/IconButton" import PhotoCameraIcon from "@material-ui/icons/PhotoCamera" import CancelIcon from "@material-ui/icons/Cancel" import { createPost } from "../../lib/api/posts" const useStyles = makeStyles((theme: Theme) => ({ form: { display: "flex", flexWrap: "wrap", width: 320 }, inputFileBtn: { marginTop: "10px" }, submitBtn: { marginTop: "10px", marginLeft: "auto" }, box: { margin: "2rem 0 4rem", width: 320 }, preview: { width: "100%" } })) const Input = styled("input")({ display: "none" }) const borderStyles = { bgcolor: "background.paper", border: 1, } interface PostFormProps { handleGetPosts: Function } const PostForm = ({ handleGetPosts }: PostFormProps) => { const classes = useStyles() const [content, setContent] = useState<string>("") const [image, setImage] = useState<File>() const [preview, setPreview] = useState<string>("") const uploadImage = useCallback((e) => { const file = e.target.files[0] setImage(file) }, []) // プレビュー機能 const previewImage = useCallback((e) => { const file = e.target.files[0] setPreview(window.URL.createObjectURL(file)) }, []) // FormData形式でデータを作成 const createFormData = (): FormData => { const formData = new FormData() formData.append("content", content) if (image) formData.append("image", image) return formData } const handleCreatePost = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const data = createFormData() await createPost(data) .then(() => { setContent("") setPreview("") setImage(undefined) handleGetPosts() }) } return ( <> <form className={classes.form} noValidate onSubmit={handleCreatePost}> <TextField placeholder="Hello World" variant="outlined" multiline fullWidth rows="4" value={content} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setContent(e.target.value) }} /> <div className={classes.inputFileBtn}> <label htmlFor="icon-button-file"> <Input accept="image/*" id="icon-button-file" type="file" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { uploadImage(e) previewImage(e) }} /> <IconButton color="inherit" component="span"> <PhotoCameraIcon /> </IconButton> </label> </div> <div className={classes.submitBtn}> <Button type="submit" variant="contained" size="large" color="inherit" disabled={!content || content.length > 140} className={classes.submitBtn} > Post </Button> </div> </form> { preview ? <Box sx={{ ...borderStyles, borderRadius: 1, borderColor: "grey.400" }} className={classes.box} > <IconButton color="inherit" onClick={() => setPreview("")} > <CancelIcon /> </IconButton> <img src={preview} alt="preview img" className={classes.preview} /> </Box> : null } </> ) } export default PostForm ./src/App.tsx import React from "react" import { BrowserRouter as Router, Switch, Route } from "react-router-dom" import PostList from "./components/post/PostList" const App: React.FC = () => { return ( <Router> <Switch> <Route exact path="/" component={PostList} /> </Switch> </Router> ) } export default App http://localhost:3000 にアクセスしてこんな感じになっていればOKです。 投稿を作成できるか、画像プレビュー機能が動いているか、投稿を削除できるかなど一通り確認してください。 あとがき お疲れ様でした。もし手順通りに進めて不具合などありましたらコメント欄にて指摘していただけると幸いです。 今回作成したコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScriptの開発環境をVSCodeで整える(ESLint, Prettier, Husky, lint-staged, commitlint, EditorConfig)

ESLintとかPrettierとか設定が難しい 1年ぶりにフロントエンドに戻って来ると色々と変わっていました。具体的には... eslint-config-prettierが非推奨になっていた huskyがv6になって動かなくなっていた このあたり本当によく変わりますよね。今回は以下のツールを設定して一流のReact + TypeScript開発環境の構築方法をまとめます。ESLintのところ以外はReactでなくてもTypeScriptでなくても使えるはずです。 Prettier ESLint Husky + lint-staged commitlint EditorConfig 今後できれば更新していきたいです(最終更新日 2021/05/04)。 Prettier Prettierとは PrettierはFormatter(コードを自動整形してくれるツール)です。 ""か'' ()をつけるか ,をつけるか ...etc といったスタイルを統一してくれます。 Prettierのインストールと設定 yarn add -D prettier デフォルトの設定を上書きしたいところだけを設定ファイルに記載します。 .prettierrc { "jsxSingleQuote": true, "singleQuote": true, "trailingComma": "all" } CIなどで使えるようにpackage.jsonにscirptを用意しておきます。 .package.json "scripts": { "lint": "prettier --check .", "format": "prettier --write .", ... }, VSCodeでファイル保存時にPrettierで自動整形する さらにファイルを保存したタイミングで自動で整形するようにするために、Prettierのプラグインをインストールします。他の開発者のために推奨プラグインに記載しておきます。 .vscode/extensions.json { "recommendations": [ "esbenp.prettier-vscode", ] } VSCodeの設定ファイルを作成して、ファイルを保存したタイミングでPrettierで整形するように設定します。 .vscode/settings.json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", } ここでyarn formatを実行して何が整形されるか見ておくとESLintとの違いがわかりやすいかもしれません。 ESLint ESLintとは ESLintはLinter(コードの品質チェックをするツール)です。 使われていない変数は無いか 意図せずにglobal変数になっている変数は無いか ===の代わりに==が使われていないか ...etc といった品質チェックをしてくれます。チェックした後に自動修正もできるのでPrettierとの役割分担が曖昧になりがちです。 これまではeslint-config-prettierを使って、ESLintからPrettierを実行する手法が一般的でしたが、この方法は非推奨になりました。 これからはPrettierでFormatをとESLintで品質チェックをするように役割を分担して、競合する部分はPrettierにやらせるようにするようです。 ESLintのインストールと設定 yarn add -D eslint ESLintの初期設定はCLIを使うと簡単です。 yarn eslint --init ✔ How would you like to use ESLint? · style ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · react ✔ Does your project use TypeScript? · No / Yes ✔ Where does your code run? · browser, node ✔ How would you like to define a style for your project? · guide ✔ Which style guide do you want to follow? · airbnb ✔ What format do you want your config file to be in? · JavaScript ここからカスタマイズしていきます。extendsに推奨設定を必要なものを入れていきます。 plugin:eslint/recommended, plugin:react/recommended, plugin:react-hooks/recommendedは入れておいても良いですが、私はAirbnbを信頼しているので入れていません。 plugin:@typescript-eslint/eslint-recommendedとplugin:@typescript-eslint/recommendedの両方を設定しているのも見かけますが、後者は前者をextendsしているので後者だけでよいでしょう(src)。 plugin:@typescript-eslint/recommended-requiring-type-checkingを入れれば型のチェックまでやってanyなどに対してwarningすることができますが、tscを実行しているようなものなのでチェックが遅くなります。あとanyを撲滅するのがかなり面倒なので今回は見送っています。 eslint-config-airbnb-typescriptを採用するのもありだと思いますが、今回は見送っています。 .eslintrc.js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ - 'plugin:react/recommended', // reactの設定はairbnbに任せます + 'plugin:@typescript-eslint/recommended', // typescriptの推奨設定を入れます 'airbnb', + 'airbnb/hooks', // reactのhooksの設定は別れているので別途入れます + 'prettier', // prettierと競合するESLintのルールを無効化します ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: {}, }; さらにTypeScriptのcompilerOptions.pathsのAliasをサポートするために一手間加えます。 yarn add -D eslint-import-resolver-typescript .eslintrc.js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ 'plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb/hooks', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: {}, + settings: { // typescriptのaliasがエラーにならないように + 'import/resolver': { + typescript: { + project: './tsconfig.json', + }, + }, + }, }; 後はエラーを消すために独断と偏見でrulesを設定してください(これが少し面倒)。 余計なものも入ってますが、参考までにrules設定済みのものを置いておきます。 .eslintrc.js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ 'plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb/hooks', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: { // hush no-use-before-define error in "import React from 'react';" // see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': ['error'], // omit .ts .tsx in import statement // see https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], // allow named exports without default export 'import/prefer-default-export': 'off', // allow importing storybooks from devDependencies // see https://github.com/storybookjs/storybook/issues/1992#issuecomment-335001056 'import/no-extraneous-dependencies': [ 'error', { devDependencies: ['.storybook/**', '**/*.stories.tsx'], }, ], // allow jsx in .tsx 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], // nextjs does not need React in every file 'react/react-in-jsx-scope': 'off', // use this pattern for default props // // interface Props { // prop1?: boolean; // } // const Component1: FC<Props> = ({ prop1 = false }: Props) => ( // <Component1 prop1={prop1} /> // ); 'react/require-default-props': 'off', // allow <App {...props} /> 'react/jsx-props-no-spreading': 'off', }, settings: { 'import/resolver': { typescript: { project: './tsconfig.json', }, }, }, }; CIなどで使えるようにpackage.jsonにscirptを用意しておきます。ESLintで;が追加された後にPrettierのprintWidthを超えた場合などに備えてPrettierを後にしています。 .package.json "scripts": { - "lint": "prettier --check .", - "format": "prettier --write .", + "lint": "eslint . && prettier --check .", + "format": "eslint --fix . && prettier --write .", ... }, VSCodeでファイル保存時にESLintで自動整形する さらにファイルを保存したタイミングで自動で整形するようにするために、ESLintのプラグインをインストールします。他の開発者のために推奨プラグインに記載しておきます。 .vscode/extensions.json { "recommendations": [ + "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", ] } VSCodeの設定ファイルにファイルを保存したタイミングでESLintで整形するように設定します。これでファイルを保存するのタイミングでPrettierとESLintの両方で整形されるようになります。 .vscode/settings.json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, } これでPrettierとESLintの設定が終わったので、yarn formatを実行しておくと良いです。 Husky + lint-staged Huskyとは HuskyはGit Hooksを設定するツールです。 commitの前に~する pushの前に~する commit messageに対して~する ...etc ということが設定できます。 lint-stagedとは lint-stagedはstageされたファイルだけを操作するためのツールです。Huskyと併用することで、commitされるファイルに対してだけPrettierやESLintのチェックをするということができるようになります。 lint-stagedのインストールと設定 yarn add -D lint-staged 先にlint-stagedの設定をします。stageされているファイルに対してESLintでチェックした後にPrettierでチェックするようにします。 .lintstagedrc { "**/*.{js,jsx,ts,tsx}": [ "yarn eslint", "yarn prettier --check" ] } 正しく設定されているか確認するにはjs, jsx, ts, tsxのファイルをstageして以下のコマンドを実行します。 yarn lint-staged Huskyのインストールと設定 次にHuskyの設定をします。 yarn add -D husky hasky@4までは.huskyrcを作成すれば良かったのですが、hasky@6ではかなり変わっているので注意が必要です。 hasky@6ではyarnとyarn v2ではインストール方法が違いますが、v2の方法でやってもv1のままで動くので、今後の事を考えてv2の手動の方法でやっておけばよいでしょう。 yarn husky install package.json { + "private": true, + "scripts": { + "postinstall": "husky install" + }, ... } pre-commit hookでyarn lint-stagedを実行するようにします。 yarn husky add .husky/pre-commit "yarn lint-staged" commitlint commitlintとは Gitのcommit messageにもルールを設定できるのが、commitlintです。ライブラリーのコcommit messageによくあるこういうやつです。 chore: upgrade xxxx docs: update README.md どういうルールにするかは自分で設定できますが、よく使うルールは用意されています。 @commitlint/config-angular @commitlint/config-conventional @commitlint/config-lerna-scopes ...etc @commitlint/config-conventionalでだいたいの場合は事足りるので今回はこれで行きます。 commitlintのインストールと設定 yarn add -D @commitlint/config-conventional @commitlint/cli commitlintの設定ファイルを作成します。 commitlintrc.js module.exports = { extends: ['@commitlint/config-conventional'], }; Huskyでcommit-msg hookでcommitlintを実行するように設定します。 yarn husky add .husky/commit-msg 'yarn commitlint --edit "$1"' これで変なcommit messageでcommitしようとすると弾かれることが確認できます。 EditorConfig EditorConfigとは 世の中にはJetBrainsの熱烈なファンがいたり、Vimマスターがいたりと、すべての開発者がVSCodeを使っているわけではありません。環境が違えば色々と違いがあり... ファイルの最終行に空行を入れるか インデントはタブなのかスペース2個なのか 改行コードはLFなのかCRLFなのか などの個人のIDEの設定によってバラバラだったりします。そんなときに登場するのがEditorConfigです。 EditorConfigのインストールと設定 導入は簡単で、ますIDEでEditorConfigのプラグインをインストールします。 EditorConfig for VS Code EditorConfig for JetBrains EditorConfig for Vim ...etc 他の開発者のために推奨プラグインに記載しておきます。 .vscode/extensions.json { "recommendations": [ "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", "esbenp.prettier-vscode" ] } .editorconfigに設定を書きます。参考までにAngularの.editorconfigを置いておきます。大体はこれで十分です。 .editorconfig # https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false まとめ かなりガチガチなReact + TypeScript開発環境ができました。これで余計なところに気を取られずに開発やレビューに集中できますね。 開発環境ツールに関しては他の言語に比べてJavaScriptが突出していますね。このくらい凄い開発環境を実現するツールがが他の言語でもあるといいなと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScriptの開発環境をVSCodeで整える(ESLint + Prettier + Husky + lint-staged + commitlint + EditorConfig)

ESLintとかPrettierとか設定が難しい 1年ぶりにフロントエンドに戻って来ると色々と変わっていました。具体的には... eslint-config-prettierが非推奨になっていた huskyがv6になって動かなくなっていた このあたり本当によく変わりますよね。今回は以下のツールを設定して一流のReact + TypeScript開発環境の構築方法をまとめます。ESLintのところ以外はReactでなくてもTypeScriptでなくても使えるはずです。 Prettier ESLint Husky + lint-staged commitlint EditorConfig 今後できれば更新していきたいです(最終更新日 2021/05/04)。 Prettier Prettierとは PrettierはFormatter(コードを自動整形してくれるツール)です。 ""か'' ()をつけるか ,をつけるか ...etc といったスタイルを統一してくれます。 Prettierのインストールと設定 yarn add -D prettier デフォルトの設定を上書きしたいところだけを設定ファイルに記載します。 .prettierrc { "jsxSingleQuote": true, "singleQuote": true, "trailingComma": "all" } CIなどで使えるようにpackage.jsonにscirptを用意しておきます。 .package.json "scripts": { "lint": "prettier --check .", "format": "prettier --write .", ... }, VSCodeでファイル保存時にPrettierで自動整形する さらにファイルを保存したタイミングで自動で整形するようにするために、Prettierのプラグインをインストールします。他の開発者のために推奨プラグインに記載しておきます。 .vscode/extensions.json { "recommendations": [ "esbenp.prettier-vscode", ] } VSCodeの設定ファイルを作成して、ファイルを保存したタイミングでPrettierで整形するように設定します。 .vscode/settings.json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", } ここでyarn formatを実行して何が整形されるか見ておくとESLintとの違いがわかりやすいかもしれません。 ESLint ESLintとは ESLintはLinter(コードの品質チェックをするツール)です。 使われていない変数は無いか 意図せずにglobal変数になっている変数は無いか ===の代わりに==が使われていないか ...etc といった品質チェックをしてくれます。チェックした後に自動修正もできるのでPrettierとの役割分担が曖昧になりがちです。 これまではeslint-config-prettierを使って、ESLintからPrettierを実行する手法が一般的でしたが、この方法は非推奨になりました。 これからはPrettierでFormatをとESLintで品質チェックをするように役割を分担して、競合する部分はPrettierにやらせるようにするようです。 ESLintのインストールと設定 yarn add -D eslint ESLintの初期設定はCLIを使うと簡単です。 yarn eslint --init ✔ How would you like to use ESLint? · style ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · react ✔ Does your project use TypeScript? · No / Yes ✔ Where does your code run? · browser, node ✔ How would you like to define a style for your project? · guide ✔ Which style guide do you want to follow? · airbnb ✔ What format do you want your config file to be in? · JavaScript ここからカスタマイズしていきます。extendsに推奨設定を必要なものを入れていきます。 plugin:eslint/recommended, plugin:react/recommended, plugin:react-hooks/recommendedは入れておいても良いですが、私はAirbnbを信頼しているので入れていません。 plugin:@typescript-eslint/eslint-recommendedとplugin:@typescript-eslint/recommendedの両方を設定しているのも見かけますが、後者は前者をextendsしているので後者だけでよいでしょう(src)。 plugin:@typescript-eslint/recommended-requiring-type-checkingを入れれば型のチェックまでやってanyなどに対してwarningすることができますが、tscを実行しているようなものなのでチェックが遅くなります。あとanyを撲滅するのがかなり面倒なので今回は見送っています。 eslint-config-airbnb-typescriptを採用するのもありだと思いますが、今回は見送っています。 .eslintrc.js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ - 'plugin:react/recommended', // reactの設定はairbnbに任せます + 'plugin:@typescript-eslint/recommended', // typescriptの推奨設定を入れます 'airbnb', + 'airbnb/hooks', // reactのhooksの設定は別れているので別途入れます + 'prettier', // prettierと競合するESLintのルールを無効化します ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: {}, }; さらにTypeScriptのcompilerOptions.pathsのAliasをサポートするために一手間加えます。 yarn add -D eslint-import-resolver-typescript .eslintrc.js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ 'plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb/hooks', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: {}, + settings: { // typescriptのaliasがエラーにならないように + 'import/resolver': { + typescript: { + project: './tsconfig.json', + }, + }, + }, }; 後はエラーを消すために独断と偏見でrulesを設定してください(これが少し面倒)。 余計なものも入ってますが、参考までにrules設定済みのものを置いておきます。 .eslintrc.js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ 'plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb/hooks', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: { // hush no-use-before-define error in "import React from 'react';" // see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': ['error'], // omit .ts .tsx in import statement // see https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], // allow named exports without default export 'import/prefer-default-export': 'off', // allow importing storybooks from devDependencies // see https://github.com/storybookjs/storybook/issues/1992#issuecomment-335001056 'import/no-extraneous-dependencies': [ 'error', { devDependencies: ['.storybook/**', '**/*.stories.tsx'], }, ], // allow jsx in .tsx 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], // nextjs does not need React in every file 'react/react-in-jsx-scope': 'off', // use this pattern for default props // // interface Props { // prop1?: boolean; // } // const Component1: FC<Props> = ({ prop1 = false }: Props) => ( // <Component1 prop1={prop1} /> // ); 'react/require-default-props': 'off', // allow <App {...props} /> 'react/jsx-props-no-spreading': 'off', }, settings: { 'import/resolver': { typescript: { project: './tsconfig.json', }, }, }, }; CIなどで使えるようにpackage.jsonにscirptを用意しておきます。ESLintで;が追加された後にPrettierのprintWidthを超えた場合などに備えてPrettierを後にしています。 .package.json "scripts": { - "lint": "prettier --check .", - "format": "prettier --write .", + "lint": "eslint . && prettier --check .", + "format": "eslint --fix . && prettier --write .", ... }, VSCodeでファイル保存時にESLintで自動整形する さらにファイルを保存したタイミングで自動で整形するようにするために、ESLintのプラグインをインストールします。他の開発者のために推奨プラグインに記載しておきます。 .vscode/extensions.json { "recommendations": [ + "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", ] } VSCodeの設定ファイルにファイルを保存したタイミングでESLintで整形するように設定します。これでファイルを保存するのタイミングでPrettierとESLintの両方で整形されるようになります。 .vscode/settings.json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, } これでPrettierとESLintの設定が終わったので、yarn formatを実行しておくと良いです。 Husky + lint-staged Huskyとは HuskyはGit Hooksを設定するツールです。 commitの前に~する pushの前に~する commit messageに対して~する ...etc ということが設定できます。 lint-stagedとは lint-stagedはstageされたファイルだけを操作するためのツールです。Huskyと併用することで、commitされるファイルに対してだけPrettierやESLintのチェックをするということができるようになります。 lint-stagedのインストールと設定 yarn add -D lint-staged 先にlint-stagedの設定をします。stageされているファイルに対してESLintでチェックした後にPrettierでチェックするようにします。 .lintstagedrc { "**/*.{js,jsx,ts,tsx}": [ "yarn eslint", "yarn prettier --check" ] } 正しく設定されているか確認するにはjs, jsx, ts, tsxのファイルをstageして以下のコマンドを実行します。 yarn lint-staged Huskyのインストールと設定 次にHuskyの設定をします。 yarn add -D husky hasky@4までは.huskyrcを作成すれば良かったのですが、hasky@6ではかなり変わっているので注意が必要です。 hasky@6ではyarnとyarn v2ではインストール方法が違いますが、v2の方法でやってもv1のままで動くので、今後の事を考えてv2の手動の方法でやっておけばよいでしょう。 yarn husky install package.json { + "private": true, + "scripts": { + "postinstall": "husky install" + }, ... } pre-commit hookでyarn lint-stagedを実行するようにします。 yarn husky add .husky/pre-commit "yarn lint-staged" commitlint commitlintとは Gitのcommit messageにもルールを設定できるのが、commitlintです。ライブラリーのコcommit messageによくあるこういうやつです。 chore: upgrade xxxx docs: update README.md どういうルールにするかは自分で設定できますが、よく使うルールは用意されています。 @commitlint/config-angular @commitlint/config-conventional @commitlint/config-lerna-scopes ...etc @commitlint/config-conventionalでだいたいの場合は事足りるので今回はこれで行きます。 commitlintのインストールと設定 yarn add -D @commitlint/config-conventional @commitlint/cli commitlintの設定ファイルを作成します。 commitlintrc.js module.exports = { extends: ['@commitlint/config-conventional'], }; Huskyでcommit-msg hookでcommitlintを実行するように設定します。 yarn husky add .husky/commit-msg 'yarn commitlint --edit "$1"' これで変なcommit messageでcommitしようとすると弾かれることが確認できます。 EditorConfig EditorConfigとは 世の中にはJetBrainsの熱烈なファンがいたり、Vimマスターがいたりと、すべての開発者がVSCodeを使っているわけではありません。環境が違えば色々と違いがあり... ファイルの最終行に空行を入れるか インデントはタブなのかスペース2個なのか 改行コードはLFなのかCRLFなのか などの個人のIDEの設定によってバラバラだったりします。そんなときに登場するのがEditorConfigです。 EditorConfigのインストールと設定 導入は簡単で、ますIDEでEditorConfigのプラグインをインストールします。 EditorConfig for VS Code EditorConfig for JetBrains EditorConfig for Vim ...etc 他の開発者のために推奨プラグインに記載しておきます。 .vscode/extensions.json { "recommendations": [ "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", "esbenp.prettier-vscode" ] } .editorconfigに設定を書きます。参考までにAngularの.editorconfigを置いておきます。大体はこれで十分です。 .editorconfig # https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false まとめ かなりガチガチなReact + TypeScript開発環境ができました。これで余計なところに気を取られずに開発やレビューに集中できますね。 開発環境ツールに関しては他の言語に比べてJavaScriptが突出していますね。このくらい凄い開発環境を実現するツールがが他の言語でもあるといいなと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む