Salesforce Mult-Framework を使用した React による UI 開発


みなさん、こんにちは! TDX 2026 では新機能「Salesforce Multi-Framework」が発表され、Salesforce の UI を React を使って開発することができるようになりました。

  • Agentforce Vibes Developer Guide: Build a React App with Salesforce Multi-Framework (Beta)

注意: 2026年4月時点ではまだ Beta 版です。

この記事では、Developer Edition で作成したスクラッチ組織を使い、内部ユーザーが利用する想定のシンプルなアプリを作成する手順を紹介します。スクラッチ組織の作成方法については、かこのブログ記事「Developer Edition + スクラッチ組織で、複数の開発・検証組織を手にいれる」をご参照ください。ログインして表示言語を日本語化した状態から進めていきます。

事前準備

まずは VS Code で新規に「Standard」で作成したプロジェクトを用意します。



React External App や React Internal App もあるのですが、結構作り込まれたテンプレートが展開されます。構造を理解することがやや大変なので、今回はシンプルなテンプレートからの作成を紹介します。

このあと、スクラッチ組織を作成、もしくは既存のスクラッチ組織に接続します。組織にログインして機能有効化を行ってください。

  • 設定 > アプリケーション > Salesforce Multi-Framework を使用した React 開発 (ベータ)
  • 「Enable Beta」ボタンを押す



この機能はオフにできませんので、必ず Developer Edition や Sandbox などで試すようにしてください。

このあとはローカルの VS Code での操作を進めます。Standard で作成したプロジェクトで、スクラッチ組織に接続した状態にしておきます。

ステップ1: テンプレートを展開

まずターミナルを開き、次のコマンドを実行します。

sf template generate ui-bundle -n myreactapp -d "./force-app/main/default/uiBundles" -t reactbasic

force-app/main/default/myreactapp ディレクトリに必要なファイルが展開されます。



このディレクトリに移動して、install と run dev を実行します。

# ディレクトリに移動
cd force-app/main/default/uiBundles/myreactapp

# 必要なライブラリをインストール
npm install

# ローカルで実行
npm run dev



ブラウザを開いて、表示された URL にアクセスしてみます。次のように表示されたら成功です。



ターミナルで Ctrl + C でローカルのサーバーを止めておきます。

ステップ2: ビルドとデプロイ

2026年4月25日時点ですが、graphqlClient.ts でエラーを検出しています。当該ファイルの13行目を次のように修正します。

修正前: const response = await data.graphql?.<TData, TVariables>(query, variables);
修正後: const response = await data.graphql?.<TData, TVariables>({query, variables});



それではビルドを行います。同じフォルダで次のコマンドを実行します。

npm run build

エラーが出なければ成功です。



それでは組織にデプロイしてみましょう。次のコマンドを実行します。(スクラッチ組織の場合のコマンドです)

sf project deploy start

Status が Succeeded で完了すれば成功です。



ステップ3: 動作確認

それでは組織にデプロイされた React アプリを見てみましょう。

  • アプリケーションランチャー > Myreactapp



先ほどのローカルテストと同じように表示されれば成功です。



ステップ4: 取引先のデータを表示できるように修正

pages ディレクトリの下に「Accounts.tsx」ファイルを作成します。(Agentforce Vibes に作ってもらいましたので、切り詰めればもっとシンプルにできるかもしれません。)

import { useEffect, useState } from 'react';
import { executeGraphQL } from '@/api/graphqlClient';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';

interface AccountNode {
  Id: string;
  Name: { value: string };
  Website: { value: string | null };
}

interface AccountsData {
  uiapi: {
    query: {
      Account: {
        edges: Array<{
          node: AccountNode;
        }>;
      };
    };
  };
}

const ACCOUNTS_QUERY = `query GetAccounts {
  uiapi {
    query {
      Account {
        edges {
          node {
            Id
            Name {
              value
            }
            Website {
              value
            }
          }
        }
      }
    }
  }
}`;

export default function Accounts() {
const [accounts, setAccounts] = useState<AccountNode[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

  useEffect(() => {
const fetchAccounts = async () => {
try {
        setLoading(true);
        setError(null);
const data = await executeGraphQL<AccountsData, undefined>(ACCOUNTS_QUERY);
const accountNodes = data.uiapi.query.Account.edges.map(edge => edge.node);
        setAccounts(accountNodes);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch accounts');
        console.error('Error fetching accounts:', err);
      } finally {
        setLoading(false);
      }
    };
    fetchAccounts();
  }, []);

if (loading) {
return (
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
        <h1 className="text-3xl font-bold text-gray-900 mb-6">取引先一覧</h1>
        <div className="space-y-2">
          <Skeleton className="h-12 w-full" />
          <Skeleton className="h-12 w-full" />
          <Skeleton className="h-12 w-full" />
          <Skeleton className="h-12 w-full" />
          <Skeleton className="h-12 w-full" />
        </div>
      </div>
    );
  }

if (error) {
return (
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
        <h1 className="text-3xl font-bold text-gray-900 mb-6">取引先一覧</h1>
        <Alert variant="destructive">
          <AlertTitle>エラー</AlertTitle>
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      </div>
    );
  }

return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <div className="mb-6">
        <h1 className="text-3xl font-bold text-gray-900">取引先一覧</h1>
                  全{accounts.length}件の取引先が登録されています
      </div>
      {accounts.length === 0 ? (
        <Alert>
          <AlertTitle>取引先がありません</AlertTitle>
          <AlertDescription>
            現在、登録されている取引先はありません。
          </AlertDescription>
        </Alert>
      ) : (
        <div className="border rounded-lg overflow-hidden">
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead className="font-semibold">ID</TableHead>
                <TableHead className="font-semibold">取引先名</TableHead>
                <TableHead className="font-semibold">ウェブサイト</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {accounts.map(account => (
                <TableRow key={account.Id}>
                  <TableCell className="font-mono text-sm">
                    {account.Id}
                  </TableCell>
                  <TableCell className="font-medium">
                    {account.Name.value}
                  </TableCell>
                  <TableCell>
                    {account.Website.value ? (
                      <a
                         href={account.Website.value}
                         target="_blank" rel="noopener noreferrer"
className="text-blue-600 hover:underline">
                        {account.Website.value}
                      </a>
                    ) : ('—')}
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      )}
    </div>
  );
}



routes.tsx ファイルを次の内容に置き換えます。

import type { RouteObject } from 'react-router';
import AppLayout from '@/appLayout';
import Home from './pages/Home';
import Accounts from './pages/Accounts';
import NotFound from './pages/NotFound';

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      {
        index: true,
        element: <Home />,
        handle: { showInNavigation: true, label: 'Home' },
      },
      {
        path: 'accounts',
        element: <Accounts />,
        handle: { showInNavigation: true, label: '取引先' },
      },
      {
        path: '*',
        element: <NotFound />,
      },
    ],
  },
];



ステップ5: 再ビルドとデプロイ

ファイルの修正が完了したら、再度ビルドを行い組織にデプロイします。

# ビルドの実行
npm run build

# デプロイの実行
sf project deploy start

# もしソースファイルエラーやコンフリクトが発生した場合(コマンドを実行するディレクトリによって --source-dir の引数は要調整)
sf project deploy start --source-dir . --ignore-conflicts

Status が Succeeded で完了すれば成功です。



ステップ6: 動作確認

スクラッチ組織の場合、取引先にはデータが何もないのでいくつか作成しておきます。



再度 Myreactapp を表示します。画面右上のメニューアイコンを押すと「取引先」選択肢が出現し、選択すると取引先の一覧が表示されれば成功です。





おわりに

今までは Lightnign Experience & Lightning Web Components と Visualforce に頼っていた Web ブラウザへの UI 提供ですが、React にも対応したことで、そのデータの表現力が格段に向上しました。いわゆる社内向けの UI はもちろん、Experience Cloud での社外向けサイトでの利用も可能です。正式リリースまで今しばらくお待ちください。

今回はすべて手動での作業手順でご紹介しましたが、もちろんバイブコーディングで開発を進めていくこともできます。Agentforce Vibes はもちろん、その他のコーディング支援ツールでも利用できるようにスキルを公開していたりしますので、ぜひご活用ください。

  • Github: Agentforce Vibes Library

SFDX プロジェクトの新規作成を行う際に、React Internal App を選ぶとより作り込まれたテンプレートが展開されます。Agentforce で作成したエージェントとのチャットも行える UI も組み込まれています。こちらもぜひ試してみてください。



参考資料

  • Agentforce Vibes Developer Guide: Build a React App with Salesforce Multi-Framework (Beta)
  • Github: Agentforce Vibes Library
  • Agentforce Vibes React (Open Beta): Quick Look

The post Salesforce Mult-Framework を使用した React による UI 開発 appeared first on Salesforce Developers Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *