0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Salesforce ReactデプロイとApex/LWC実装ポイント【Summer'26】

0
Posted at

はじめに

SalesforceでReactのコードがそのままデプロイできると聞いて、手元の環境で試してみました。

LWCのAPIやApexのユーザーモード対応など、実装で影響が出そうな変更点も同時にSummer '26で入っています。

この記事ではsf template generate ui-bundleを使ったセットアップから、Apexのwith sharingデフォルト化やLWCのState Managerまで、実装者目線でポイントを整理します。ベータ機能特有の制約にも触れるので、導入前の確認に役立てば嬉しいです。

SalesforceでReactが動く、Multi-Frameworkとは

Salesforce Multi-Frameworkは、Reactのようなフレームワークを使ってSalesforce上に直接アプリを作れる新しい仕組みです。

そもそもフレームワークというのは、アプリを作るときの土台になる部品セットのようなものです。Excelでいうとテンプレート集に近いイメージで、ゼロから全部組み立てなくて済むようにしてくれます。

これまでSalesforceでアプリを作るときは、LWCかAuraという専用の仕組みを使うのが基本でした。世の中で人気のReactやVueをそのまま使うことはできませんでした。Multi-Frameworkはこの制約を取り払って、Reactのコードをそのままデプロイできるようにしたものです。

正直、最初に「SalesforceでReactが動く」と聞いたときは、また新しいツールを覚えるのかと身構えました。でも実態はシンプルで、GraphQLでレコードを取得したりApexのメソッドを呼び出したりする部分は@salesforce/sdk-dataというパッケージが面倒を見てくれます。

現時点ではオープンベータという扱いで、スクラッチ組織とサンドボックスでしか使えません。本番環境にはまだデプロイできない点は覚えておいてください。生成には新しいAgentforce Vibesの画面から「React App」タイルを選ぶ方法と、Salesforce CLIのsf template generate ui-bundleコマンドを使う方法があります。

LWCが不要になるわけではなく、両方が並行して動く形です。既存のLWCコンポーネントはこれまで通り動きますし、LightningページにReactコンポーネントを埋め込むマイクロフロントエンドの仕組みも開発者向けプレビューとして進んでいます。

つまり、Reactの経験がある人がSalesforce開発に参加しやすくなった、という理解でOKです。

出典:Build with React, Run on Salesforce: Introducing Salesforce Multi-Framework

以下、記事に追記できる形でセットアップ手順とサンプルコードをまとめました。Case(取引先ケース)を一覧表示するだけのシンプルな例にしています。

実際に手を動かしてみる

正直に言うと、ここから先は何度か手戻りがありました。最初はブログの紹介コードをそのまま信じて書き進めたのですが、実際に生成されるテンプレートの中身とはいくつかズレがあり、そのたびに型定義を見ながら直す羽目になりました。最終的に動いたコードと、そこにたどり着くまでの手順をまとめます。

ひな形を作る

sf template generate ui-bundle --name MyReactApp --template reactbasic --output-dir force-app/main/default/uiBundles

--nameは必須フラグです。付け忘れると「Missing required flag name」と怒られます。--template reactbasicまで指定することで、React用のVite設定一式が入った状態で生成されます。

cd force-app/main/default/uiBundles/MyReactApp
npm install

中身を自分の目で確認する

ここが今回一番大事だったところです。ブログの説明を鵜呑みにせず、まずpackage.jsonで実際に何が入っているか見ます。

cat package.json

執筆時点では@salesforce/platform-sdkというパッケージが入っていました。データ取得はこちらが担当します。

続けてsrcの構成も確認します。

find src -type f

reactbasicテンプレートは、次のような役割分担になっていました。

  • src/app.tsx — アプリのエントリーポイント。react-routerのルーティングを組んでいるだけで、画面の中身はここには書かない
  • src/routes.tsx — URLと表示するページの対応表
  • src/pages/Home.tsx — トップページの中身
  • src/components/ — 自作コンポーネントの置き場所

つまり画面に何かを足したいときは、エントリーポイントではなくsrc/pages配下のページを直接編集するのが正解でした。

Caseを一覧表示するコンポーネントを書く

src/components/CaseList.tsxを新規作成します。

import { useState, useEffect } from 'react';
import { createDataSDK } from '@salesforce/platform-sdk';

type CaseRecord = {
  Id: string;
  Subject: { value: string };
  Status: { value: string };
  Priority: { value: string };
};

type CaseQueryResult = {
  uiapi: {
    query: {
      Case: {
        edges: { node: CaseRecord }[];
      };
    };
  };
};

function statusBadgeClass(status: string) {
  switch (status) {
    case 'Closed':
      return 'bg-gray-100 text-gray-600 border-gray-300';
    case 'New':
      return 'bg-blue-50 text-blue-700 border-blue-300';
    default:
      return 'bg-yellow-50 text-yellow-700 border-yellow-300';
  }
}

function priorityBadgeClass(priority: string) {
  switch (priority) {
    case 'High':
      return 'bg-red-50 text-red-700 border-red-300';
    case 'Medium':
      return 'bg-orange-50 text-orange-700 border-orange-300';
    default:
      return 'bg-green-50 text-green-700 border-green-300';
  }
}

export function CaseList() {
  const [cases, setCases] = useState<CaseRecord[]>([]);

  useEffect(() => {
    async function fetchCases() {
      const sdk = await createDataSDK();

      if (!sdk.graphql) {
        console.error('この画面ではGraphQLによるデータ取得がサポートされていません。');
        return;
      }

      const result = await sdk.graphql.query<CaseQueryResult>({
        query: `
          query GetCases {
            uiapi {
              query {
                Case {
                  edges {
                    node {
                      Id
                      Subject { value }
                      Status { value }
                      Priority { value }
                    }
                  }
                }
              }
            }
          }
        `,
      });

      const edges = result.data?.uiapi.query.Case.edges ?? [];
      setCases(edges.map((e) => e.node));
    }

    fetchCases();
  }, []);

  return (
    <div className="max-w-3xl mx-auto mt-8 rounded border border-gray-300 bg-white shadow-sm overflow-hidden">
      <div className="flex items-center gap-2 border-b border-gray-200 bg-gray-50 px-4 py-3">
        <div className="h-6 w-6 rounded bg-blue-600 flex items-center justify-center text-white text-xs font-bold">
          C
        </div>
        <h2 className="text-sm font-semibold text-gray-700">Cases</h2>
        <span className="ml-auto text-xs text-gray-400">{cases.length}</span>
      </div>

      <ul className="divide-y divide-gray-100">
        {cases.map((c) => (
          <li
            key={c.Id}
            className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors"
          >
            <span className="text-sm text-gray-800 truncate">{c.Subject.value}</span>
            <div className="flex gap-2 shrink-0 ml-4">
              <span className={`text-xs px-2 py-0.5 rounded-full border ${statusBadgeClass(c.Status.value)}`}>
                {c.Status.value}
              </span>
              <span className={`text-xs px-2 py-0.5 rounded-full border ${priorityBadgeClass(c.Priority.value)}`}>
                {c.Priority.value}
              </span>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

createDataSDK()はPromiseを返す非同期関数なのでawaitが必要です。sdk.graphqlは関数ではなくオブジェクトで、その中のqueryメソッドを呼ぶ形になっています。見た目は白いカードに青いヘッダー帯、ステータスと優先度を色分けバッジにして、Lightningのリストビューに寄せています。

既存のページに組み込む

src/pages/Home.tsxにimportと呼び出しを足すだけです。

import { CaseList } from '@/components/CaseList';

export default function Home() {
  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <div className="text-center">
        <h1 className="text-4xl font-bold text-gray-900 mb-4">Home</h1>
        <p className="text-lg text-gray-600 mb-8">
          Welcome to your React application.
        </p>
      </div>
      <CaseList />
    </div>
  );
}

エントリーポイント(app.tsx)は一切触りません。ルーティングの入り口と、画面の中身は別物という切り分けです。

動かして、デプロイする

npm run dev

localhost:5173を開くと、認証済みのorgに対して実際にクエリが飛び、Case一覧がカード形式で表示されます。

動作を確認できたらビルドしてプロジェクトルートに戻ってからデプロイします。

npm run build
sf project deploy start --source-dir force-app/main/default/uiBundles/MyReactApp

デプロイ後、App Launcherで「MyReactApp」を検索すればアプリが起動するはずです。

つまずいたポイント

ここまでの過程で、実際に何度かエラーに当たりました。

  • パッケージ名が公式ブログの記述(@salesforce/sdk-data)と実際のもの(@salesforce/platform-sdk)で違っていた
  • createDataSDK()は同期関数ではなく、Promiseを返す非同期関数だった
  • sdk.graphqlは関数ではなく、querymutateを持つオブジェクトだった
  • テンプレートはreact-routerでルーティングされており、画面を変えるにはapp.tsxではなくsrc/pages配下を編集する必要があった

ベータ機能を触っていると、紹介記事のサンプルコードと実際に生成されるコードにズレがあることは珍しくありません。動かなかったときは、まずpackage.jsonと実際のディレクトリ構成を自分の目で確認し、型定義(.d.ts)を直接読む。これが結局一番の近道でした。

Apexがセキュリティ重視の設計に変わった

Summer '26のAPIバージョン67.0から、Apexの安全のための初期設定がいくつも変わりました。

一番大きいのは、SOQLやDMLといったデータベース操作が、デフォルトでユーザーモードになったことです。ユーザーモードというのは、実行しているユーザー本人の権限だけでデータを扱う動き方のことです。これまでは何も指定しないと管理者権限に近い形でデータへアクセスできてしまう場面がありました。67.0以降はその逆で、権限を広げたいときにこそ明示的な宣言が必要になります。

with sharingを書かずにクラスを作ると、これまでは共有ルールを無視するwithout sharing扱いでした。67.0からはこれが逆転して、何も書かなければwith sharingが既定になります。共有ルールを無視したいときのほうが、あえて宣言する必要があるわけです。

長らく使われてきたWITH SECURITY_ENFORCEDは、67.0のクラスではコンパイルが通らなくなりました。代わりにWITH USER_MODEを使う書き方に統一されています。私も過去のコードをそのまま流用しようとしてエラーになった経験があるので、既存プロジェクトを67.0に上げる前に必ずチェックしておいた方が良さそうです。

地味に嬉しいのが、トリプルクォート(''')による複数行文字列と、String.template()による名前付き埋め込みです。JSONやメール本文を組み立てるときの文字列連結地獄から解放されます。

つまり、権限まわりの初期設定がより安全な方向に変わった、という理解でOKです。

LWCもState Managerを中心に大きく進化

LWC側もSummer '26でまとまったアップデートが入りました。

目玉はState Managers、日本語でいう状態管理レイヤーのGA(正式リリース)です。これまでコンポーネントの中に書いていたデータとその操作ロジックを、外側の専用モジュールに切り出せるようになりました。

@lwc/stateパッケージのdefineStateという仕組みを使い、
atomで状態を持ち、
computedで派生値を作り、
setAtomでしか値を書き換えられない

というルールになっています。

ロジックがコンポーネントから独立するので、画面を描画しなくてもロジック単体でテストできるようになります。実務で使うなら、ここはテストのしやすさに直結するのでちょっと注目しておいた方が良さそうです。

もうひとつ覚えておきたいのが、lightning/accApiという新しいモジュールです。これはLWCのコンポーネントからAgentforceのサイドパネルを操作できる仕組みで、「このレコードを要約して」のようなボタンを自分のコンポーネントに仕込めます。open、close、executeの3つの非同期メソッドだけで動くシンプルな作りです。

セキュリティ面では、Lightning Web Securityがdata: URI形式のダウンロードをブロックするようになりました。アンカータグのhrefに直接データURLを入れてファイルをダウンロードさせる実装をしている場合は、動かなくなるので注意してください。blob:形式のURLに切り替えるのが公式の対処法です。

大量データの表示に悩んでいた人には、仮想化に対応したlightning-dynamic-list-containerも朗報です。数千件のリストでも、画面に見えている部分だけを描画してくれます。まだ開発者向けプレビューの段階ですが、今後の定番になりそうな機能です。

つまり、State Managerでロジックを整理しつつ、Agentforceとの連携もLWCから直接できるようになった、という理解でOKです。

出典:The Salesforce Developer's Guide to the Summer '26 Release

ここは注意!つまずきポイント

Multi-Frameworkは、今のところスクラッチ組織とサンドボックスでしか試せない

本番環境で使おうとして「あれ、デプロイできない」とつまずく人が出そうなので気をつけてください。対応言語も英語のみで、日本語組織ではまだ試せない点も要注意です。

Apexのwith sharingがデフォルトになる変更は、APIバージョン67.0でコンパイルしたクラスにだけ影響

既存の古いクラスが突然挙動を変えるわけではないので、そこは安心してください。ただし新規クラスや67.0へバージョンアップしたクラスでは、意図せず権限が絞られて「データが取れない」という問い合わせが増える可能性があります。

追加費用については、今回調べた公式ブログの中では特に別料金の記載は見当たりませんでした。Summer '26のプラットフォームリリースに含まれる形で提供されているようですが、正式な価格体系はエディションごとのリリースノートで確認することをおすすめします。

まとめ

  • Salesforce Multi-FrameworkでReactアプリをSalesforce上に直接デプロイできるようになった(現在はオープンベータ、スクラッチ組織とサンドボックスのみ)
  • Apexはユーザーモードやwith sharingがデフォルトになり、セキュリティ重視の設計に変わった
  • LWCはState Managerによるロジック分離とlightning/accApiによるAgentforce連携が目玉
  • どの機能もベータや開発者向けプレビューの段階のものが多く、本番導入は情報を見ながら慎重に進めたい

新しい仕組みが増えると身構えてしまいますが、一つずつ触ってみると意外と手に馴染みます。少しずつ慣れていきましょう!


AI×資格学習の情報をnoteで発信しています。

note


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?