MochiuWiki : SUSE, EC, PCB
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
検索
個人用ツール
ログイン
Toggle dark mode
名前空間
ページ
議論
表示
閲覧
ソースを閲覧
履歴を表示
Tauriの基礎 - React連携パターンのソースを表示
提供: MochiuWiki : SUSE, EC, PCB
←
Tauriの基礎 - React連携パターン
あなたには「このページの編集」を行う権限がありません。理由は以下の通りです:
この操作は、次のグループのいずれかに属する利用者のみが実行できます:
管理者
、new-group。
このページのソースの閲覧やコピーができます。
== 概要 == TauriとReactの連携は、RustバックエンドとWebフロントエンド間のIPC (プロセス間通信) を通じて実現される。<br> <br> フロントエンドからは <code>invoke</code> 関数を使用してRustで定義されたコマンドを呼び出し、バックエンドからはイベントシステムを使用してフロントエンドに通知を送信できる。<br> <br> React連携における主な考慮事項は以下の通りである。<br> * 非同期処理の管理 *: Tauriコマンドは非同期で実行されるため、async / awaitとReactの状態管理を適切に連携させる必要がある。 * ライフサイクル管理 *: コンポーネントのマウント / アンマウントに合わせてイベントリスナーを登録 / 解除し、メモリリークを防止する必要がある。 * 型安全性 *: TypeScriptを使用して、RustとTypeScript間で型を整合させ、ランタイムエラーを防止する。 * カスタムHookの活用 *: Tauriコマンド呼び出しをカスタムHookでカプセル化して、再利用性と保守性を向上させる。 <br><br> == invokeの基本 == <code>invoke</code> 関数は、フロントエンドからRustバックエンドのコマンドを呼び出すためのAPIである。<br> <br> ==== Rustコマンドの定義 ==== まず、Rust側で <code>#[tauri::command]</code> 属性を使用してコマンドを定義する。<br> <br> <syntaxhighlight lang="rust"> // src-tauri/src/lib.rs use serde::{Deserialize, Serialize}; // カスタム型の定義 (serdeでシリアライズ/デシリアライズ) #[derive(Debug, Serialize, Deserialize)] pub struct User { pub id: u32, pub name: String, pub email: String, } // 同期コマンド #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) } // 非同期コマンド #[tauri::command] async fn get_user(id: u32) -> Result<User, String> { // データベースやAPIからの取得処理を想定 Ok(User { id, name: "John Doe".to_string(), email: "john@example.com".to_string(), }) } // 複数引数のコマンド #[tauri::command] fn calculate(a: i32, b: i32, operation: String) -> Result<i32, String> { match operation.as_str() { "add" => Ok(a + b), "subtract" => Ok(a - b), "multiply" => Ok(a * b), _ => Err(format!("Unknown operation: {}", operation)), } } // コマンドを登録 #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ greet, get_user, calculate ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } </syntaxhighlight> <br> ==== TypeScriptからの呼び出し ==== フロントエンドから <code>invoke</code> 関数を使用してコマンドを呼び出す。<br> <br> <syntaxhighlight lang="typescript"> // src/api/commands.ts import { invoke } from '@tauri-apps/api/core'; // 型定義 (Rust側の型と整合させる) interface User { id: number; name: string; email: string; } // 基本的な呼び出し export async function greet(name: string): Promise<string> { return await invoke('greet', { name }); } // ジェネリクスで戻り値の型を指定 export async function getUser(id: number): Promise<User> { return await invoke<User>('get_user', { id }); } // エラーハンドリングを含む呼び出し export async function calculate( a: number, b: number, operation: string ): Promise<number> { try { return await invoke<number>('calculate', { a, b, operation }); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`計算エラー: ${message}`); } } </syntaxhighlight> <br> ==== エラーハンドリング ==== Rust側で <code>Result<T, E></code> を返すコマンドは、エラー時にTypeScript側で例外としてスローされる。<br> <br> <syntaxhighlight lang="typescript"> // src/api/commands.ts import { invoke } from '@tauri-apps/api/core'; // カスタムエラークラス export class TauriCommandError extends Error { constructor( public command: string, message: string ) { super(`Command "${command}" failed: ${message}`); this.name = 'TauriCommandError'; } } // エラーハンドリング用ラッパー関数 export async function safeInvoke<T>( command: string, args: Record<string, unknown> = {} ): Promise<T> { try { return await invoke<T>(command, args); } catch (error) { // Tauriのエラーオブジェクトを文字列に変換 const message = typeof error === 'string' ? error : error instanceof Error ? error.message : JSON.stringify(error); throw new TauriCommandError(command, message); } } </syntaxhighlight> <br><br> == useEffectでのinvoke呼び出し == Reactコンポーネントで <code>invoke</code> を使用する場合は、<code>useEffect</code> と組み合わせてライフサイクル管理を行う。<br> <br> ==== マウント時実行パターン ==== コンポーネントのマウント時にデータを取得する基本的なパターンを以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/components/UserProfile.tsx import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; interface User { id: number; name: string; email: string; } export function UserProfile({ userId }: { userId: number }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { // 非同期関数を定義して即時実行 const fetchUser = async () => { setLoading(true); setError(null); try { const result = await invoke<User>('get_user', { id: userId }); setUser(result); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); } finally { setLoading(false); } }; fetchUser(); // クリーンアップ関数 (必要に応じて) return () => { // 非同期処理のキャンセル等があればここで実施 }; }, [userId]); // userIdが変更されたら再実行 if (loading) return <div>読み込み中...</div>; if (error) return <div>エラー: {error}</div>; if (!user) return null; return ( <div className="user-profile"> <h2>{user.name}</h2> <p>Email: {user.email}</p> </div> ); } </syntaxhighlight> <br> ==== 無限ループの防止 ==== <code>useEffect</code> の依存配列に注意しないと、無限ループが発生する可能性がある。<br> <br> <syntaxhighlight lang="typescript"> // src/components/Settings.tsx import { useState, useEffect, useRef } from 'react'; import { invoke } from '@tauri-apps/api/core'; interface Settings { theme: string; language: string; } export function SettingsPanel() { const [settings, setSettings] = useState<Settings | null>(null); const [loading, setLoading] = useState(true); // 初期化済みフラグ (無限ループ防止) const isInitialized = useRef(false); useEffect(() => { // 既に初期化済みの場合はスキップ if (isInitialized.current) return; isInitialized.current = true; const loadSettings = async () => { try { const result = await invoke<Settings>('get_settings'); setSettings(result); } catch (error) { console.error('設定の読み込みに失敗:', error); } finally { setLoading(false); } }; loadSettings(); }, []); // 空の依存配列 = マウント時のみ実行 if (loading) return <div>読み込み中...</div>; return ( <div className="settings-panel"> <h2>設定</h2> <pre>{JSON.stringify(settings, null, 2)}</pre> </div> ); } </syntaxhighlight> <br> ==== 依存配列の注意点 ==== <code>useEffect</code> の依存配列にオブジェクトや関数を含める場合は注意が必要である。<br> <br> <syntaxhighlight lang="typescript"> // src/components/SearchPanel.tsx import { useState, useEffect, useCallback, useRef } from 'react'; import { invoke } from '@tauri-apps/api/core'; interface SearchResult { id: number; title: string; } export function SearchPanel() { const [query, setQuery] = useState(''); const [results, setResults] = useState<SearchResult[]>([]); const [searching, setSearching] = useState(false); // 検索関数をuseCallbackでメモ化 const search = useCallback(async (searchQuery: string) => { if (!searchQuery.trim()) { setResults([]); return; } setSearching(true); try { const items = await invoke<SearchResult[]>('search', { query: searchQuery, }); setResults(items); } catch (error) { console.error('検索エラー:', error); setResults([]); } finally { setSearching(false); } }, []); // 依存なし = 関数は再作成されない // クエリ変更時に検索実行 (デバウンス処理) useEffect(() => { const timer = setTimeout(() => { search(query); }, 300); // 300ms デバウンス return () => clearTimeout(timer); }, [query, search]); // searchはメモ化されているので安全 return ( <div> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="検索..." /> {searching && <span>検索中...</span>} <ul> {results.map((item) => ( <li key={item.id}>{item.title}</li> ))} </ul> </div> ); } </syntaxhighlight> <br><br> == カスタムフックの作成 == Tauriコマンド呼び出しをカプセル化したカスタムフックを作成することにより、ソースコードの再利用性と保守性が向上する。<br> <br> ==== useTauriCommandフック ==== 汎用的なTauriコマンド呼び出しフックの例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/hooks/useTauriCommand.ts import { useState, useCallback, useRef, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; // Hookの戻り値の型 interface UseTauriCommandResult<T, Args extends Record<string, unknown>> { data: T | null; error: string | null; loading: boolean; execute: (args: Args) => Promise<T | null>; reset: () => void; } // 汎用TauriコマンドHook export function useTauriCommand<T, Args extends Record<string, unknown> = Record<string, unknown>>( command: string ): UseTauriCommandResult<T, Args> { const [data, setData] = useState<T | null>(null); const [error, setError] = useState<string | null>(null); const [loading, setLoading] = useState(false); // アンマウント時のクリーンアップ用 const isMounted = useRef(true); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); // コマンド実行関数 const execute = useCallback(async (args: Args): Promise<T | null> => { setLoading(true); setError(null); try { const result = await invoke<T>(command, args); // アンマウント後は状態を更新しない if (isMounted.current) { setData(result); } return result; } catch (err) { const message = typeof err === 'string' ? err : err instanceof Error ? err.message : 'Unknown error'; if (isMounted.current) { setError(message); } return null; } finally { if (isMounted.current) { setLoading(false); } } }, [command]); // 状態リセット関数 const reset = useCallback(() => { setData(null); setError(null); setLoading(false); }, []); return { data, error, loading, execute, reset }; } </syntaxhighlight> <br> ==== 使用例 ==== <code>useTauriCommand</code> フックの例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/components/FileList.tsx import { useTauriCommand } from '../hooks/useTauriCommand'; interface FileInfo { name: string; size: number; modified: string; } interface ListFilesArgs { directory: string; pattern?: string; } export function FileList() { const { data: files, error, loading, execute, } = useTauriCommand<FileInfo[], ListFilesArgs>('list_files'); const handleListFiles = async (directory: string) => { await execute({ directory, pattern: '*.txt' }); }; return ( <div> <button onClick={() => handleListFiles('/home/user/documents')} disabled={loading} > {loading ? '読み込み中...' : 'ファイル一覧を取得'} </button> {error && ( <div className="error">エラー: {error}</div> )} {files && ( <ul> {files.map((file, index) => ( <li key={index}> <strong>{file.name}</strong> <span> - {file.size} bytes</span> </li> ))} </ul> )} </div> ); } </syntaxhighlight> <br> ==== 自動実行フック ==== マウント時に自動でコマンドを実行するフックの例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/hooks/useTauriQuery.ts import { useState, useEffect, useRef, useCallback } from 'react'; import { invoke } from '@tauri-apps/api/core'; interface UseTauriQueryResult<T> { data: T | null; error: string | null; loading: boolean; refetch: () => Promise<void>; } // 自動実行クエリHook export function useTauriQuery<T, Args extends Record<string, unknown>>( command: string, args: Args, options: { enabled?: boolean; // 自動実行を有効にするか refetchInterval?: number; // 定期再取得間隔 (ms) } = {} ): UseTauriQueryResult<T> { const { enabled = true, refetchInterval } = options; const [data, setData] = useState<T | null>(null); const [error, setError] = useState<string | null>(null); const [loading, setLoading] = useState(false); const isMounted = useRef(true); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); // データ取得関数 const fetchData = useCallback(async () => { if (!enabled) return; setLoading(true); setError(null); try { const result = await invoke<T>(command, args); if (isMounted.current) { setData(result); } } catch (err) { const message = typeof err === 'string' ? err : err instanceof Error ? err.message : 'Unknown error'; if (isMounted.current) { setError(message); } } finally { if (isMounted.current) { setLoading(false); } } }, [command, JSON.stringify(args), enabled]); // 初回実行 useEffect(() => { fetchData(); }, [fetchData]); // 定期再取得 useEffect(() => { if (!refetchInterval) return; const interval = setInterval(fetchData, refetchInterval); return () => clearInterval(interval); }, [fetchData, refetchInterval]); return { data, error, loading, refetch: fetchData, }; } </syntaxhighlight> <br> ==== 自動実行フックの使用例 ==== <syntaxhighlight lang="typescript"> // src/components/SystemStatus.tsx import { useTauriQuery } from '../hooks/useTauriQuery'; interface SystemInfo { cpu_usage: number; memory_usage: number; uptime: number; } export function SystemStatus() { // 5秒ごとに自動更新 const { data, loading, error, refetch } = useTauriQuery<SystemInfo, {}>( 'get_system_info', {}, { enabled: true, refetchInterval: 5000, } ); if (error) { return <div className="error">エラー: {error}</div>; } return ( <div className="system-status"> <h2>システム状態</h2> {loading && !data && <div>読み込み中...</div>} {data && ( <div> <div>CPU使用率: {data.cpu_usage.toFixed(1)}%</div> <div>メモリ使用率: {data.memory_usage.toFixed(1)}%</div> <div>稼働時間: {Math.floor(data.uptime / 3600)}時間</div> </div> )} <button onClick={refetch} disabled={loading}> 更新 </button> </div> ); } </syntaxhighlight> <br><br> == イベントリスナーの管理 == Tauriのイベントシステムを使用して、Rustバックエンドからフロントエンドに通知を送信できる。<br> <br> ==== listenの使用 ==== <code>listen</code> 関数を使用してイベントを購読する。<br> <br> <syntaxhighlight lang="rust"> // src-tauri/src/lib.rs use tauri::{AppHandle, Emitter}; use serde::Serialize; // イベントペイロードの型定義 #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] struct DownloadProgress { download_id: usize, progress: u32, total: u32, } #[tauri::command] async fn start_download(app: AppHandle, url: String) -> Result<(), String> { // ダウンロード開始イベント app.emit("download-started", &url) .map_err(|e| e.to_string())?; // 進捗イベント (例: 25%, 50%, 75%, 100%) for progress in [25, 50, 75, 100] { app.emit("download-progress", DownloadProgress { download_id: 1, progress, total: 100, }).map_err(|e| e.to_string())?; } // 完了イベント app.emit("download-finished", &url) .map_err(|e| e.to_string())?; Ok(()) } </syntaxhighlight> <br> TypeScript側でイベントを購読する。<br> <br> <syntaxhighlight lang="typescript"> // src/components/DownloadManager.tsx import { useEffect, useState } from 'react'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; // イベントペイロードの型定義 interface DownloadProgress { downloadId: number; progress: number; total: number; } export function DownloadManager() { const [progress, setProgress] = useState(0); const [status, setStatus] = useState<'idle' | 'downloading' | 'completed'>('idle'); useEffect(() => { // イベントリスナーを登録 const unlistenPromise = Promise.all([ // ダウンロード開始イベント listen<string>('download-started', (event) => { console.log('ダウンロード開始:', event.payload); setStatus('downloading'); setProgress(0); }), // 進捗イベント listen<DownloadProgress>('download-progress', (event) => { const { progress, total } = event.payload; setProgress(progress); console.log(`進捗: ${progress}/${total}%`); }), // 完了イベント listen<string>('download-finished', (event) => { console.log('ダウンロード完了:', event.payload); setStatus('completed'); setProgress(100); }), ]); // クリーンアップ: 全てのリスナーを解除 return () => { unlistenPromise.then((unlisteners) => { unlisteners.forEach((unlisten) => unlisten()); }); }; }, []); // 空の依存配列 = マウント時のみ登録 return ( <div> <h2>ダウンロード状態</h2> <div>ステータス: {status}</div> {status === 'downloading' && ( <div className="progress-bar"> <div style={{ width: `${progress}%` }} /> <span>{progress}%</span> </div> )} </div> ); } </syntaxhighlight> <br> ==== カスタムイベントHook ==== イベントリスナー管理をカプセル化したカスタムフックの例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/hooks/useTauriEvent.ts import { useEffect, useRef } from 'react'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; // 汎用イベントHook export function useTauriEvent<T>( eventName: string, callback: (payload: T) => void ) { const callbackRef = useRef(callback); // コールバックを最新に保つ useEffect(() => { callbackRef.current = callback; }, [callback]); useEffect(() => { let unlisten: UnlistenFn | undefined; // イベントリスナーを登録 const setupListener = async () => { unlisten = await listen<T>(eventName, (event) => { callbackRef.current(event.payload); }); }; setupListener(); // クリーンアップ return () => { if (unlisten) { unlisten(); } }; }, [eventName]); // eventNameが変更されたら再登録 } // 複数イベント用フック export function useTauriEvents<T extends Record<string, unknown>>( events: { [K in keyof T]: string }, callbacks: { [K in keyof T]: (payload: T[K]) => void } ) { useEffect(() => { const unlisteners: Promise<UnlistenFn>[] = []; for (const key of Object.keys(events) as (keyof T)[]) { const eventName = events[key]; const callback = callbacks[key]; const unlistenPromise = listen(eventName, (event) => { callback(event.payload as T[typeof key]); }); unlisteners.push(unlistenPromise); } return () => { Promise.all(unlisteners).then((fns) => { fns.forEach((unlisten) => unlisten()); }); }; }, []); } </syntaxhighlight> <br> ==== カスタムイベントフックの使用例 ==== <syntaxhighlight lang="typescript"> // src/components/NotificationHandler.tsx import { useTauriEvent } from '../hooks/useTauriEvent'; interface NotificationPayload { title: string; message: string; type: 'info' | 'warning' | 'error'; } export function NotificationHandler() { const [notifications, setNotifications] = useState<NotificationPayload[]>([]); // 通知イベントを購読 useTauriEvent<NotificationPayload>('notification', (payload) => { setNotifications((prev) => [...prev, payload]); // 5秒後に自動削除 setTimeout(() => { setNotifications((prev) => prev.filter((n) => n !== payload)); }, 5000); }); return ( <div className="notifications"> {notifications.map((notification, index) => ( <div key={index} className={`notification notification-${notification.type}`} > <strong>{notification.title}</strong> <p>{notification.message}</p> </div> ))} </div> ); } </syntaxhighlight> <br><br> == TypeScript型定義 == TauriとReact / TypeScriptの連携において、型安全性を確保することは重要である。<br> <br> ==== コマンド引数の型 ==== Rustコマンドの引数は、<code>serde::Deserialize</code> を定義した構造体で定義できる。<br> <br> <syntaxhighlight lang="rust"> // src-tauri/src/lib.rs use serde::{Deserialize, Serialize}; // コマンド引数の構造体 #[derive(Debug, Deserialize)] pub struct CreateFileArgs { pub path: String, pub content: String, pub overwrite: bool, } // 戻り値の構造体 #[derive(Debug, Serialize)] pub struct CreateFileResult { pub success: bool, pub bytes_written: usize, pub message: String, } #[tauri::command] fn create_file(args: CreateFileArgs) -> Result<CreateFileResult, String> { // ファイル作成処理 Ok(CreateFileResult { success: true, bytes_written: args.content.len(), message: format!("Created: {}", args.path), }) } </syntaxhighlight> <br> TypeScript側でも対応する型を定義する。<br> <br> <syntaxhighlight lang="typescript"> // src/types/commands.ts // コマンド引数の型 (RustのCreateFileArgsに対応) export interface CreateFileArgs { path: string; content: string; overwrite: boolean; } // 戻り値の型 (RustのCreateFileResultに対応) export interface CreateFileResult { success: boolean; bytesWritten: number; // camelCaseに変換される message: string; } // 型安全なinvokeラッパー import { invoke } from '@tauri-apps/api/core'; export async function createFile(args: CreateFileArgs): Promise<CreateFileResult> { return await invoke<CreateFileResult>('create_file', { args }); } </syntaxhighlight> <br> ==== serdeとの互換性 ==== Rustのserdeはデフォルトでsnake_caseを使用し、TypeScriptではキャメルケース (camelCase) が一般的である。<br> <code>#[serde(rename_all = "camelCase")]</code> 属性を使用して、自動的に変換できる。<br> <br> <syntaxhighlight lang="rust"> // Rust側 #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UserSettings { pub theme_color: String, // themeColor に変換 pub font_size: u32, // fontSize に変換 pub auto_save_enabled: bool, // autoSaveEnabled に変換 } </syntaxhighlight> <br> <syntaxhighlight lang="typescript"> // TypeScript側 (camelCase) export interface UserSettings { themeColor: string; fontSize: number; autoSaveEnabled: boolean; } </syntaxhighlight> <br> ==== ジェネリクス活用 ==== 汎用的なコマンド呼び出し関数でジェネリクスを活用する例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/api/tauriClient.ts import { invoke } from '@tauri-apps/api/core'; // コマンド定義の型 type CommandArgs = Record<string, unknown>; // 型安全なAPIクライアント export class TauriClient { // GET的なコマンド (引数なし、戻り値あり) static async query<T>(command: string): Promise<T> { return await invoke<T>(command); } // GET的なコマンド (引数あり、戻り値あり) static async queryWithArgs<T, A extends CommandArgs>( command: string, args: A ): Promise<T> { return await invoke<T>(command, args); } // MUTATION的なコマンド (戻り値なし) static async mutate<A extends CommandArgs>( command: string, args: A ): Promise<void> { await invoke(command, args); } // MUTATION的なコマンド (戻り値あり) static async mutateWithResult<T, A extends CommandArgs>( command: string, args: A ): Promise<T> { return await invoke<T>(command, args); } } // 使用例 interface User { id: number; name: string; } interface SearchArgs { query: string; limit: number; } // クエリ実行 const users = await TauriClient.queryWithResult<User[], SearchArgs>( 'search_users', { query: 'john', limit: 10 } ); </syntaxhighlight> <br><br> == tauri-spectaによる型自動生成 == tauri-spectaライブラリを使用すると、RustコードからTypeScriptの型定義を自動生成できる。<br> <br> ==== セットアップ ==== まず、依存関係を追加する。<br> <br> # spectaとtauri-spectaを追加 cargo add specta cargo add tauri-specta --features javascript,typescript <br> ==== spectaアノテーションの追加 ==== コマンドと型に <code>specta::Type</code> を追加する。<br> <br> <syntaxhighlight lang="rust"> // src-tauri/src/lib.rs use serde::{Deserialize, Serialize}; use specta::Type; use tauri_specta::{ts, collect_types}; // カスタム型にSpectaサポートを追加 #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct User { pub id: u32, pub name: String, pub email: String, pub created_at: String, } #[derive(Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct CreateUserArgs { pub name: String, pub email: String, } // コマンドにspectaアノテーションを追加 #[tauri::command] #[specta::specta] fn get_user(id: u32) -> Result<User, String> { Ok(User { id, name: "John Doe".to_string(), email: "john@example.com".to_string(), created_at: "2025-01-01".to_string(), }) } #[tauri::command] #[specta::specta] async fn create_user(args: CreateUserArgs) -> Result<User, String> { Ok(User { id: 1, name: args.name, email: args.email, created_at: "2025-01-15".to_string(), }) } #[tauri::command] #[specta::specta] fn list_users() -> Vec<User> { vec![ User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string(), created_at: "2025-01-01".to_string(), }, ] } // コマンド登録 #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ get_user, create_user, list_users ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } // デバッグビルド時にTypeScript型を生成 #[cfg(debug_assertions)] fn export_types() { ts::export( collect_types![get_user, create_user, list_users], "../src/bindings.ts" ).unwrap(); } // ビルド時に型をエクスポート #[cfg(debug_assertions)] #[tauri::command] fn __export_types() { export_types(); } </syntaxhighlight> <br> ==== 生成されたTypeScriptバインディング ==== <u>src/bindings.ts</u> ファイルに以下に示すような型定義が生成される。<br> <br> <syntaxhighlight lang="typescript"> // src/bindings.ts (自動生成) // このファイルはRustコードから自動生成されています。手動で編集しないでください。 import { invoke } from "@tauri-apps/api/core"; export interface User { id: number; name: string; email: string; createdAt: string; } export interface CreateUserArgs { name: string; email: string; } export async function getUser(id: number): Promise<User> { return await invoke<User>("get_user", { id }); } export async function createUser(args: CreateUserArgs): Promise<User> { return await invoke<User>("create_user", { args }); } export async function listUsers(): Promise<User[]> { return await invoke<User[]>("list_users"); } </syntaxhighlight> <br> ==== フロントエンドでの使用 ==== 生成されたバインディングをインポートして使用する。<br> <br> <syntaxhighlight lang="typescript"> // src/components/UserManager.tsx import { useState, useEffect } from 'react'; // 自動生成されたバインディングをインポート import { getUser, createUser, listUsers, User } from '../bindings'; export function UserManager() { const [users, setUsers] = useState<User[]>([]); const [loading, setLoading] = useState(false); // ユーザ一覧を取得 const fetchUsers = async () => { setLoading(true); try { const result = await listUsers(); // 型安全! setUsers(result); } catch (error) { console.error('ユーザ取得エラー:', error); } finally { setLoading(false); } }; useEffect(() => { fetchUsers(); }, []); // 新規ユーザ作成 const handleCreateUser = async () => { try { const newUser = await createUser({ name: 'New User', email: 'new@example.com', }); console.log('作成されたユーザ:', newUser); fetchUsers(); // 一覧を更新 } catch (error) { console.error('ユーザ作成エラー:', error); } }; return ( <div> <h2>ユーザ管理</h2> <button onClick={handleCreateUser}>新規ユーザ作成</button> {loading ? ( <div>読み込み中...</div> ) : ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> )} </div> ); } </syntaxhighlight> <br> ==== 型エクスポートの自動化 ==== ビルドプロセスに型エクスポートを統合する。<br> <br> <syntaxhighlight lang="rust"> // src-tauri/build.rs (ビルドスクリプト) fn main() { // ビルド前に型をエクスポート #[cfg(debug_assertions)] { println!("cargo:rerun-if-changed=src/lib.rs"); // build.rsからは直接エクスポートできないため、別の方法 (テストやCI) で型をエクスポートする } } </syntaxhighlight> <br> または、テストとして定義する。<br> <br> <syntaxhighlight lang="rust"> // src-tauri/src/lib.rs #[cfg(test)] mod tests { use super::*; use tauri_specta::ts; use specta::collect_types; #[test] fn export_bindings() { // テスト実行時にTypeScript型をエクスポート ts::export( collect_types![get_user, create_user, list_users], "../src/bindings.ts" ).unwrap(); } } </syntaxhighlight> <br> # テストを実行して型をエクスポート cargo test export_bindings <br><br> == サンプルコード == ==== 状態管理ライブラリとの連携 ==== Zustand等の状態管理ライブラリとTauriを連携させる例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/store/appStore.ts import { create } from 'zustand'; import { invoke } from '@tauri-apps/api/core'; interface AppState { // 状態 user: User | null; settings: Settings | null; loading: boolean; error: string | null; // アクション fetchUser: (id: number) => Promise<void>; updateSettings: (settings: Partial<Settings>) => Promise<void>; reset: () => void; } interface User { id: number; name: string; email: string; } interface Settings { theme: string; language: string; notifications: boolean; } export const useAppStore = create<AppState>((set, get) => ({ // 初期状態 user: null, settings: null, loading: false, error: null, // ユーザ取得 fetchUser: async (id: number) => { set({ loading: true, error: null }); try { const user = await invoke<User>('get_user', { id }); set({ user, loading: false }); } catch (error) { const message = typeof error === 'string' ? error : 'Unknown error'; set({ error: message, loading: false }); } }, // 設定更新 updateSettings: async (newSettings: Partial<Settings>) => { const currentSettings = get().settings; if (!currentSettings) return; const mergedSettings = { ...currentSettings, ...newSettings }; try { await invoke('update_settings', { settings: mergedSettings }); set({ settings: mergedSettings }); } catch (error) { const message = typeof error === 'string' ? error : 'Unknown error'; set({ error: message }); } }, // リセット reset: () => { set({ user: null, settings: null, loading: false, error: null }); }, })); </syntaxhighlight> <br> ==== フォーム処理 ==== React Hook FormとTauriを組み合わせたフォーム処理の例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/components/UserForm.tsx import { useForm } from 'react-hook-form'; import { invoke } from '@tauri-apps/api/core'; interface UserFormData { name: string; email: string; age: number; bio: string; } interface UserFormProps { onSuccess?: (user: UserFormData) => void; } export function UserForm({ onSuccess }: UserFormProps) { const { register, handleSubmit, formState: { errors, isSubmitting }, setError, } = useForm<UserFormData>(); // フォーム送信処理 const onSubmit = async (data: UserFormData) => { try { // Rustコマンドを呼び出し const result = await invoke<{ success: boolean; id: number }>( 'create_user', { user: data } ); if (result.success) { onSuccess?.(data); } } catch (error) { // サーバー側バリデーションエラー const message = typeof error === 'string' ? error : 'Unknown error'; setError('root', { message }); } }; return ( <form onSubmit={handleSubmit(onSubmit)}> {/* 名前フィールド */} <div> <label>名前</label> <input {...register('name', { required: '名前は必須です', minLength: { value: 2, message: '2文字以上で入力してください' }, })} /> {errors.name && <span className="error">{errors.name.message}</span>} </div> {/* メールフィールド */} <div> <label>メール</label> <input type="email" {...register('email', { required: 'メールは必須です', pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: '有効なメールアドレスを入力してください', }, })} /> {errors.email && <span className="error">{errors.email.message}</span>} </div> {/* 年齢フィールド */} <div> <label>年齢</label> <input type="number" {...register('age', { required: '年齢は必須です', min: { value: 0, message: '0以上の値を入力してください' }, max: { value: 150, message: '有効な年齢を入力してください' }, })} /> {errors.age && <span className="error">{errors.age.message}</span>} </div> {/* 送信ボタン */} <button type="submit" disabled={isSubmitting}> {isSubmitting ? '送信中...' : '登録'} </button> {/* 全体エラー */} {errors.root && <div className="error">{errors.root.message}</div>} </form> ); } </syntaxhighlight> <br> ==== ファイル操作 ==== ファイル選択とTauriバックエンドでのファイル処理の例を以下に示す。<br> <br> <syntaxhighlight lang="typescript"> // src/components/FileProcessor.tsx import { useState } from 'react'; import { open } from '@tauri-apps/plugin-dialog'; import { invoke } from '@tauri-apps/api/core'; interface ProcessResult { success: boolean; message: string; data?: unknown; } export function FileProcessor() { const [selectedPath, setSelectedPath] = useState<string | null>(null); const [processing, setProcessing] = useState(false); const [result, setResult] = useState<ProcessResult | null>(null); // ファイル選択ダイアログを開く const handleSelectFile = async () => { const selected = await open({ multiple: false, filters: [ { name: 'Text Files', extensions: ['txt', 'md', 'json'] }, { name: 'All Files', extensions: ['*'] }, ], }); if (selected) { setSelectedPath(selected as string); setResult(null); } }; // ファイル処理を実行 const handleProcess = async () => { if (!selectedPath) return; setProcessing(true); setResult(null); try { const processResult = await invoke<ProcessResult>('process_file', { path: selectedPath, }); setResult(processResult); } catch (error) { const message = typeof error === 'string' ? error : 'Unknown error'; setResult({ success: false, message }); } finally { setProcessing(false); } }; return ( <div className="file-processor"> <h2>ファイル処理</h2> {/* ファイル選択 */} <button onClick={handleSelectFile}> ファイルを選択 </button> {/* 選択されたファイル */} {selectedPath && ( <div className="selected-file"> 選択: {selectedPath} </div> )} {/* 処理実行 */} <button onClick={handleProcess} disabled={!selectedPath || processing} > {processing ? '処理中...' : '処理実行'} </button> {/* 結果表示 */} {result && ( <div className={`result ${result.success ? 'success' : 'error'}`}> {result.message} {result.data && ( <pre>{JSON.stringify(result.data, null, 2)}</pre> )} </div> )} </div> ); } </syntaxhighlight> <br><br> == 参考リンク == * [https://tauri.app/develop/calling-rust/ Tauri公式ドキュメント - Calling Rust from JavaScript] * [https://tauri.app/develop/calling-frontend/ Tauri公式ドキュメント - Calling JavaScript from Rust] * [https://github.com/oscartbeaumont/tauri-specta tauri-specta (GitHub)] * [https://tauri.app/reference/javascript-api/ Tauri JavaScript API Reference] <br><br> {{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Tauri,React,TypeScript,Hook,IPC,invoke,useEffect,Rust,Desktop App,SUSE,Linux |description={{PAGENAME}} - TauriとReact/TypeScriptの連携パターンとベストプラクティス | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux |image=/resources/assets/MochiuLogo_Single_Blue.png }} __FORCETOC__ [[カテゴリ:Rust]]
Tauriの基礎 - React連携パターン
に戻る。
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
Collapse