よく使うコマンド | ビルド/コンパイル | Dockerの起動など

Sail版

Dockerサービス起動とDockerコンテナ起動
cd /mnt/c/Users/user/git/react_demo2/dev
sudo service docker start
./vendor/bin/sail up -d
	
ビルド/コンパイル
./vendor/bin/sail npm run build
	
Dockerコンテナ停止とDockerサービス終了
./vendor/bin/sail stop
sudo service docker stop
	

ルーティング: 画面毎にtsxファイルを割り当てる

1つのtsxファイルに全画面の関数コンポーネントを書くわけにもいかないため、 画面ごとに割り当てられたtsxファイルをインポートして実行したいものです。例えば「http://localhost/demo」にアクセスしたとき、demo.tsxをインポートして実行します。

このような動的なルーティングを実現するための一般的な方法は、React Routerを使用することです。React Routerは、指定されたURLに基づいて異なるReactコンポーネントを表示するための強力なルーティングライブラリです。

以下は、React Routerを使用してdemo.tsxをhttp://localhost/demoでロードするための手順です:

必要なパッケージをインストールします。
npm install react-router-dom @types/react-router-dom

resources/ts/index.tsx


import React from 'react';
import ReactDOM from 'react-dom';
import {
  BrowserRouter as Router,
  Route,
  Routes
} from 'react-router-dom';
import Demo from './demo';

const App: React.FC = () => {
  return (
    <Router>
      <Routes>
        <Route path="/demo" element={<Demo />} />
        {/* 他のルートもこちらに追加可能 */}
      </Routes>
    </Router>
  );
};

export default App;

ReactDOM.render(<App />, document.getElementById('react_app'));
	

resources/ts/demo.tsx


import React from 'react';

const Demo: React.FC = () => {
  return <div>Demo Page!</div>;
};

export default Demo;
	

resources/views/demo/index.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>
        @viteReactRefresh
         @vite(['resources/sass/app.scss', 'resources/ts/index.tsx'])


    </head>
    <body>
    	<div id="react_app"></div><!-- Reactの埋め込み部分 -->
  
    </body>
</html>
	

表示切替ボタン | 閉じるボタン

id属性で要素を指定して、表示/非表示を切り替える汎用的な関数コンポーネントの見本です。

resources/ts/cmn/ToggleButton.tsx


// ToggleButton.tsx
import React, { useState } from 'react';

type ToggleButtonProps = {
  targetId: string;
  label: string;
};

const ToggleButton: React.FC<ToggleButtonProps> = ({ targetId, label }) => {
  const [isVisible, setIsVisible] = useState<boolean>(false);

  const toggleVisibility = () => {
    const element = document.getElementById(targetId);
    if (element) {
      if (isVisible) {
        element.style.display = 'none';
      } else {
        element.style.display = 'block';
      }
      setIsVisible(!isVisible);
    }
  };

  return <button onClick={toggleVisibility} class="btn btn-info">{isVisible ? `${label}(閉じる)` : `${label}`}</button>;
};

export default ToggleButton;
	

resources/ts/demo.tsx(クライアント側へToggleButton関数コンポーネントを実装)


import React from 'react';
import ReactDOM from 'react-dom';
import ToggleButton from './cmn/ToggleButton';

const Demo: React.FC = () => {
  return <div>Demo Page!<button type="button" class='btn btn-success'>テスト</button></div>;
};

ReactDOM.render(<ToggleButton targetId="cat_contents" label="コンテンツ1" />, document.getElementById('toggle-button'));

export default Demo;
	

resources/views/demo/index.blade.php


  		    <div id="toggle-button"></div> <!-- ボタンをこの要素に埋め込みます -->

   			 <div id="cat_contents" style="display:none;">
				秋の初めごろはバッタやコオロギなどの虫たちが活発に活動します。<br>
				キュウリの苗のほとんどは、それらの虫たちに食べつくされてしまいます。<br>
				しかし、雑草の側で育つキュウリは、虫の被害が半分で済みます。<br>
				#自然 #農業
   			 </div>
	

配列データから一覧を生成する

resources/ts/demo/ItemList.tsx


import React from 'react';

interface Item {
  id: number;
  name: string;
}

const ItemList: React.FC = () => {
  const items: Item[] = [
    { id: 1, name: "アイテム1" },
    { id: 2, name: "アイテム2" },
    { id: 3, name: "アイテム3" },
  ];

  return (
    <div>
      <h1>アイテム一覧の見本</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default ItemList;
	

resources/ts/demo.tsx


import React from 'react';
import ReactDOM from 'react-dom';
import ItemList from './demo/ItemList';

ReactDOM.render(<ItemList />, document.getElementById('item_list'));

export default Demo;
	

resources/views/demo/index.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>
        @viteReactRefresh
        @vite(['resources/sass/app.scss', 'resources/ts/index.tsx'])
    </head>
    <body>

  		<div id="item_list"></div>
  		
    </body>
</html>

	

テキストボックスで入力した値を一覧に追加する

resources/ts/demo/itemListAdd.tsx


import React, { useState } from 'react';

interface Item {
  id: number;
  name: string;
}

const ItemListAdd: React.FC = () => {
  const [inputValue, setInputValue] = useState<string>('');
  const [items, setItems] = useState<Item[]>([]);

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value);
  };

  const handleAddItem = () => {
    if (inputValue.trim() === '') return;

    const newItem: Item = {
      id: new Date().getTime(),
      name: inputValue,
    };

    setItems([...items, newItem]);
    setInputValue('');
  };

  return (
    <div>
      <h1>アイテム一覧・追加</h1>
      <input
        type="text"
        placeholder="アイテム名を入力してください"
        value={inputValue}
        onChange={handleInputChange}
      />
      <button onClick={handleAddItem}>追加</button>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default ItemListAdd;
	

このコードでは、useStateフックを使用して、inputValue(テキストボックスの値)とitems(アイテムの一覧)の状態を管理しています。テキストボックスの値が変更されたときには、handleInputChange関数が呼ばれてinputValueを更新します。

また、追加ボタンをクリックすると、handleAddItem関数が呼ばれ、新しいアイテムがitemsに追加されます。アイテムは一意のID(ここではタイムスタンプを使用)とテキストボックスから取得した名前で作成されます。

最後に、<ul>内でitemsをマッピングしてリストアイテムを表示します。それぞれのリストアイテムには一意のkeyが設定されています。

このコンポーネントをルートコンポーネントに追加し、テキストボックスで入力した値を一覧に追加するシンプルなToDoリストのようなアプリケーションができあがります。必要に応じてスタイリングや他の機能を追加できます。

resources/ts/demo.tsx


import React from 'react';
import ReactDOM from 'react-dom';
import ItemListAdd from './demo/ItemListAdd';

ReactDOM.render(<ItemListAdd />, document.getElementById('item_list_add'));

export default Demo;
	

resources/views/demo/index.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>
        @viteReactRefresh
        @vite(['resources/sass/app.scss', 'resources/ts/index.tsx'])
    </head>
    <body>

  		<div id="item_list_add" class="mt-3"></div>
  		
    </body>
</html>

	


Hooks: useEffectについて

useEffect とは

useEffect は React Hooks の一部として React 16.8 で導入されました。 クラスコンポーネントのライフサイクルメソッド(例:componentDidMount, componentDidUpdate, componentWillUnmount)に相当する機能を、関数コンポーネント内で使えるように提供するものです。

主に3つの使い方があります。コンポーネントのマウント時に一度だけ実行、変数の変更時に実行、非同期処理のクリーンアップ時に実行があります。

コンポーネントのマウント時に一度だけ実行:


	useEffect(() => {
	    console.log("Component did mount!");
	}, []);
	

特定の変数の変更時に実行:


	const [count, setCount] = useState(0);
	
	useEffect(() => {
	    console.log("Count has changed!", count);
	}, [count]);
	

非同期処理のクリーンアップ時に実行:


	useEffect(() => {
	    const timerId = setInterval(() => {
	        console.log("This runs every second!");
	    }, 1000);
	
	    return () => {
	        clearInterval(timerId); // クリーンアップ関数
	    };
	}, []);
	
注意点

useEffectの詳細解説


	useEffect(() => {
	    // 何らかの副作用を起こすコード
	}, [依存配列]);
	
副作用の関数: useEffect の第1引数として渡される関数の中には、副作用を伴うコードを書きます。これにはデータの取得、DOM の更新、タイマーの設定などが含まれます。
依存配列: useEffect の第2引数として渡される配列は、副作用関数が依存する値のリストを示します。この配列内のどれかの値が変わるたびに、副作用関数は再実行されます。


hidden要素からjson文字列を取得し、パースしてデータを取得

以下の例では、useEffect内で隠し要素の値を取得し、それをパースしてステートに保存しています。
useEffectを関数コンポーネントのマウント時に一回だけ実行するのに利用しています。

また、直接DOMを操作するのはReactのアンチパターンになる場合があるため、この方法はサーバー側でレンダリングされたHTMLを操作する必要がある場合など、特定のシナリオでのみ推奨されます。
可能であれば、データをpropsやcontextを通して渡すなどのReactの"idiomatic"な方法を使用することをおすすめします。

resources/ts/demo/GetJsonTest.tsx


import React, { useEffect, useState } from 'react';

type HiddenData = {
  key: string;
  // 他の必要な型情報も追加できます
};

const GetJsonTest: React.FC = () => {
  const [data, setData] = useState<HiddenData | null>(null);

  useEffect(() => {
    const rawData = document.getElementById("data_json") as HTMLInputElement;

    if (rawData) {
      const parsedData: HiddenData = JSON.parse(rawData.value);
      setData(parsedData);
    }
  }, []); // このeffectはコンポーネントのマウント時に一度だけ実行されます

  if (!data) return null; // データがまだロードされていないか、データが存在しない場合のハンドリング

  return (
    <div>
      <h1>{data.key}</h1>
      {/* 他のデータの表示も行えます */}
    </div>
  );
};

export default GetJsonTest;
	

resources/ts/demo.tsx


import React from 'react';
import ReactDOM from 'react-dom';
import GetJsonTest from './demo/GetJsonTest';

ReactDOM.render(<GetJsonTest />, document.getElementById('get_json_test'));

export default Demo;
	

resources/views/demo/index.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>
        @viteReactRefresh
        @vite(['resources/sass/app.scss', 'resources/ts/index.tsx'])
    </head>
    <body>

  		<input type="hidden" id="data_json" value='{"key": "猫にエサを与えなかったが、イヌがエサを猫に与えた。"}' />
  		<div id="get_json_test"></div>
  		
    </body>
</html>

	


React + TypeScriptでのSPA | 旧Ajax、バックエンドはLaravel10

axiosにてSPA通信を行います。axiosのインストールについては割愛します。

フロントエンド:resources/ts/demo/SpaDemo.tsx


import React, { useState, useEffect } from 'react';
import axios from '../cmn/Spa';

const SpaDemo: React.FC = () => {
	const [data, setData] = useState<any>(null);
	const [errorHtml, setErrorHtml] = useState<string | null>(null);

	useEffect(() => {
		const fetchData = async () => {
			try {

				let postData = {'buta':'豚の高級住宅'};
				const response = await axios.post('demo/spa_demo',postData);

				if(typeof response.data === 'string'){
					setErrorHtml(response.request.responseText);
				}
				setData(response.data);

			} catch (error) {
				console.error('Error fetching data:', error);
			}
		};

		fetchData();
	}, []);

	if (!data) return <p>Loading...</p>;

	return (
		<div>
			<h1>SPA Demo</h1>
		{errorHtml && <div dangerouslySetInnerHTML={{ __html: errorHtml }} />}
			<pre>{JSON.stringify(data, null, 2)}</pre>
		</div>
	);
}

export default SpaDemo;
	

フロントエンド:resources/ts/cmn/Spa.ts



import axios from 'axios';

let baseURL = `${window.location.protocol}//${window.location.hostname}`;

if (window.location.port) {
  baseURL += `:${window.location.port}`;
}

const instance = axios.create({
  baseURL: baseURL,
  timeout: 10000,
  headers: {'X-Requested-With': 'XMLHttpRequest'},
});


const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');

if (token) {
  instance.defaults.headers.common['X-CSRF-TOKEN'] = token;
} else {
  console.error('CSRF token not found');
}

export default instance;
	

バックエンド:app/Http/Controllers/DemoController.php


class DemoController extends Controller
{

	public function spa_demo(Request $request){
		
// 		// すでにログアウトになったらlogoutであることをフロントエンド側に知らせる。
// 		if(¥Auth::id() == null) return json_encode(['err_msg'=>'logout']);
		
		$buta = $request->buta;

		$res = [];
		
		$res['buta'] = $buta;
		$res['name'] = '新しい猫2';
		$res['age'] = 1;
		$res['date'] = '2020-7-23';
		
		$json = json_encode($res, JSON_HEX_TAG | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_HEX_APOS);
		
		return $json;
	}
	
}
	

バックエンド:resources/views/demo/index.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">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <title>Laravel</title>
        @viteReactRefresh
        @vite(['resources/sass/app.scss', 'resources/ts/index.tsx'])
    </head>
    <body>
  		
  		<div id="spa_demo" class="mt-3"></div>
  		
    </body>
</html>

	

バックエンド:routes/web.php


<?php

use Illuminate¥Support¥Facades¥Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});


Route::get('demo', 'App¥Http¥Controllers¥DemoController@index');
Route::post('demo/spa_demo', 'App¥Http¥Controllers¥DemoController@spa_demo');

Auth::routes();

Route::get('/home', [App¥Http¥Controllers¥HomeController::class, 'index'])->name('home');

	

useEffectについて

応用


  useEffect(() => {
    testRefresh(params) // params配列に変化があれば更新処理を発動させる。testRefreshはuseCallbackでメモ化してある。
  }, [params])
	

ReactのuseEffectは、コンポーネントのライフサイクルにおいて副作用(side effects)を扱うためのフックです。
デフォルトでは、useEffect内の副作用はコンポーネントのマウント後(初回レンダリング後)と、コンポーネントのどの値が更新された後でも実行されます。

useEffectはどのような場面で仕様されるか?

useEffectフックは多様な用途で使用されますが、一般的によく行われる処理をいくつか挙げます:

  1. データフェッチング: コンポーネントがマウントされた後にAPIからデータを取得し、そのデータをコンポーネントの状態にセットします。 いわゆるSPAやAjaxで利用されます。
  2. イベントリスナーの設定/解除: コンポーネントのライフサイクルに応じてイベントリスナーを追加し、コンポーネントのアンマウント時にそれをクリーンアップします。
  3. 外部データの購読および解除: WebSocketやFirebaseのリスナーのような外部データソースへの購読をセットアップし、不要になったら解除します。
  4. DOMの直接操作: ライブラリ(D3.jsなど)を使用してDOMを直接操作する場合、またはフォーカスの管理、ウィンドウサイズの監視、アニメーションの実行など、DOMに対する副作用を行います。
  5. タイマーのセットアップ/解除: setTimeoutやsetIntervalを使用したタイマーの設定と、それを適切にクリーンアップします。
  6. 副作用がある関数の呼び出し: ローカルストレージへのアクセス、カスタムフックからの副作用関数の呼び出しなど、副作用を伴う任意の関数を実行します。
  7. パフォーマンスの最適化: メモ化された値やコールバックを依存関係として持ち、それらが変わった時だけ計算を行うようにして、不要なレンダリングを避けます。
  8. 状態の同期: プロップスやグローバルな状態の変化に応じて、ローカルの状態を更新します。
これらはuseEffectの一般的な使用例ですが、副作用として分類される幅広いタスクを実行するために使われます。ただし、副作用が発生するたびにコンポーネントが再レンダリングされることを避けるために、依存配列の管理には注意が必要です。

使用例


import React, { useState, useEffect } from 'react';

function ExampleComponent() {
  const [count, setCount] = useState(0);

  // マウント時とcountが変わるたびに実行される
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // 依存関係配列にcountを指定

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
	

使用例2:データのフェッチ (SPA, 旧AJAX)


	useEffect(() => {
		const fetchData = async () => {
			try {

				let postData = {'buta':'豚の高級住宅'};
				const response = await axios.post('demo/spa_demo',postData);

				if(typeof response.data === 'string'){
					setErrorHtml(response.request.responseText);
				}
				setData(response.data);

			} catch (error) {
				console.error('Error fetching data:', error);
			}
		};

		fetchData();
	}, []);
	

useCallbackについて

useCallbackフックはReactのフックの一つで、主にパフォーマンス最適化のために使用されます。 useCallbackは、メモ化されたコールバック関数を返します。 メモ化とは関数を何度も作り直さないように記憶する仕組みです。 これによりコンポーネントが再レンダリングされたときに同じ関数のインスタンスを再利用することができます。

useCallbackはuseEffectやuseMemoなどと一緒に使われることが多いようです。
一覧用のデータをフェッチするときにuseEffectと合わせてuseCallbackを用いるのが定番です。
見本

import React, { useState, useCallback } from 'react';

// React.memoでメモ化された子コンポーネント
const Button = React.memo(({ onClick, children }) => {
  console.log(`Rendering button: ${children}`);
  return (
    <button onClick={onClick}>{children}</button>
  );
});

const App = () => {
  const [count, setCount] = useState(0);
  
  // useCallbackを使用してhandleClick関数をメモ化
  const handleClick = useCallback(() => {
    // 何らかの処理を実行(この例ではカウントを増やす)
    setCount((prevCount) => prevCount + 1);
  }, []); // 依存関係配列が空なので、コンポーネントが再レンダリングされても新しい関数は作成されない

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={handleClick}>Increase Count</Button>
    </div>
  );
};

export default App;
	
この例では、handleClick関数はAppコンポーネントが再レンダリングされても再作成されません。React.memoによってメモ化されたButtonコンポーネントは、propsとして渡されたonClick関数が同一の参照である限り再レンダリングされません。useCallbackの依存配列が空なので、handleClickは一度作成されると再作成されることはありません。

useStateについて

ReactのuseStateは、関数コンポーネントで状態を持たせるためのフックです。

useStateの見本


import React, { useState } from 'react';

function MyComponent() {
  // useStateフックにより、count状態を初期値0で作成します。
  // setCountはその状態を更新する関数です。
  const [count, setCount] = useState(0);

  // この関数は、ボタンがクリックされるたびに呼ばれます。
  // これは、新しいcount値をReactに通知し、Reactはコンポーネントを再レンダリングします。
  function handleClick() {
    setCount(count + 1); // count状態を更新します。
  }

  // このUIは、コンポーネントが再レンダリングされるたびに更新されたcount値を表示します。
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>
        Click me
      </button>
    </div>
  );
}
	
上記のサンプルの場合、countとsetCountはMyComponent関数内の複数処理に対して共有される変数のようにふるまう。ローカルスコープのようなもの。

useStateの有効範囲(スコープの範囲)は、サブコンポーネントにまで及びますか?→いいえ