WordPressにReactアプリを組み込む

develop

1. 前提条件

 Wordpressの中に動的に動く仕組みを入れられないかと考えました。簡単そうなのはPythonかJavaScriptです。今回は、JavaScriptで動的プログラムを動かしたいということでReactを組み込むことに挑戦します。

 Reactの開発環境は、MacでもWindowsでもLinuxでも良いです。

 以下を参考に開発環境をご準備ください。

2. ローカル環境でReactアプリをビルドする

 今回は、クイックソートのシミュレータを動かしてみます。

 自分の開発環境で実行していきます。自分のホームディレクトリは/home/myhomeだとします。開発環境は、/home/myhome/developとします。この場合、/home/myhome/developで作業していきます。

 まずはReactアプリを作成します。

npx create-react-app quick-sort-visualization

これで、/home/myhome/develop/quick-sort-visualizationというディレクトリができてアプリ構築の一式がセットされます。自分独自のアプリ名をつける場合は、「quick-sort-visualization」の部分を自分のアプリの名前に変えて実行してください。(以下同様です)

/home/myhome/develop/quick-sort-visualizationができたので、そこに移動します。

cd quick-sort-visualization

必要なライブラリをインストールしていきます。

今回はtailwind cssをインストールします。

※2025-04-24時点で、tailwindの最新版がうまくインストールできないので一つ前のバージョンをインストールして使うことにします。

npm install -D [email protected] postcss autoprefixer

次に、このアプリにtailwindの設定ファイルを初期設定します。

npx tailwindcss init -p

これで、tailwind.config.jsができあがります。このままだとうまく動かないので、以下のように修正します。

module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],  // ← JSXに対応!
  theme: {
    extend: {},
  },
  plugins: [],
}

上記の「”./src/**/*.{js,jsx,ts,tsx}”」の部分を追記することになります。

さらにsrc/index.cssにも以下を追記します。

@tailwind base;
@tailwind components;
@tailwind utilities;

index.jsに以下を記載します。(書いてあれば確認だけでOK)

import './index.css';

QuickSortVisualizationのコードをsrc/QuickSortVisualization.jsに保存します。(新規作成して保存することになります)

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

const QuickSortVisualization = () => {
  // 配列とアルゴリズムの状態
  const [, setArray] = useState([]);
  const [steps, setSteps] = useState([]);
  const [currentStepIndex, setCurrentStepIndex] = useState(0);
  const [currentStep, setCurrentStep] = useState(null);
  const [sortingComplete, setSortingComplete] = useState(false);
  const [autoPlay, setAutoPlay] = useState(false);
  const [speed, setSpeed] = useState(500);
  const [arraySize, setArraySize] = useState(8);
  
  // クイックソートの各ステップを記録する関数
  const generateSortSteps = (arr) => {
    const steps = [];
    const array = [...arr];
    
    // 初期状態を記録
    steps.push({
      array: [...array],
      pivotIndex: -1,
      leftPointer: -1,
      rightPointer: -1,
      partitionStart: 0,
      partitionEnd: array.length - 1,
      sortedIndices: [],
      explanation: `初期配列: [${array.join(', ')}]。クイックソートを開始します。`
    });
    
    // クイックソートのアルゴリズム
    const quickSort = (arr, start, end, sortedIndices = []) => {
      if (start >= end) {
        if (start === end) {
          // 要素が1つの場合はソート済みとマーク
          sortedIndices.push(start);
          steps.push({
            array: [...arr],
            pivotIndex: -1,
            leftPointer: -1,
            rightPointer: -1,
            partitionStart: start,
            partitionEnd: end,
            sortedIndices: [...sortedIndices],
            explanation: `要素が1つの場合、既にソート済みです。`
          });
        }
        return;
      }
      
      // ピボットのインデックスと値を選択(ここでは最後の要素)
      const pivotIndex = end;
      const pivotValue = arr[pivotIndex];
      
      steps.push({
        array: [...arr],
        pivotIndex: pivotIndex,
        leftPointer: -1,
        rightPointer: -1,
        partitionStart: start,
        partitionEnd: end,
        sortedIndices: [...sortedIndices],
        explanation: `partition(${start}, ${end})を実行します。ピボット値として ${pivotValue} を選択しました。`
      });
      
      // パーティションプロセス
      let i = start - 1; // 左側の要素を追跡するインデックス
      
      for (let j = start; j < end; j++) {
        steps.push({
          array: [...arr],
          pivotIndex: pivotIndex,
          leftPointer: i,
          rightPointer: j,
          partitionStart: start,
          partitionEnd: end,
          sortedIndices: [...sortedIndices],
          explanation: `${arr[j]} とピボット値 ${pivotValue} を比較します。`
        });
        
        if (arr[j] <= pivotValue) {
          i++;
          // 要素を交換
          if (i !== j) {
            [arr[i], arr[j]] = [arr[j], arr[i]];
            steps.push({
              array: [...arr],
              pivotIndex: pivotIndex,
              leftPointer: i,
              rightPointer: j,
              partitionStart: start,
              partitionEnd: end,
              sortedIndices: [...sortedIndices],
              explanation: `${arr[j]}${pivotValue} なので、${arr[i]}${arr[j]} を交換しました。`
            });
          } else {
            steps.push({
              array: [...arr],
              pivotIndex: pivotIndex,
              leftPointer: i,
              rightPointer: j,
              partitionStart: start,
              partitionEnd: end,
              sortedIndices: [...sortedIndices],
              explanation: `${arr[j]}${pivotValue} ですが、同じ位置なので交換は不要です。`
            });
          }
        } else {
          steps.push({
            array: [...arr],
            pivotIndex: pivotIndex,
            leftPointer: i,
            rightPointer: j,
            partitionStart: start,
            partitionEnd: end,
            sortedIndices: [...sortedIndices],
            explanation: `${arr[j]} > ${pivotValue} なので、交換しません。`
          });
        }
      }
      
      // ピボットを正しい位置に移動
      i++;
      [arr[i], arr[pivotIndex]] = [arr[pivotIndex], arr[i]];
      
      steps.push({
        array: [...arr],
        pivotIndex: i, // 新しいピボット位置
        leftPointer: -1,
        rightPointer: -1,
        partitionStart: start,
        partitionEnd: end,
        sortedIndices: [...sortedIndices],
        explanation: `ピボット ${pivotValue} を正しい位置 ${i} に配置しました。これでピボットは最終的な位置にあります。`
      });
      
      // ピボットの位置をソート済みとしてマーク
      sortedIndices.push(i);
      
      // 左右の部分配列を再帰的にソート
      quickSort(arr, start, i - 1, sortedIndices);
      quickSort(arr, i + 1, end, sortedIndices);
    };
    
    // クイックソートを実行
    quickSort(array, 0, array.length - 1);
    
    // ソート完了のステップを追加
    steps.push({
      array: [...array],
      pivotIndex: -1,
      leftPointer: -1,
      rightPointer: -1,
      partitionStart: -1,
      partitionEnd: -1,
      sortedIndices: Array.from({ length: array.length }, (_, i) => i), // すべての要素をソート済みとしてマーク
      explanation: `ソートが完了しました!最終配列: [${array.join(', ')}]`
    });
    
    return steps;
  };
  
  // 新しい配列を生成する関数
  const generateArray = useCallback((size) => {
    // 重複のない値をランダムに生成
    const newArray = [];
    while (newArray.length < size) {
      const value = Math.floor(Math.random() * 100) + 1;
      if (!newArray.includes(value)) {
        newArray.push(value);
      }
    }
    
    setArray(newArray);
    const newSteps = generateSortSteps(newArray);
    setSteps(newSteps);
    setCurrentStepIndex(0);
    setCurrentStep(newSteps[0]);
    setSortingComplete(false);
    setAutoPlay(false);
  }, []);
  
  // 初期化
  useEffect(() => {
    generateArray(arraySize);
  }, [arraySize, generateArray]);
  
  // 次のステップに進む
  const nextStep = () => {
    if (currentStepIndex < steps.length - 1) {
      const nextIndex = currentStepIndex + 1;
      setCurrentStepIndex(nextIndex);
      setCurrentStep(steps[nextIndex]);
      
      if (nextIndex === steps.length - 1) {
        setSortingComplete(true);
        setAutoPlay(false);
      }
    }
  };
  
  // 前のステップに戻る
  const prevStep = () => {
    if (currentStepIndex > 0) {
      const prevIndex = currentStepIndex - 1;
      setCurrentStepIndex(prevIndex);
      setCurrentStep(steps[prevIndex]);
      setSortingComplete(false);
    }
  };
  
  // 自動再生の制御
  useEffect(() => {
    let timer;
    if (autoPlay && !sortingComplete && currentStepIndex < steps.length - 1) {
      timer = setTimeout(() => {
        nextStep();
      }, speed);
    }
    return () => clearTimeout(timer);
  }, [autoPlay, currentStepIndex, sortingComplete, speed, nextStep, steps.length]);
  
  // 配列サイズ変更時の処理
  const handleSizeChange = (e) => {
    const newSize = parseInt(e.target.value);
    setArraySize(newSize);
    generateArray(newSize);
  };
  
  // 現在の状態に基づいた配列の表示
  const renderArray = () => {
    if (!currentStep) return null;
    
    return currentStep.array.map((value, index) => {
      // 要素の状態に基づいてスタイルを決定
      let elementClass = "bg-gray-200"; // デフォルト(未ソート)
      
      // ソート済みインデックスの場合
      if (currentStep.sortedIndices.includes(index)) {
        elementClass = "bg-green-200";
      }
      
      // ピボット要素
      if (index === currentStep.pivotIndex) {
        elementClass = "bg-red-400";
      }
      
      // 左ポインタ
      if (index === currentStep.leftPointer) {
        elementClass = "bg-blue-400";
      }
      
      // 右ポインタ
      if (index === currentStep.rightPointer) {
        elementClass = "bg-yellow-400";
      }
      
      // 現在のパーティション範囲
      if (index >= currentStep.partitionStart && 
          index <= currentStep.partitionEnd && 
          currentStep.partitionStart !== -1 && 
          currentStep.partitionEnd !== -1 &&
          !currentStep.sortedIndices.includes(index) &&
          index !== currentStep.pivotIndex &&
          index !== currentStep.leftPointer &&
          index !== currentStep.rightPointer) {
        elementClass = "bg-purple-100";
      }
      
      // 要素の高さをその値に比例させる(最大値を制限)
      const maxHeight = 150; // 最大高さを制限(px)
      const scaleFactor = 1.5; // スケールファクターを小さくする
      const height = `${Math.min(value * scaleFactor + 20, maxHeight)}px`;
      
      return (
        <div
          key={index}
          className={`w-8 sm:w-10 md:w-12 flex flex-col items-center transition-all duration-300 ${elementClass}`}
          style={{ height }}
        >
          <div className="mt-auto font-bold text-xs sm:text-sm">{value}</div>
          <div className="text-xs mt-1">{index}</div>
        </div>
      );
    });
  };
  
  return (
    <div className="flex flex-col p-4 bg-gray-50 rounded-lg shadow-md mx-auto w-full" style={{ maxWidth: "800px" }}>
      <h1 className="text-2xl font-bold text-center mb-4">クイックソートの視覚化</h1>
      
      <div className="flex justify-center mb-6 w-full overflow-x-auto">
        <div className="flex items-end space-x-1 mx-auto">
          {renderArray()}
        </div>
      </div>
      
      <div className="mb-4 p-3 bg-white rounded shadow">
        <h3 className="font-semibold mb-2 text-sm">説明:</h3>
        <p className="text-sm">{currentStep?.explanation || ''}</p>
      </div>
      
      <div className="mb-4">
        <div className="flex flex-wrap justify-center items-center gap-2 mb-4">
          <button
            onClick={() => generateArray(arraySize)}
            className="bg-indigo-600 text-white px-3 py-1 text-sm rounded hover:bg-indigo-700"
          >
            新しい配列を生成
          </button>
          
          <button
            onClick={prevStep}
            disabled={currentStepIndex <= 0}
            className={`px-3 py-1 text-sm rounded ${
              currentStepIndex <= 0 ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700'
            }`}
          >
            前へ
          </button>
          
          <button
            onClick={nextStep}
            disabled={currentStepIndex >= steps.length - 1}
            className={`px-3 py-1 text-sm rounded ${
              currentStepIndex >= steps.length - 1 ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700'
            }`}
          >
            次へ
          </button>
          
          <button
            onClick={() => setAutoPlay(!autoPlay)}
            disabled={currentStepIndex >= steps.length - 1}
            className={`px-3 py-1 text-sm rounded ${
              autoPlay ? 'bg-red-600 text-white hover:bg-red-700' : 'bg-green-600 text-white hover:bg-green-700'
            } ${currentStepIndex >= steps.length - 1 ? 'opacity-50 cursor-not-allowed' : ''}`}
          >
            {autoPlay ? '停止' : '自動再生'}
          </button>
        </div>
        
        <div className="flex flex-wrap justify-center items-center gap-4 text-sm">
          <div className="flex items-center">
            <label htmlFor="speedRange" className="mr-2">速度:</label>
            <input
              id="speedRange"
              type="range"
              min="100"
              max="2000"
              step="100"
              value={speed}
              onChange={(e) => setSpeed(parseInt(e.target.value))}
              className="w-24 sm:w-32"
            />
            <span className="ml-2">{speed}ms</span>
          </div>
          
          <div className="flex items-center">
            <label htmlFor="sizeRange" className="mr-2">配列サイズ:</label>
            <input
              id="sizeRange"
              type="range"
              min="4"
              max="15"
              value={arraySize}
              onChange={handleSizeChange}
              className="w-24 sm:w-32"
            />
            <span className="ml-2">{arraySize}</span>
          </div>
          
          <div>
            <span className="font-semibold">進捗: </span>
            <span>{currentStepIndex + 1} / {steps.length}</span>
          </div>
        </div>
      </div>
      
      <div className="bg-white p-3 rounded shadow text-sm">
        <h3 className="font-semibold mb-2">カラー凡例:</h3>
        <div className="flex flex-wrap justify-center gap-2 sm:gap-3">
          <div className="flex items-center">
            <div className="w-4 h-4 bg-green-200 mr-1"></div>
            <span>ソート済み</span>
          </div>
          <div className="flex items-center">
            <div className="w-4 h-4 bg-red-400 mr-1"></div>
            <span>ピボット</span>
          </div>
          <div className="flex items-center">
            <div className="w-4 h-4 bg-blue-400 mr-1"></div>
            <span>左ポインタ</span>
          </div>
          <div className="flex items-center">
            <div className="w-4 h-4 bg-yellow-400 mr-1"></div>
            <span>右ポインタ</span>
          </div>
          <div className="flex items-center">
            <div className="w-4 h-4 bg-purple-100 mr-1"></div>
            <span>処理範囲</span>
          </div>
        </div>
      </div>
    </div>
  );
};

export default QuickSortVisualization;

src/App.jsを以下のように編集します。

import React from 'react';
import QuickSortVisualization from './QuickSortVisualization';

function App() {
return (
<div className="App">
<header className="App-header">
<h1>クイックソート(Quick Sort)</h1>
</header>
<main>
<QuickSortVisualization />
</main>
</div>
);
}

export default App;

src/index.jsの中にあるrootの部分をquick-sortに置き換えます。(※1)

const root = ReactDOM.createRoot(document.getElementById('quick-sort'));

ここまでできたらアプリをビルドします。

npm run build

ローカルで動かすには、あとひと作業します。

public/index.htmlを修正します。

<div id="merge-sort"></div>

という箇所がありますので

<div id="quick-sort"></div>

のように修正します。これは、先におこなった(※1)Domツリーのノード名をrootからquick-sortに修正したことへの対応です。これを行っておくことで、複数のアプリをノード名で切り替えられるようになります。(詳しい説明は長くなるので今回はここまでです)

ここまでできたらローカルで動かしてみます。

npm start

デフォルトのWebブラウザが立ち上がってReactアプリが動作します。

まずは、第Ⅰ段階終了です。

3. ローカルからリモートへ作ったモジュール類を転送する

事前にsshでリモート環境へアクセスできるように準備しておきます。

まず、最初にリモート環境を整えます。

1. ローカルからリモートへログイン

リモートのサーバーへsshでログインします。

ssh -i ~/.ssh/(秘密鍵) (サーバーユーザー名)@(サーバー名) -p (ポート番号)

それぞれを自分の固有値に設定してログインしてください。

2. リモートサーバーで格納用ディレクトリ作成

次に、Wordpressのインストールされたディレクトリに移動してwp-content/pluginsへ移動します。

このディレクトリに、プラグインの名称のディレクトリを作成します。

今回は、my-react-pluginとします。

mkdir my-react-plugin

さらに、quick-sortを配置するディレクトリを作成します。

cd my-react-plugin
mkdir quick-sort

ここまでで、リモートサーバー側の作業は一段落です。

3. ローカルからリモートへファイルをコピー

作業は、先にアプリをビルドした/home/myhome/develop/quick-sort-visualizationで行います。

今回は、buildディレクトリ以下をコピーします。

scp -r -i ./ssh/(秘密鍵) -P (ポート番号) ./build/* (ユーザー名)@(サーバー名):/(自分の環境に合わせたディレクトリ)/wp-content/plugins/my-react-plugin/quick-sort

これでファイル転送完了です。

4. WordPressへの組み込み

1. リモートサーバーでWordpressの設定ファイルを記述

さて、2.の終わりの段階で、my-react-pluginディレクトリにいます。

ここに、my-react-plugin.phpというファイルを作成します。

function my_plugin_enqueue_scripts() {
    $plugin_url = plugin_dir_url(__FILE__);
    // quick sort 用
    wp_enqueue_script('quick-sort-js', $plugin_url . 'quick-sort/static/js/main.????????.js', array(), '1.0.0', true);

    // app2 用
    wp_enqueue_script('my-react-app2-js', $plugin_url . 'app2/static/js/main.????????.js', array(), '1.0.0', true);

    // CSSも必要なら読み込む
    wp_enqueue_style(
        'quick-sort-css',
        $plugin_url . 'quick-sort/static/css/main.????????.css',
        array(), 
        '1.0.0'
    );

    wp_enqueue_style(
        'app2-css',
        $plugin_url . 'app2/static/css/main.????????.css',
        array(), 
        '1.0.0'
    );
}
add_action('wp_enqueue_scripts', 'my_plugin_enqueue_scripts');
    
// ショートコードでそれぞれに対応する <div> を返す
function render_quick_sort_div() {
    return '<div id="quick-sort"></div>';
}
add_shortcode('quick_sort', 'quick_sort_render_div');
    
function render_app2_div() {
    return '<div id="root-app2"></div>';
}
add_shortcode('my_react_app2', 'app2_render_div');

上記では、quick-sortとapp2という2つのアプリを登録する場合を例にしています。

上のブロックでは、jsとcssを登録します。

下のブロックでは、登録したjsを呼び出すショートコードを登録します。

????????となっている部分は、ハッシュ値がそれぞれについているのでそれをlsなどで確認して置き換えてください。

2. WordPressのプラグインを有効化

ここまでくると、プラグインとして認識されます。

WordPressの管理画面で「プラグイン」を開きます。

「my plugin」というプラグインが表示されていると思いますので、有効化します。

3. ショートコードの設置

ここまでくればほぼ終了です。

このプラグインを設置したい箇所にショートコードを挿入します。

先のコードだと[quick_sort]や[my_react_app2]となります。

4. 動作確認

設置したショートコードからReactが呼び出されるか確認しましょう。

無事しれません。動作したら完了です。お疲れさまでした。

サンプルでつけたQuick SortのReactでお試しいただけると、より具体的なイメージが湧くかもしれません。

タイトルとURLをコピーしました