みなさん、こんにちは! TDX 2026 では新機能「Salesforce Multi-Framework」が発表され、Salesforce の UI を React を使って開発することができるようになりました。
注意: 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: テンプレートを展開
まずターミナルを開き、次のコマンドを実行します。
1sf template generate ui-bundle -n myreactapp -d "./force-app/main/default/uiBundles" -t reactbasicforce-app/main/default/myreactapp ディレクトリに必要なファイルが展開されます。
このディレクトリに移動して、install と run dev を実行します。
1# ディレクトリに移動
2cd force-app/main/default/uiBundles/myreactapp
3
4# 必要なライブラリをインストール
5npm install
6
7# ローカルで実行
8npm run dev
ブラウザを開いて、表示された URL にアクセスしてみます。次のように表示されたら成功です。
ターミナルで Ctrl + C でローカルのサーバーを止めておきます。
ステップ2: ビルドとデプロイ
2026年4月25日時点ですが、graphqlClient.ts でエラーを検出しています。当該ファイルの13行目を次のように修正します。
1修正前: const response = await data.graphql?.<TData, TVariables>(query, variables);
2修正後: const response = await data.graphql?.<TData, TVariables>({query, variables});
それではビルドを行います。同じフォルダで次のコマンドを実行します。
1npm run buildエラーが出なければ成功です。
それでは組織にデプロイしてみましょう。次のコマンドを実行します。(スクラッチ組織の場合のコマンドです)
1sf project deploy startStatus が Succeeded で完了すれば成功です。
ステップ3: 動作確認
それでは組織にデプロイされた React アプリを見てみましょう。
- アプリケーションラン���ャー > Myreactapp
先ほどのローカルテストと同じように表示されれば成功です。
ステップ4: 取引先のデータを表示できるように修正
pages ディレクトリの下に「Accounts.tsx」ファイルを作成します。(Agentforce Vibes に作ってもらいましたので、切り詰めればもっとシンプルにできるかもしれません。)
1import { useEffect, useState } from 'react';
2import { executeGraphQL } from '@/api/graphqlClient';
3import {
4 Table,
5 TableBody,
6 TableCell,
7 TableHead,
8 TableHeader,
9 TableRow,
10} from '@/components/ui/table';
11import { Skeleton } from '@/components/ui/skeleton';
12import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
13
14interface AccountNode {
15 Id: string;
16 Name: { value: string };
17 Website: { value: string | null };
18}
19
20interface AccountsData {
21 uiapi: {
22 query: {
23 Account: {
24 edges: Array<{
25 node: AccountNode;
26 }>;
27 };
28 };
29 };
30}
31
32const ACCOUNTS_QUERY = `query GetAccounts {
33 uiapi {
34 query {
35 Account {
36 edges {
37 node {
38 Id
39 Name {
40 value
41 }
42 Website {
43 value
44 }
45 }
46 }
47 }
48 }
49 }
50}`;
51
52export default function Accounts() {
53const [accounts, setAccounts] = useState<AccountNode[]>([]);
54const [loading, setLoading] = useState(true);
55const [error, setError] = useState<string | null>(null);
56
57 useEffect(() => {
58const fetchAccounts = async () => {
59try {
60 setLoading(true);
61 setError(null);
62const data = await executeGraphQL<AccountsData, undefined>(ACCOUNTS_QUERY);
63const accountNodes = data.uiapi.query.Account.edges.map(edge => edge.node);
64 setAccounts(accountNodes);
65 } catch (err) {
66 setError(err instanceof Error ? err.message : 'Failed to fetch accounts');
67 console.error('Error fetching accounts:', err);
68 } finally {
69 setLoading(false);
70 }
71 };
72 fetchAccounts();
73 }, []);
74
75if (loading) {
76return (
77 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
78 <h1 className="text-3xl font-bold text-gray-900 mb-6">取引先一覧</h1>
79 <div className="space-y-2">
80 <Skeleton className="h-12 w-full" />
81 <Skeleton className="h-12 w-full" />
82 <Skeleton className="h-12 w-full" />
83 <Skeleton className="h-12 w-full" />
84 <Skeleton className="h-12 w-full" />
85 </div>
86 </div>
87 );
88 }
89
90if (error) {
91return (
92 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
93 <h1 className="text-3xl font-bold text-gray-900 mb-6">取引先一覧</h1>
94 <Alert variant="destructive">
95 <AlertTitle>エラー</AlertTitle>
96 <AlertDescription>{error}</AlertDescription>
97 </Alert>
98 </div>
99 );
100 }
101
102return (
103 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
104 <div className="mb-6">
105 <h1 className="text-3xl font-bold text-gray-900">取引先一覧</h1>
106 全{accounts.length}件の取引先が登録されています
107 </div>
108 {accounts.length === 0 ? (
109 <Alert>
110 <AlertTitle>取引先がありません</AlertTitle>
111 <AlertDescription>
112 現在、登録されている取引先はありません。
113 </AlertDescription>
114 </Alert>
115 ) : (
116 <div className="border rounded-lg overflow-hidden">
117 <Table>
118 <TableHeader>
119 <TableRow>
120 <TableHead className="font-semibold">ID</TableHead>
121 <TableHead className="font-semibold">取引先名</TableHead>
122 <TableHead className="font-semibold">ウェブサイト</TableHead>
123 </TableRow>
124 </TableHeader>
125 <TableBody>
126 {accounts.map(account => (
127 <TableRow key={account.Id}>
128 <TableCell className="font-mono text-sm">
129 {account.Id}
130 </TableCell>
131 <TableCell className="font-medium">
132 {account.Name.value}
133 </TableCell>
134 <TableCell>
135 {account.Website.value ? (
136 <a
137 href={account.Website.value}
138 target="_blank" rel="noopener noreferrer"
139className="text-blue-600 hover:underline">
140 {account.Website.value}
141 </a>
142 ) : ('—')}
143 </TableCell>
144 </TableRow>
145 ))}
146 </TableBody>
147 </Table>
148 </div>
149 )}
150 </div>
151 );
152}
routes.tsx ファイルを次の内容に置き換えます。
1import type { RouteObject } from 'react-router';
2import AppLayout from '@/appLayout';
3import Home from './pages/Home';
4import Accounts from './pages/Accounts';
5import NotFound from './pages/NotFound';
6
7export const routes: RouteObject[] = [
8 {
9 path: '/',
10 element: <AppLayout />,
11 children: [
12 {
13 index: true,
14 element: <Home />,
15 handle: { showInNavigation: true, label: 'Home' },
16 },
17 {
18 path: 'accounts',
19 element: <Accounts />,
20 handle: { showInNavigation: true, label: '取引先' },
21 },
22 {
23 path: '*',
24 element: <NotFound />,
25 },
26 ],
27 },
28];
ステップ5: 再ビルドとデプロイ
ファイルの修正が完了したら、再度ビルドを行い組織にデプロイします。
1# ビルドの実行
2npm run build
3
4# デプロイの実行
5sf project deploy start
6
7# もしソースファイルエラーやコンフリクトが発生した場合(コマンドを実行するディレクトリによって --source-dir の引数は要調整)
8sf project deploy start --source-dir . --ignore-conflictsStatus が Succeeded で完了すれば成功です。
ステップ6: 動作確認
スクラッチ組織の場合、取引先にはデータが何もないのでいくつか作成しておきます。
再度 Myreactapp を表示します。画面右上のメニューアイコンを押すと「取引先」選択肢が出現し、選択すると取引先の一覧が表示されれば成功です。
おわりに
今までは Lightnign Experience & Lightning Web Components と Visualforce に頼っていた Web ブラウザへの UI 提供ですが、React にも対応したことで、そのデータの表現力が格段に向上しました。いわゆる社内向けの UI はもちろん、Experience Cloud での社外向けサイトでの利用も可能です。正式リリースまで今しばらくお待ちください。
今回はすべて手動での作業手順でご紹介しましたが、もちろんバイブコーディングで開発を進めていくこともできます。Agentforce Vibes はもちろん、その他のコーディング支援ツールでも利用できるようにスキルを公開していたりしますので、ぜひご活用ください。
SFDX プロジェクトの新規作成を行う際に、React Internal App を選ぶとより作り込まれたテンプレートが展開されます。Agentforce で作成したエージェントとのチャットも行える UI も組み込まれています。こちらもぜひ試してみてください。

