- 投稿日:2021-05-16T20:17:00+09:00
React/leafletで画像を重ねる
/sample-app/src/components/Map.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; import img from "./virus_corona.png"; L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); const imageUrl = img; const imageBounds = [ [-90, -180], // lowerLeft[lat, lng] [90, 180], // upperRight[lat, lng] ]; const Map = () => { useEffect(() => { // https://leafletjs.com/index.html var map = L.map("map").setView([0, 0], 2); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); // https://leafletjs.com/reference-1.7.1.html#imageoverlay L.imageOverlay(imageUrl, imageBounds).addTo(map); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default Map;
- 投稿日:2021-05-16T20:17:00+09:00
React/leafletでGMTで作成した地図画像を貼り付け
Reactプロジェクトの作成 $ npx create-react-app sample-app $ cd sample-app $ npm install leaflet $ npm start 地図画像の作成 leafletのデフォルトのprojectionはEPSG:3857。 $ gmt pscoast -JEPSG:3857 -R-180/180/-80/80 -G0/255/0 -S0/0/255 > tmp.eps $ gmt psconvert tmp.eps -TG -A -P -D/home/yono2844/Workspace/sample-app/src/components/ -Fm ap.png 画像の貼り付け /sample-app/src/components/Map.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; import img from "./map.png"; L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); const imageUrl = img; const imageBounds = [ [-80, -180], // lowerLeft[lat, lng] [80, 180], // upperRight[lat, lng] ]; const Map = () => { useEffect(() => { // https://leafletjs.com/index.html var map = L.map("map").setView([0, 0], 2); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); // https://leafletjs.com/reference0.7.1.html#imageoverlay L.imageOverlay(imageUrl, imageBounds, { opacity: 0.4 }).addTo(map); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default Map; js/sample-app/src/components/App.js import React from "react"; import Map from "./components/Map"; const App = () => { return ( <div> <Map /> </div> ); }; export default App;
- 投稿日:2021-05-16T19:50:16+09:00
Reactでleaflet③
/sample-app/src/components/Map0.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); const Map0 = () => { useEffect(() => { // https://leafletjs.com/index.html var map = L.map("map").setView([51.505, -0.09], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); L.marker([51.5, -0.09]) .addTo(map) .bindPopup("A pretty CSS3 popup.<br> Easily customizable.") .openPopup(); // https://leafletjs.com/examples/quick-start/ L.circle([51.508, -0.11], { color: "red", fillColor: "#f03", fillOpacity: 0.5, radius: 500, }) .bindPopup("I am a circle.") .addTo(map); L.polygon([ [51.509, -0.08], [51.503, -0.06], [51.51, -0.047], ]) .bindPopup("I am a polygon.") .addTo(map); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default Map0;
- 投稿日:2021-05-16T19:46:56+09:00
Reactでleaflet②
/sample-app/src/components/Map0.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); const Map0 = () => { useEffect(() => { // https://leafletjs.com/index.html var map = L.map("map").setView([51.505, -0.09], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); L.marker([51.5, -0.09]) .addTo(map) .bindPopup("A pretty CSS3 popup.<br> Easily customizable.") .openPopup(); // https://leafletjs.com/examples/quick-start/ L.circle([51.508, -0.11], { color: "red", fillColor: "#f03", fillOpacity: 0.5, radius: 500, }).addTo(map); L.polygon([ [51.509, -0.08], [51.503, -0.06], [51.51, -0.047], ]).addTo(map); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default Map0;
- 投稿日:2021-05-16T19:38:41+09:00
Reactでleaflet①
/sample-app/src/components/Map0.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); const Map0 = () => { useEffect(() => { // https://leafletjs.com/ var map = L.map("map").setView([51.505, -0.09], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); L.marker([51.5, -0.09]) .addTo(map) .bindPopup("A pretty CSS3 popup.<br> Easily customizable.") .openPopup(); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default Map0;
- 投稿日:2021-05-16T19:38:41+09:00
Reactでleaflet
/sample-app/src/components/Map0.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); const Map0 = () => { useEffect(() => { var map = L.map("map").setView([51.505, -0.09], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); L.marker([51.5, -0.09]) .addTo(map) .bindPopup("A pretty CSS3 popup.<br> Easily customizable.") .openPopup(); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default Map0;
- 投稿日:2021-05-16T17:01:32+09:00
Reactでleaflet.timedimension(雑記)
$ npx create-react-app sample-app $ cd sample-app $ npm install leaflet $ npm install leaflet.timedimension $ npm start /sample-app/src/MyMap2.js import React, { useEffect } from "react"; import "leaflet/dist/leaflet.css"; import "leaflet-timedimension/dist/leaflet.timedimension.control.min.css"; import L from "leaflet"; import "leaflet-timedimension"; import data from "./data2.json"; const MyMap2 = () => { console.log(data); useEffect(() => { var map = L.map("map", { zoom: 3, center: [38.705, 1.15], timeDimension: true, timeDimensionControl: true, timeDimensionOptions: { times: [1496314227000, 1504263027000], currentTime: 1496314227000, // period: "P1M", }, timeDimensionControlOptions: { playerOptions: { transitionTime: 1000, startOver: true }, }, }); const dataLayer = L.geoJson(data); L.timeDimension.layer.geoJson(dataLayer).addTo(map); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default MyMap2;
- 投稿日:2021-05-16T16:15:38+09:00
Reactでchart.js
/sample-app/src/MyChart.js import React, { useRef, useEffect } from "react"; import Chart from "chart.js/auto"; const labels = ["January", "February", "March", "April", "May", "June"]; const data = { labels: labels, datasets: [ { label: "My First dataset", backgroundColor: "rgb(255, 99, 132)", borderColor: "rgb(255, 99, 132)", data: [0, 10, 5, 2, 20, 30, 45], }, ], }; const config = { type: "line", data, options: {}, }; const MyChart = () => { const ref = useRef(null); useEffect(() => { const myChart = new Chart(ref.current, config); }, []); return ( <div> <canvas id="myChart" ref={ref}></canvas> </div> ); }; export default MyChart;
- 投稿日:2021-05-16T16:00:24+09:00
Reactでleaflet
$ npx create-react-app sample-app $ cd sample-app $ npm install leaflet $ npm start /sample-app/src/MyMap.js import React, { useEffect } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import icon from "leaflet/dist/images/marker-icon.png"; import shadow from "leaflet/dist/images/marker-shadow.png"; const MyMap = () => { useEffect(() => { L.Marker.prototype.options.icon = L.icon({ iconUrl: icon, shadowUrl: shadow, }); var map = L.map("map").setView([51.505, -0.09], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); L.marker([51.5, -0.09]) .addTo(map) .bindPopup("A pretty CSS3 popup.<br> Easily customizable.") .openPopup(); }, []); return <div id="map" style={{ height: "100vh" }}></div>; }; export default MyMap;
- 投稿日:2021-05-16T15:25:14+09:00
Laravelで作成したAPIをフロントエンドで受け取り画面へ描画する
前提 今回はaxiosを利用して下記URLで作成した一覧取得のapiを受け取って表示するまでを行います。 LaravelでAPIを作成 axiosとは... バックエンドとのデータのやり取りを簡単にできるライブラリです。 下記コマンドで取得するAPIのパスを確認する php artisan route:list axiosを利用してみる tsxファイルに下記を記述する。 import React, { useEffect, useState } from "react" import axios from "axios" # 型を定義する type Hoge = { id: number hoge: string fuga: boolean created_at: Date updated_at: Date } const HogePage: React.VFC = () => { const [hoges, setHoges] = useState<Hoge[]>([]) # apiを取得する const getHoges = async () => { const { data } = await axios.get<Hoge[]>('api/hoges') console.log(data) setHoges(data) } # ページにアクセスしたら実行する useEffect(() => { getHoges }) } return ( <> <div clasName="inner"> <ul className="hoge-list"> {hoges.map(hoge => ( <li key={hoge.id}> <label className="checkbox-label"> <input type="checkbox" className="checkbox-input" /> </label> <div><span>{task.title}</span</div> </li> )) } </ul> </div> <> 下記の記事のようにreactRouterのルーティングになっている前提でlocalhost:8000//hogesへアクセスすると... Laravelで作成した投稿一覧APIを取得して画面に描画することができました! Laravel環境でreactRouterのルーティングを使用できるようにする
- 投稿日:2021-05-16T15:18:58+09:00
Laravel環境でreactRouterのルーティングを使用できるようにする
前提 reactRouterとTypeScriptで使用するための定義ファイルのライブラリをインストールする。 $ npm i -D react-router-dom @types/react-router-dom reactRouterの定義ファイルを作成する touch resources/ts/router.tsx import React from 'react' import { BrowserRouter, Switch, Route, Link } from "react-router-dom" import TaskPage from './pages/hoges' const Router = () => { return ( <BrowserRouter> <Switch> <Route path="/"> <HogePage /> </Route> </Switch> </BrowserRouter> ); } export default Router resources/ts/index.tsxへ下記を追記する import Router from "./router" ReactDom.render( <Router /> document.getElementById('app') ) 現在LaravelのRouterと競合してhomeディレクトリへ遷移してもreactRouterのRouteが反映されないのでweb.phpを下記のように編集する Route::get('{all}', function () { return view('index'); })->where(['all' => '.*']); reactRouterで遷移できるようになりました!
- 投稿日:2021-05-16T15:10:07+09:00
react-chartjs-2を使わないでchart.js
useRef()を使うと良い。 $ npx create-react-app sample-app $ cd sample-app $ npm install chart.js@2.9.4 $ npm start /sample-app/MyChart.js import React, { useRef, useState, useEffect } from "react"; import Chart from "chart.js"; const MyChart = ({ config }) => { const chartRef = useRef(null); const [chart, setChart] = useState(null); useEffect(() => { const newChart = new Chart(chartRef.current, config); setChart(newChart); }, [chartRef]); return ( <div> <canvas ref={chartRef}></canvas> </div> ); }; export default MyChart; /sample-app/App.js import logo from "./logo.svg"; import "./App.css"; import MyChart from "./MyChart"; const labels = ["January", "February", "March", "April", "May", "June"]; const data = { labels: labels, datasets: [ { label: "A", data: [0, 10, 5, 2, 20, 30, 45], backgroundColor: "rgba(255, 99, 132, 0.4)", borderColor: "rgb(255, 99, 132)", }, ], }; const config = { type: "line", data, options: {}, }; function App() { return ( <div className="App"> <MyChart config={config} /> </div> ); } export default App;
- 投稿日:2021-05-16T15:10:07+09:00
Reactでreact-chartjs-2を使わないでchart.js
useRef()を使って実装。 $ npx create-react-app sample-app $ cd sample-app $ npm install chart.js $ npm start /sample-app/MyChart.js import React, { useRef, useState, useEffect } from "react"; import Chart from "chart.js/auto"; const labels = ["January", "February", "March", "April", "May", "June"]; const data = { labels: labels, datasets: [ { label: "My First dataset", backgroundColor: "rgb(255, 99, 132)", borderColor: "rgb(255, 99, 132)", data: [0, 10, 5, 2, 20, 30, 45], }, ], }; const config = { type: "line", data, options: {}, }; const MyChart = () => { const ref = useRef(null); const [chart, setChart] = useState(null); useEffect(() => { const newChart = new Chart(ref.current, config); setChart(newChart); }, []); return ( <div> <canvas ref={ref}></canvas> </div> ); }; export default MyChart; /sample-app/App.js import logo from "./logo.svg"; import "./App.css"; import MyChart from "./MyChart"; function App() { return ( <div className="App"> <MyChart /> </div> ); } export default App;
- 投稿日:2021-05-16T14:47:39+09:00
Reactを使ったテックブログを作成してみた
はじめに Reactを学習していくにあたり、REST APIを用いたCRUD操作の勉強のため技術ブログを作成してみました。 今回は簡易的に利用できるJSON Serverを使ってデータのやり取りを行っています。アプリケーション内の状態管理にはRedux Toolkit(createAsyncThunk、createEntityAdapter)を利用しました。今後は外部サイトに公開できるように外部のデータベースと連携していきたいところです。 記事一覧ページと記事詳細ページ 管理画面と記事確認画面 新規・編集画面 仕様について 今回は下記のような仕様を想定して制作を行いました。 閲覧ユーザー向けの機能 記事一覧を表示する(公開設定している記事のみ) 記事詳細を表示する いいねボタンを押すといいねが1増える(何度も押せる) 管理ユーザー向けの機能 登録記事一覧を表示する 記事詳細を表示する 記事の編集ができる 記事の公開・非公開の切り替えができる 記事の削除ができる 新規記事の作成ができる 記事はマークダウン記法で登録できる ログイン機能はなし 画像の投稿機能はなし 主な利用サービス・利用技術 React Redux Toolkit JSON Server … JSONファイルをデータベースとして利用できる簡易APIサーバー react-markdown … Reactで使えるマークダウン変換ライブラリ styled-components TypeScript JSON Server 今回外部APIとしてJSON Serverを利用しました。 データベースとしてJSONファイルをそのまま利用することができるので、余計な手間をかけずにすぐに開発に取り掛かることができます。JSON形式のデータを返すAPIであれば、JSON Serverで作成後にURLだけ変えれば外部との連携に簡単に切り替えることもできます。 セットアップ $ yarn add json-server データベースとして利用するJSONファイルを用意します。 フォーマットとしてはリソース名を1階層目に必ず用意しなくてはいけません。 今回はブログサイトを想定していますので下記のようなダミーデータを用意しました。 src/data/db.json { "posts": [ { "id": "1", "title": "create-react-app直後にやる環境構築の備忘録", "createdAt": 1620804168398, "updatedAt": 1621064460898, "body": "本文テキスト1", "image": "/assets/images/dummy01.jpeg", "like": 100, "publish": false }, { "id": "2", "title": "Reactを使ったお天気アプリを作成してみた", "createdAt": 1620804168398, "updatedAt": 1621070951011, "body": "本文テキスト2", "image": "/assets/images/dummy01.jpeg", "like": 112, "publish": true } ] } React内で扱う型としては下記を想定しています。 投稿「Post」とその一覧「Post[]」という形です。 src/types/index.ts export type Post = { id: string; createdAt: number; updatedAt?: number; title: string; body: string; image: string; like: number; publish: boolean; }; export type Posts = Post[]; package.jsonにJSON Server起動用のスクリプトを追記します。 package.json "scripts": { "json-server": "json-server --watch src/data/db.json -p 3001 -d 2000" }, --watchをつけることで常時監視モードで起動することができます。 Reactがポート3000を利用するため、-p 3001として3001番ポートで起動するように変更。 また、-d 2000として2秒の待ち時間を入れるようにしています。待ち時間を入れることで非同期のテストも体感的にわかりやすくなのでとても便利です。 $ yarn json-server ブラウザでhttp://localhost:3001/postsを開くと登録した記事一覧を表示することができます。 JSON ServerへのCRUD操作 今回は記事の登録、編集、削除に合わせてRedux ToolkitのcreateAsyncThunk、axiosを利用してJSON Serverへアクセスしました。 const post:Post = 記事データ // 登録 POST const response = await axios .post<Post>(`${URL}/posts`, post) .catch((err) => { // エラー処理 }); return response.data; // 編集 PUT const response = await axios .put<Post>(`${URL}/posts/${post.id}`, post) .catch((err) => { // エラー処理 }); return response.data; // 削除 DELETE const response = await axios .delete<Post>(`${URL}/posts/${post.id}`, { data: { id: post.id }, }) .catch((err) => { // エラー処理 }); return response.data; 上記は省略していますが前後の処理については下記ソースを参照いただければと思います。 Redux Toolkit JSON Serverからデータの取得にはcreateAsyncThunkを経由して、createEntityAdapterを使い処理を行っています。 詳細は下記にも記事を投稿させていただきました。 苦労した点 React Routerで複数のページを扱いながら、その中でできる限りムダな通信や処理は行わないように考えることに時間をかなり要してしまいました。レイアウト用のコンポーネントからAPIへ通信を行うようにし、記事の情報は最初に一度だけ取りに行くように、また詳細ページに直接アクセスした際や遷移先でリロードを行っても記事が表示されるように工夫を行いました。ただ今回は制限などがある外部のAPIではなくローカル環境で構築ができたので制限にびくびくする必要なく精神衛生上よかったように思います。 また非同期の処理については相変わらず怪しい知識のままなのですが、今回はRedux ToolkitのcreateAsyncThunkを利用することで比較的簡単に処理を分岐できたのが大きな発見でした。 今後の拡張 今後の拡張としては今の所下記を想定したいところです。。。 管理画面の認証機能 画像の投稿機能 Express、mongoDBを用いたバックエンド構築 さいごに チュートリアル的なTODOアプリからちょっと進んだ?、APIを用いたCRUDなアプリケーションを作成することでより知識を深めることができました。JSON Serverで気軽にバックエンドを構築できてたのもあり、制作期間も1週間程度とかなり短期間ですすめることができたのも大きな一歩です。 テックブログもすこしずつ拡張しながらさらに知識を深めていこうと思います。 関連記事 参考サイト React JSON Server Redux
- 投稿日:2021-05-16T14:45:43+09:00
ReduxとRedux Toolkitについて[createAsyncThunkとcreateEntityAdapter推し]
はじめに React初学者がテックブログを作成しながらReduxとRedux Toolkitについて調べてみた備忘録です。 学び始めたときにはすでにToolkitが1.4まで進んでいたので最初から利用させていただいてますが、個人的にはcreateAsyncThunkとcreateEntityAdapter推しです? 誤り等ありましたらやさしくご指摘だけると幸いです??♂️ Reduxとは Reduxは、「Action」と呼ばれるイベントを用いて、アプリケーションの状態(state)を管理・更新するためのライブラリです。React以外にも利用されていますがReactとともに用いられることが多いようです。 Fluxというデータフローの概念を利用して設計されており、主にAction、Store、Reducerの要素で構成されています。 Reactでは各コンポーネントがpropとstateを持っていますが、アプリケーションの規模が大きくなりにつれ、コンポーネント間のやり取りが大変になってきます。俗にprop drilling problemとも呼ばれています。 Reduxを用いるとStoreという唯一の箱のようなものでstateを管理するため、どこからでも更新、参照できるという大きなメリットを持っています。 Reduxの大まかな流れとしては、 ActionはActionCreatorで作られます。 Storeではstateを管理しています。 ReducerはActionとstateから新しいstateを作成します。 まず、ユーザーは行いたいActionを、Storeに対してdispatchというメソッドを用いて送信します。 次にReducerが、Storeが受け取ったActionにしたがってstateから新しいstateを作成して返します。 必ず新しいstateのみを返すことで、Reduxではアプリケーションの中で状態がいつ、どのように更新されたかを容易に把握することができるようになっています。 タイムトラベルデバッギングとも言われているようで、デベロッパーツールを用いると時間軸に沿った動きをデバッグすることができます。 ツールとしてはとても有益な反面、ルールが厳格で手続き上記載する内容も冗長になることで学習コスト、運用コストもかかるため最近では脱Redux論もさかんに議論されているようです。 Redux Toolkit そんな中公式のRedux Toolkitが2019/10にリリースされました。 Redux ToolkitはReduxをより簡潔に記述するためのツールです。公式ページに掲載されているRedux Style GuideでもToolkitの利用を推奨しています。 Redux Toolkitを使うメリット 初期設定などがほとんど不要になる Redux DevTools Extension(デバッギングツール)の設定や非同期通信を扱うReduxThunkなどのミドルウェアが同梱されているため、設定不要で利用することができます。 コード量(ファイル数?)が少なくなる Action、ActionCreator、ReducerがcreateSliceというひとつの記述でまとめて記載することができます。 一つのファイルのコード行数は増えるかも。。 stateのイミュータブルを意識なくてよい Reducerの中で新しいstateを返す際には必ずイミュータブルな値となるようにしなければなりませんでしたが、Toolkitでは内部で自動的にイミュータブルな値として処理を行ってくれます。 TypeScriptの型が効く Toolkitでは多くの型が設定されいるので型による補完と制御の恩恵を受けることができます。 create-react-appのReduxテンプレートを使うとそのまま利用できる部分も多くとても便利です。 createAsyncThunkとcreateEntityAdapterが便利 Toolkitの1.3から追加されたcreateAsyncThunkとcreateEntityAdapterがとても便利です。 createAsyncThunk 非同期処理に対応したActionCreatorを生成する関数で、下記3種の状態を簡単に管理することができます。 pending: 非同期処理中 fulfilled: 非同期処理の成功時 rejected: 非同期処理の失敗時 createEntityAdapter 型情報をもとにエンティティ操作用のAdapterを生成し、CRUD(create, read, update, delete)操作の機能を提供してくれる関数です。 データベースを扱うような形で処理を行うことができるようになります。 { id: 1, title: 'create-react-app直後にやる環境構築の備忘録', createdAt: 1620804168398, updatedAt: 1621064460898, body: '本文テキスト', image: '/assets/images/dummy01.jpeg', like: 167, publish: false } 例えば上記のような形式を下記のような形に自動的に変換して管理してくれます。 { postsEntity: { ids: [ 1, 2 ], entities: { '1': { id: 1, title: 'create-react-app直後にやる環境構築の備忘録', createdAt: 1620804168398, updatedAt: 1621064460898, body: '本文テキスト', image: '/assets/images/dummy01.jpeg', like: 167, publish: false }, '2': { id: 2, title: 'Reactを使ったお天気アプリを作成してみた', createdAt: 1620804168398, updatedAt: 1621070951011, body: '本文テキスト2', image: '/assets/images/dummy01.jpeg', like: 112, publish: true }, }, } } データのエンティティ化(ノーマライズ)についてのは下記でサンプルをベースに解説されています。 実装例 少々長いのですが実装を行ったサンプルになります。 まずはベースとなる型情報。ブログの投稿をイメージしています。 src/types/index.ts export type Post = { id: string; createdAt: number; updatedAt?: number; title: string; body: string; image: string; like: number; publish: boolean; }; export type Posts = Post[]; 以下投稿一覧の取得や更新、削除を行うSliceのサンプルです。 qiitaのリンク こちらで作成したテックブログのソースコードになっています。 createEntityAdapterで作成したAdapterに対しては下記のメソッドが利用できます。 addOne 1件追加 addMany 複数追加 setAll 全件追加上書き removeOne 1件削除 removeMany 複数削除 removeAll 全件削除 updateOne 1件を更新 updateMany 複数更新 upsertOne 1件を更新、存在しない場合は追加 upsertMany 複数を更新、存在しない場合は追加 が利用できます。 src/features/posts/postsEntitySlice.ts import { createAsyncThunk, createEntityAdapter, createSlice, PayloadAction, } from '@reduxjs/toolkit'; import { RootState } from 'src/app/store'; import axios from 'axios'; import { Post, Posts } from 'src/types'; export type PostsState = { posts: Posts; status: 'idle' | 'loading' | 'failed'; message: string; }; /** * entity用のアダプターを生成 */ const postsAdapter = createEntityAdapter<Post>({ // 記事のデフォルトの並び順を降順に変更 selectId: (post) => post.id, sortComparer: (a, b) => { if (a.id < b.id) { return 1; } else { return -1; } }, }); const postInitialEntityState = postsAdapter.getInitialState({ // 型以外に設定したいものはここで用意 status: 'idle', message: '', }); // APIのエンドポイント const URL = process.env.REACT_APP_JSON_SERVER_URL || 'http://localhost:3000'; /** * 投稿一覧を取得する */ export const fetchEntityPosts = createAsyncThunk('posts/fetchEntityPosts', async (_, thunkApi) => { const response = await axios.get<Posts>(`${URL}/posts`).catch((err) => { thunkApi.rejectWithValue(err); // thunkApiを利用してエラーメッセージなどをreducerにわたすことができる throw err; }); return response.data; }); /** * 投稿を追加する */ export const addEntityPost = createAsyncThunk( 'posts/addEntityPost', async (post: Omit<Post, 'id'>, thunkApi) => { const response = await axios.post<Post>(`${URL}/posts`, post).catch((err) => { thunkApi.rejectWithValue(err); throw err; }); return response.data; }, ); /** * 投稿を編集する */ export const updateEntityPost = createAsyncThunk( 'posts/updateEntityPost', async (post: Post, thunkApi) => { const response = await axios.put<Post>(`${URL}/posts/${post.id}`, post).catch((err) => { thunkApi.rejectWithValue(err); throw err; }); return response.data; }, ); /** * 投稿を削除する */ export const deleteEntityPost = createAsyncThunk( 'posts/deleteEntityPost', async (postId: string, thunkApi) => { const response = await axios .delete<Post>(`${URL}/posts/${postId}`, { data: { id: postId }, }) .catch((err) => { thunkApi.rejectWithValue(err); throw err; }); return { data: response.data, postId }; }, ); /** * 該当する投稿IDにいいねを1プラスする */ export const putLikes = createAsyncThunk('posts/putLikes', async (postData: Post, thunkApi) => { const response = await axios .put<Post>(`${URL}/posts/${postData.id}`, { ...postData, like: postData.like + 1, }) .catch((err) => { thunkApi.rejectWithValue(err); throw err; }); return { id: response.data.id, like: response.data.like }; }); /** * 公開・非公開を切り替える */ export const togglePublish = createAsyncThunk( 'posts/togglePublish', async (postData: Post, thunkApi) => { const response = await axios .put<Post>(`${URL}/posts/${postData.id}`, { ...postData, publish: !postData.publish, }) .catch((err) => { thunkApi.rejectWithValue(err); throw err; }); return { id: response.data.id, publish: response.data.publish }; }, ); export const postsEntitySlice = createSlice({ name: 'postsEntity', initialState: postInitialEntityState, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchEntityPosts.pending, (state) => { // ローディング中 state.status = 'loading'; state.message = ''; }) .addCase(fetchEntityPosts.rejected, (state, action) => { // 失敗 state.status = 'failed'; if (action.error.message) { state.message = action.error.message; } }) .addCase(fetchEntityPosts.fulfilled, (state, action) => { // 成功 state.status = 'idle'; // 取得した投稿全件をstoreに登録 postsAdapter.setAll(state, action.payload); }) .addCase(addEntityPost.pending, (state) => { state.status = 'loading'; state.message = ''; }) .addCase(addEntityPost.rejected, (state, action) => { state.status = 'failed'; if (action.error.message) { state.message = action.error.message; } }) .addCase(addEntityPost.fulfilled, (state, action) => { state.status = 'idle'; // 1件をstoreに新規登録 postsAdapter.addOne(state, action.payload); }) .addCase(updateEntityPost.pending, (state) => { state.message = ''; }) .addCase(updateEntityPost.rejected, (state, action) => { if (action.error.message) { state.message = action.error.message; } }) .addCase(updateEntityPost.fulfilled, (state, action: PayloadAction<Post>) => { state.status = 'idle'; const { id, ...updateData } = action.payload; // 1件をidで指定して更新 postsAdapter.updateOne(state, { id: id, changes: { ...updateData }, }); }) .addCase(deleteEntityPost.pending, (state) => { state.status = 'loading'; state.message = ''; }) .addCase(deleteEntityPost.rejected, (state, action) => { state.status = 'failed'; if (action.error.message) { state.message = action.error.message; } }) .addCase(deleteEntityPost.fulfilled, (state, action) => { state.status = 'idle'; postsAdapter.removeOne(state, action.payload.postId); }) .addCase(putLikes.pending, (state) => { state.message = ''; }) .addCase(putLikes.rejected, (state, action) => { if (action.error.message) { state.message = action.error.message; } }) .addCase(putLikes.fulfilled, (state, action: PayloadAction<{ id: string; like: number }>) => { state.status = 'idle'; // 1件のlikeという項目のみ更新 postsAdapter.updateOne(state, { id: action.payload.id, changes: { like: action.payload.like }, }); }) .addCase(togglePublish.pending, (state) => { state.message = ''; }) .addCase(togglePublish.rejected, (state, action) => { if (action.error.message) { state.message = action.error.message; } }) .addCase( togglePublish.fulfilled, (state, action: PayloadAction<{ id: string; publish: boolean }>) => { // 1件のpublishという項目のみ更新 state.status = 'idle'; postsAdapter.updateOne(state, { id: action.payload.id, changes: { publish: action.payload.publish }, }); }, ); }, }); export default postsEntitySlice.reducer; // 全件取得用のselector export const selectPosts = postsAdapter.getSelectors<RootState>((state) => state.postsEntity); // 単一項目取得用のselector export const selectStatus = (state: RootState) => state.postsEntity.status; export const selectMessage = (state: RootState) => state.postsEntity.message; コンポーネント側では下記のように利用できます。 またuseSelectorやuseDispatchについても下記のように型を扱える形で利用が推奨されています。 src/app/hooks.ts(reduxテンプレート) import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; コンポーネント側ではusuSelectorからAdapterに対して下記メソッドでデータを取得することができます。 selectIds idの一覧を配列で取得 selectEntities entitiesオブジェクトを取得 selectAll 全件の配列を取得 selectTotal 件数を取得 selectById 該当するidに紐づくデータを取得 selectAllとselectByIdが一番利用することになるかと思います。 コンポーネント側 // 全件を取得 const posts = useAppSelector(selectPosts.selectAll); // idを指定して取得 const post = useAppSelector((state) => selectPosts.selectById(state, postId)); このような形でエンティティ化されたものから、意識せずに通常の配列の形に変換されて変数へ格納することが可能です。 またcreateAsyncThunkでの実行結果は下記のようにコンポーネント上でも参照することができます。 コンポーネント側 const resultAction = await dispatch(fetchEntityPosts()); // 失敗時であればrejected、 // 成功時、ローディング時はそれぞれfullfilled、pendingとmatchさせることで判別が可能 if (fetchEntityPosts.rejected.match(resultAction)) { alert('データの取得に失敗しました。'); } さいごに Reactを勉強し始めた時点ですでにToolkitも1.4になっていたので最初からToolkitを利用していましたが、Reduxについては公式サイトのドキュメントを読み直すたびに新しい気付きがあります。とりあえず、しばらくはcreateAsyncThunkとcreateEntityAdapter推しでいこうと思います。 関連サイト
- 投稿日:2021-05-16T12:50:40+09:00
React Routerでルーティング(React + TypeScript環境)
この記事では、React Routerというライブラリを用いてルーティングを行う方法について説明しています。 1. React + TypeScript プロジェクトの作成 こちらの記事で詳しく紹介していますが、以下のコマンドでプロジェクトを作成します。 npx create-react-app react-sample --template typescript 2.react-router-dom をインストール TypeScript版のreact-router-domをインストールします。 npm install react-router-dom @types/react-router-dom 3.遷移したいページのコンポーネントを作成 src/以下に遷移したいページのコンポーネントを作成します。 今回はFormというページを作成したいので、以下のようなファイルを作成します。 src/Form.tsx import React from "react"; import "./Form.css"; const Form = () => { return <div className="form"> Form Component</div>; }; export default Form; src/以下にCSSも作成しておきます。 src/Form.css .form { text-align: center; margin-top: 40px; font-size: 28px; } 4.index.tsxを書き換え 先ほど作成したFormコンポーネントと、react-router-domから必要なモジュールを読み込んで利用します。 src/index.tsx import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import Form from "./Form"; import reportWebVitals from "./reportWebVitals"; import { Route, BrowserRouter } from "react-router-dom"; ReactDOM.render( <React.StrictMode> <BrowserRouter> <div> <Route exact path="/" component={App} /> <Route exact path="/form" component={Form} /> </div> </BrowserRouter> </React.StrictMode>, document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); <Route exact path="/" component={App} /> <Route exact path="/form" component={Form} /> 上記ののコード部分でルーティングの設定を行っていて、/に接続するとトップページ、/formに接続すると3.で作成したページが表示されます。 http://localhost:3000/に接続 http://localhost:3000/formに接続 リンクを追加 作成したページへのリンクを作成します。 src/App.tsxにトップページのコードが記載されているので、こちらにリンクを追加します。 import { Link } from "react-router-dom"; <Link className="App-link" to="/Form"> Form </Link> 上記のコードをsrc/App.tsxに追加 src/App.tsx import React from "react"; import logo from "./logo.svg"; import "./App.css"; import { Link } from "react-router-dom"; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.tsx</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <Link className="App-link" to="/Form"> Form </Link> </header> </div> ); } export default App; ここまで完了して、http://localhost:3000/formに接続にするとFormへのリンクが表示され、クリックするとルーティングが行われます。
- 投稿日:2021-05-16T11:46:01+09:00
【React】知っておくと便利なカスタムフック作成のコツ
Reactでプロジェクトをつくる際、コンポーネント間で共通部分を一つにまとめることを意識しています。 表示部分に関してはできている(つもり)のですが、処理部分についてはレビューでも何回か指摘されるなど、あまりできていませんでした。 カスタムフックを利用すればいいことはわかっていたのですが、そもそもの作り方や使い方を理解できていなかったことが原因です。 ただ、カスタムフックをいくつかつくってみると作成にはコツがあることがわかりましたので、記事にまとめることにしました。 処理の共通化について悩んでいる方の参考になればと思います。 カスタムフックとは カスタムフック(Custom Hooks)は、コンポーネント間で共通な処理(ロジック)を1つにまとめた再利用可能な関数です。 例えば以下のように、AppとAnalyticsという2つコンポーネントで"動画の取得"や"動画の選択"といった処理が共通である場合、各処理のカスタムフックを作成することでどちらのコンポーネントでも同じ処理を使えるようになります。 カスタムフック作成のコツ 作成のコツとして”作成時に意識すること”と”作成のプロセス”についてまとめました。 作成時に意識すること カスタムフックのコードはフックを利用するコンポーネントとは分離する カスタムフックの中ではReact Hook(use~)を少なくとも1つ以上使う 1つのカスタムフックは1つの目的だけをもつ 特に最後の"1つのカスタムフックは1つの目的だけをもつ"については、SOLIDの単一責任の原則に近い考え方になります。 一人のアクターが使うようにカスタムフックが設計されていないと、気づかないうちに別のアクターによって中身が修正されてしまったり、同じフックを2人以上のアクターが修正してしまうことでコンフリクトが起こってしまう可能性がでてきてしまいます。 作成のプロセス 処理部分を一行ずつ確認し目的ごとに分別する Inputを抽出する Outputを抽出する Inputを引数、Outputを戻り値としてコードをカスタムフックに抽出する カスタムフックの作成例 YouTubeのAPIから動画を5件取得して、選択した動画をメインに表示するような画面を考えます。 ちなみにデフォルト表示は"YAMADA KATSUMI"の検索結果です。 動画の取得処理に関するカスタムフックuseVideosを作成していきます。 もともとのコードは以下のようになっています。 App.js const App = () => { const [videos, setVideos] = useState([]); const [selectedVideo, setSelectedVideo] = useState(null); useEffect(() => { onTermSubmit('YAMADA KATSUMI'); }, []); const onTermSubmit = async (term) => { const response = await youtube.get('/search', { params: { q: term, }, }); setVideos(response.data.items); setSelectedVideo(response.data.items[0]); }; return ( <div className="ui container"> <SearchBar onFormSubmit={onTermSubmit} /> <div className="ui grid"> <div className="ui row"> <div className="eleven wide column"> <VideoDetail video={selectedVideo} /> </div> <div className="five wide column"> <VideoList onVideoSelect={setSelectedVideo} videos={videos} /> </div> </div> </div> </div> ); }; 処理部分を一行ずつ確認し目的ごとに分別する 動画の取得処理を"Video"、動画の選択処理を"Selection"として該当コードを分別します。 Inputを抽出する カスタムフックを複数のコンポーネントで使えるように、"Video"の処理でInputになる箇所を抽出します。 今回のケースだと"YAMADA KATSUMI"の部分がInputにあたります。 これでカスタムフックを使用するコンポーネントごとにデフォルトの検索条件を変えることができるようになります。 Outputを抽出する コンポーネントで使用する箇所をOutputとして抽出します。 今回のケースでは、videosとonTermSubmitが該当します。 Inputを引数、Outputを戻り値としてコードをカスタムフックに抽出する 抽出したInputとOutputをもとに、カスタムフックuseVideosを作成します。 Inputにあたる部分をdefaultSearchTermとし、Outputにあたる部分をreturn [videos, search]としています。 useVideos.js import { useState, useEffect } from 'react'; import youtube from '../apis/youtube'; const useVideos = (defaultSearchTerm) => { const [videos, setVideos] = useState([]); useEffect(() => { search(defaultSearchTerm); }, [defaultSearchTerm]); const search = async (term) => { const response = await youtube.get('/search', { params: { q: term, }, }); setVideos(response.data.items); }; return [videos, search]; }; export default useVideos; 作成したカスタムフックuseVideosをApp.jsでconst [videos, search] = useVideos('YAMADA KATSUMI')のように読み込みます。 処理が一つにまとまったことでコードも読みやすくなりました。 App.js const App = () => { const [selectedVideo, setSelectedVideo] = useState(null); const [videos, search] = useVideos('YAMADA KATSUMI'); useEffect(() => { setSelectedVideo(videos[0]); }, [videos]); return ( <div className="ui container"> <SearchBar onFormSubmit={search} /> <div className="ui grid"> <div className="ui row"> <div className="eleven wide column"> <VideoDetail video={selectedVideo} /> </div> <div className="five wide column"> <VideoList onVideoSelect={setSelectedVideo} videos={videos} /> </div> </div> </div> </div> ); }; 参考資料
- 投稿日:2021-05-16T02:32:13+09:00
Vue.jsとReact.jsのプロジェクト作成から公開準備(※コーディングは含みません。)
Vue.jsとReact.jsのプロジェクト作成から公開準備 Javascriptで使用できるフロントエンド側のフレームワーク、VueとReactのプロジェクト作成コマンドを忘れないためと、ビルドしてからの流れについての説明が見当たらず、せっかく個人開発でやってみても誰にもリリースすることがないように記す。 なお、今回はそれぞれポートフォリオをフロントエンド側でのみ、作成したという程で作成しました。 動作環境 MacBook Air (Retina, 13-inch, 2018) BicSur 11.3.1 1.6 GHz Dual-Core Intel Core i5 6 GB 2133 MHz LPDDR3 Node.js v13.8.0 @vue/cli 4.5.13 各プロジェクトの作成 Vue.js vue create vue-portfolio React.js npx create-react-app react-portfolio インストール確認 Vue.js ..省略.. ? Successfully created project vue-portfolio. ? Get started with the following commands: $ cd vue-portfolio $ npm run serve React.js ..省略.. Success! Created react-portfolio at XXXXXXX/react-portfolio Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd react-portfolio npm start Happy hacking! インストール後のnpmサーバーを起動して画面の確認 Vue.js ■実行 cd vue-portfolio npm run serve ■結果 DONE Compiled successfully in 5762ms App running at: - Local: http://localhost:8080/ - Network: http://192.168.10.9:8080/ Note that the development build is not optimized. To create a production build, run npm run build. No issues found. ■画像 React.js ■実行 cd react-portfolio npm start ■結果 Compiled successfully! You can now view react-portfolio in the browser. Local: http://localhost:3000 On Your Network: http://192.168.10.9:3000 Note that the development build is not optimized. To create a production build, use npm run build. ■画像 いじる ※自分だけのポートフォリオを作成してください。 今回はわかりやすくトップページのみ日本語に変更をしていきます。 Vue.js .src/views/Home.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png"> <!-- <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/> --> <HelloWorld msg="ようこそ。あなたのVue.jsとTypeScript App"/> //上からの変更点 </div> </template> <script lang="ts"> import { defineComponent } from 'vue' import HelloWorld from '@/components/HelloWorld.vue' // @ is an alias to /src export default defineComponent({ name: 'Home', components: { HelloWorld } }) </script> React.js .src/App.js import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> {/* Edit <code>src/App.js</code> and save to reload. */} <code>src/App.js</code>を編集と保存して再読み込み。{/* 上の行を変更 */} </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App; ポートフォリオ公開準備 自分だけのポートフォリオを作成したら公開するために、ビルドを行なっていきます。 Vue.js STEP1:作成したコードをビルド 以下のコマンドを実行する npm run build STEP2:ビルド結果の確認 ビルド結果 「.dist」配下にビルドしたファイルが作成 DONE Compiled successfully in 11164ms File Size Gzipped dist/js/chunk-vendors.506821c6.js 118.83 KiB 42.80 KiB dist/js/app.67acf451.js 6.65 KiB 2.39 KiB dist/js/about.2b8983e6.js 0.34 KiB 0.26 KiB dist/css/app.aaf04fac.css 0.42 KiB 0.26 KiB Images and other types of assets omitted. DONE Build complete. The dist directory is ready to be deployed. INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html React.js STEP1:作成したコードをビルド 以下のコマンドを実行する npm run build STEP2:ビルド結果の確認 ビルド結果 「.build」配下にビルドしたファイルが作成 Compiled successfully. File sizes after gzip: 41.34 KB build/static/js/2.a9157932.chunk.js 1.63 KB build/static/js/3.4db56475.chunk.js 1.17 KB build/static/js/runtime-main.e0b02365.js 631 B build/static/js/main.5d1e222e.chunk.js 574 B build/static/css/main.9d5b29c0.chunk.css The project was built assuming it is hosted at /. You can control this with the homepage field in your package.json. The build folder is ready to be deployed. You may serve it with a static server: npm install -g serve serve -s build Find out more about deployment here: https://cra.link/deployment 作成したものをAWS S3へアップロードしてリリース ※AWSアカウントを持っている前提で記載しております。 共通 公式HPに公開方法が記載されております。 そのため掻い摘んで説明 STEP1:S3バケットの作成 サービス検索ボックスより「S3」と検索 右側オレンジボタンの「バケットを作成」をクリック STEP2:一般的な設定 バケット名は小文字英数字と半角記号のみで設定(※一意になるように命名) AWSリージョンについては今回はデフォルトのまま(アジアパシフィック(東京)ap-northeast-1) STEP3:このバケットのブロックパブリックアクセス設定 「パブリックアクセスをすべてブロック」のチェックを外す 「パブリックアクセスのブロックをすべてオフにすると、このバケットとバケット内のオブジェクトが公開される可能性があります。」の確認にチェックを入れる STEP4:バケットの作成 指定項目以外についてはデフォルトのまま 右下の「バケットを作成」をクリック STEP5:静的ウェブサイトホスティング設定 作成したバケットをクリック プロパティタブを選択 「静的ウェブサイトホスティング」の「編集」をクリック 「有効にする」にチェックを入れ「ホスティングタイプ」に「静的ウェブサイトをホストする」がチェック入っていること 「インデックスドキュメント」に「index.html」を入力 右下の「変更の保存」をクリック STEP6:バケットポリシー アクセス許可タブをクリック バケットポリシー欄の編集をクリック 以下の内容を入力し、["arn:aws:s3:::Bucket-Name/*"]部分の[Bucket-Name]をバケット名へ変更 右下の「変更の保存」をクリック { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::XXXXXXXXX/*" ] } ] } STEP7:ビルドしたファイルのアップロード オブジェクトタブから「アップロード」をクリック 以下のファイルに対して、アップロードを行う Vueの場合は「.dist」配下のファイルすべて Reactの場合は「.build」配下のファイルすべて STEP8:外部ネットワークで確認 プロパティタブの「静的ウェブサイトホスティング」にある「バケットウェブサイトエンドポイント」をクリック アップロードした内容が閲覧できること 以上。 Appendix ?ビルドしたプログラムをすぐ見れない? VueもReactも共通してだが、ビルドされた「index.html」についてはサブディレクトリのままでは閲覧することができない。理由はビルド時にファイル内に記載される読み込みファイルが絶対パスのドメイン直下(ルートフォルダ)で記載がされているため、初心者はAWS S3などにあげて確認するほうがよい。
- 投稿日:2021-05-16T00:52:16+09:00
LaravelにReactとTypeScriptを導入する
前提 Laravelでプロジェクトを作成していて、nodeはインストール済みの想定で進めていきます。 TypeScriptをインストールする webpack.mix.js(webpackのラッパーライブラリ)を下記のように編集する mix.ts('resources/ts/index.tsx', 'public/js') .sass('resources/sass/app.scss', 'public/css'); 先ほどの編集内容を反映させる npm install プロジェクト内にnode_modulesディレクトリが作成されていればOK。 一旦下記コマンドでビルドする。 npm run prod package.jsonにwebpack.mix.jsで編集した内容の不足分が記載される(typescriptとsass) Reactをインストールする npm i -D react react-dom @types/react @types/react-dom npm install package.jsonに追記されているか確認する。 React用のTypeScriptの設定ファイルを作成する tsc --init --jsx react すると... tsconfig.jsonが作成される。 動作確認 ①webpack.mix.jsに記載した下記を作成する。 ■resources/ts/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/sass/app.scss →resources/css/app.cssをリネームする ②下記コマンドでビルドする npm run prod ③resources/views/welcome.blade.phpをresources/views/index.blade.phpへリネームする 下記のように書き直す <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> <link rel="stylesheet" href="{{ mix('/css/app.css') }}"> </head> <body> <div id="app"></div> </body> <script src="{{ mix('/js/index.js') }}"></script> </html> ④web.phpでrouteをwelcomeからindexへ変更する ⑤キャッシュ対策でファイルパスにパラメーターを付与する webpack.mix.jsに下記を追記する if (mix.inProduction()) { mix.version() } ⑥ビルトインサーバーを起動してlocalhost:8000へアクセスする php artisan serve 編集した内容が表示されていることを確認できました!