ページの作成:「== 概要 == Tauriの状態管理は、Rustバックエンドとフロントエンド (WebView) 間でアプリケーション全体の状態を共有・管理するための仕組みである。<br> <br> 従来のWebアプリケーションでは、フロントエンド側で状態を管理するのが一般的だが、TauriアプリケーションではRustバックエンド側で状態を保持し、必要に応じてフロントエンドから参照・更新で…」 |
編集の要約なし |
||
| 12行目: | 12行目: | ||
<br> | <br> | ||
フロントエンド (React + TypeScript) からは、invoke()関数を通じてRustコマンドを呼び出して、状態の取得・更新を行う。<br> | フロントエンド (React + TypeScript) からは、invoke()関数を通じてRustコマンドを呼び出して、状態の取得・更新を行う。<br> | ||
<br> | <br><br> | ||
== 状態管理の基本概念 == | == 状態管理の基本概念 == | ||
| 332行目: | 332行目: | ||
<br> | <br> | ||
<syntaxhighlight lang="rust"> | <syntaxhighlight lang="rust"> | ||
use tauri::{ | use tauri::{AppHandle, Manager}; | ||
use std::sync::Mutex; | use std::sync::Mutex; | ||
use std::time::Duration; | |||
fn start_background_task(app: AppHandle) { | |||
// 非同期タスクを開始 | |||
// moveキーワードでappの所有権をタスクに移動 | |||
tokio::spawn(async move { | |||
// 無限ループで定期的な処理を実行 | |||
loop { | |||
// 60秒待機 | |||
tokio::time::sleep(Duration::from_secs(60)).await; | |||
// バックグラウンドから状態にアクセス | |||
// AppHandleを通じて登録済みの状態を取得 | |||
let state = app.state::<Mutex<AppState>>(); | |||
// Mutexをロックして可変参照を取得 | |||
let mut state = state.lock().unwrap(); | |||
// 最終同期時刻を更新 | |||
state.last_sync = Some(chrono::Utc::now()); | |||
// 定期的な同期処理等 | |||
println!("Background sync completed"); | |||
} | |||
}); | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| 421行目: | 423行目: | ||
<syntaxhighlight lang="typescript"> | <syntaxhighlight lang="typescript"> | ||
// src/hooks/useCounter.ts | // src/hooks/useCounter.ts | ||
// カウンタ状態を管理するカスタムフック | |||
import { useState, useEffect, useCallback } from 'react'; | import { useState, useEffect, useCallback } from 'react'; | ||
import { invoke } from '@tauri-apps/api/core'; | import { invoke } from '@tauri-apps/api/core'; | ||
export function useCounter() { | export function useCounter() { | ||
const [count, setCount] = useState<number>(0); | const [count, setCount] = useState<number>(0); // カウンタの現在値 | ||
const [loading, setLoading] = useState<boolean>(false); | const [loading, setLoading] = useState<boolean>(false); // 通信中かどうかのフラグ | ||
const [error, setError] = useState<string | null>(null); | const [error, setError] = useState<string | null>(null); // エラーメッセージ (エラーがない場合はnull) | ||
// | // コンポーネントマウント時に初期値を取得 | ||
useEffect(() => { | useEffect(() => { | ||
fetchCounter(); | fetchCounter(); | ||
}, []); | }, []); | ||
// Rustバックエンドからカウンター値を取得する関数 | |||
const fetchCounter = useCallback(async () => { | const fetchCounter = useCallback(async () => { | ||
setLoading(true); | setLoading(true); // ロード開始 | ||
setError(null); | setError(null); // エラーをクリア | ||
try { | try { | ||
// Rustコマンド "get_counter" を呼び出し | |||
const value = await invoke<number>('get_counter'); | const value = await invoke<number>('get_counter'); | ||
setCount(value); | setCount(value); // 取得した値をセット | ||
} | } | ||
catch (err) { | catch (err) { | ||
setError(String(err)); | setError(String(err)); // エラーをセット | ||
} | } | ||
finally { | finally { | ||
setLoading(false); | setLoading(false); // ロード終了 | ||
} | } | ||
}, []); | }, []); | ||
// カウンタをインクリメントする関数 | |||
const increment = useCallback(async () => { | const increment = useCallback(async () => { | ||
setLoading(true); | setLoading(true); | ||
setError(null); | setError(null); | ||
try { | try { | ||
// Rustコマンド "increment_counter" を呼び出し | |||
// 戻り値として新しいカウンター値を取得 | |||
const newValue = await invoke<number>('increment_counter'); | const newValue = await invoke<number>('increment_counter'); | ||
setCount(newValue); | setCount(newValue); // 新しい値をセット | ||
return newValue; | return newValue; // 呼び出し元にも値を返す | ||
} | } | ||
catch (err) { | catch (err) { | ||
setError(String(err)); | setError(String(err)); | ||
throw err; | throw err; // エラーを呼び出し元に伝播 | ||
} | } | ||
finally { | finally { | ||
| 466行目: | 474行目: | ||
}, []); | }, []); | ||
// フックの使用者に公開する値と関数 | |||
return { | return { | ||
count, | count, // 現在のカウンタ値 | ||
loading, | loading, // 通信中かどうか | ||
error, | error, // エラーメッセージ | ||
fetchCounter, | fetchCounter, // 値を再取得する関数 | ||
increment, | increment, // インクリメント関数 | ||
}; | }; | ||
} | } | ||
| 510行目: | 519行目: | ||
== React + TypeScriptでの実装例 == | == React + TypeScriptでの実装例 == | ||
==== | ==== 状態管理のパターン ==== | ||
より複雑なアプリケーション向けに、Context APIと組み合わせた状態管理パターンを示す。<br> | より複雑なアプリケーション向けに、Context APIと組み合わせた状態管理パターンを示す。<br> | ||
<br> | <br> | ||
| 518行目: | 527行目: | ||
import { invoke } from '@tauri-apps/api/core'; | import { invoke } from '@tauri-apps/api/core'; | ||
// | // ===== 型定義セクション ===== | ||
// アプリケーション全体の状態を表す型 | |||
interface AppState { | interface AppState { | ||
user: User | null; | user: User | null; // 現在のユーザ (未ログインならnull) | ||
settings: Settings; | settings: Settings; // アプリケーション設定 | ||
notifications: Notification[]; | notifications: Notification[]; // 通知リスト | ||
} | } | ||
// ユーザ情報の型 | |||
interface User { | interface User { | ||
id: string; | id: string; | ||
| 531行目: | 543行目: | ||
} | } | ||
// アプリケーション設定の型 | |||
interface Settings { | interface Settings { | ||
theme: 'light' | 'dark'; | theme: 'light' | 'dark'; // テーマ (light または dark) | ||
language: string; | language: string; // 言語設定 | ||
notifications: boolean; | notifications: boolean; // 通知の有効/無効 | ||
} | } | ||
// 通知メッセージの型 | |||
interface Notification { | interface Notification { | ||
id: string; | id: string; // 通知の一意識別子 | ||
message: string; | message: string; // 通知メッセージ本文 | ||
timestamp: Date; | timestamp: Date; // タイムスタンプ | ||
} | } | ||
// | // コンテキストが提供する値の型 | ||
interface AppStateContextType { | interface AppStateContextType { | ||
state: AppState; | state: AppState; // 現在の状態 | ||
loading: boolean; | loading: boolean; // ロード中かどうか | ||
error: string | null; | error: string | null; // エラーメッセージ | ||
updateUser: (user: User) => Promise<void>; | updateUser: (user: User) => Promise<void>; // ユーザ更新関数 | ||
updateSettings: (settings: Partial<Settings>) => Promise<void>; | updateSettings: (settings: Partial<Settings>) => Promise<void>; // 設定更新関数 | ||
addNotification: (message: string) => Promise<void>; | addNotification: (message: string) => Promise<void>; // 通知追加関数 | ||
refreshState: () => Promise<void>; | refreshState: () => Promise<void>; // 状態再取得関数 | ||
} | } | ||
// | // Reactコンテキストを作成 (初期値はnull) | ||
const AppStateContext = createContext<AppStateContextType | null>(null); | const AppStateContext = createContext<AppStateContextType | null>(null); | ||
// プロバイダーコンポーネント | // ===== プロバイダーコンポーネント ===== | ||
// アプリケーション全体をラップして状態を提供する | |||
export function AppStateProvider({ children }: { children: ReactNode }) { | export function AppStateProvider({ children }: { children: ReactNode }) { | ||
// アプリケーション状態を管理 | |||
const [state, setState] = useState<AppState>({ | const [state, setState] = useState<AppState>({ | ||
user: null, | user: null, | ||
| 564行目: | 580行目: | ||
notifications: [], | notifications: [], | ||
}); | }); | ||
// ロード状態とエラー状態を管理 | |||
const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||
const [error, setError] = useState<string | null>(null); | const [error, setError] = useState<string | null>(null); | ||
// | // Rustバックエンドから状態を取得する関数 | ||
const refreshState = useCallback(async () => { | const refreshState = useCallback(async () => { | ||
setLoading(true); | setLoading(true); // ロード開始 | ||
setError(null); | setError(null); // エラーをクリア | ||
try { | try { | ||
// Rustコマンドを呼び出して状態を取得 | |||
const appState = await invoke<AppState>('get_full_state'); | const appState = await invoke<AppState>('get_full_state'); | ||
setState(appState); | setState(appState); // 取得した状態をセット | ||
} | } | ||
catch (err) { | catch (err) { | ||
setError(String(err)); | setError(String(err)); // エラーをセット | ||
} | } | ||
finally { | finally { | ||
setLoading(false); | setLoading(false); // ロード終了 | ||
} | } | ||
}, []); | }, []); | ||
// | // コンポーネントマウント時に初期状態を取得 | ||
useEffect(() => { | useEffect(() => { | ||
refreshState(); | refreshState(); | ||
}, [refreshState]); | }, [refreshState]); | ||
// | // ユーザ情報を更新する関数 | ||
const updateUser = useCallback(async (user: User) => { | const updateUser = useCallback(async (user: User) => { | ||
setLoading(true); | setLoading(true); | ||
setError(null); | setError(null); | ||
try { | try { | ||
// Rustコマンドでユーザを更新 | |||
await invoke('update_user', { user }); | await invoke('update_user', { user }); | ||
// ローカル状態も更新 | |||
setState(prev => ({ ...prev, user })); | setState(prev => ({ ...prev, user })); | ||
} | } | ||
catch (err) { | catch (err) { | ||
setError(String(err)); | setError(String(err)); | ||
throw err; | throw err; // 呼び出し元にエラーを伝播 | ||
} | } | ||
finally { | finally { | ||
| 605行目: | 626行目: | ||
}, []); | }, []); | ||
// | // 設定を部分的に更新する関数 | ||
const updateSettings = useCallback(async (settings: Partial<Settings>) => { | const updateSettings = useCallback(async (settings: Partial<Settings>) => { | ||
setLoading(true); | setLoading(true); | ||
setError(null); | setError(null); | ||
try { | try { | ||
// Rustコマンドで設定を更新し、新しい設定を取得 | |||
const newSettings = await invoke<Settings>('update_settings', { settings }); | const newSettings = await invoke<Settings>('update_settings', { settings }); | ||
// ローカル状態を更新 | |||
setState(prev => ({ ...prev, settings: newSettings })); | setState(prev => ({ ...prev, settings: newSettings })); | ||
} | } | ||
| 622行目: | 645行目: | ||
}, []); | }, []); | ||
// | // 新しい通知を追加する関数 | ||
const addNotification = useCallback(async (message: string) => { | const addNotification = useCallback(async (message: string) => { | ||
try { | try { | ||
// Rustコマンドで通知を作成 | |||
const notification = await invoke<Notification>('add_notification', { message }); | const notification = await invoke<Notification>('add_notification', { message }); | ||
// 通知リストに追加 | |||
setState(prev => ({ | setState(prev => ({ | ||
...prev, | ...prev, | ||
| 632行目: | 657行目: | ||
} | } | ||
catch (err) { | catch (err) { | ||
// 通知追加の失敗はコンソールにログ出力のみ | |||
console.error('Failed to add notification:', err); | console.error('Failed to add notification:', err); | ||
} | } | ||
}, []); | }, []); | ||
// コンテキストプロバイダを返す | |||
return ( | return ( | ||
<AppStateContext.Provider | <AppStateContext.Provider | ||
| 653行目: | 680行目: | ||
} | } | ||
// カスタムフック | // ===== カスタムフック ===== | ||
// 他のコンポーネントから状態にアクセスするためのフック | |||
export function useAppState() { | export function useAppState() { | ||
const context = useContext(AppStateContext); | const context = useContext(AppStateContext); | ||
// プロバイダ外で使用された場合はエラーをスロー | |||
if (!context) { | if (!context) { | ||
throw new Error('useAppState must be used within AppStateProvider'); | throw new Error('useAppState must be used within AppStateProvider'); | ||
| 670行目: | 699行目: | ||
use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||
// ユーザ情報を表す構造体 | |||
#[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
struct User { | struct User { | ||
| 677行目: | 707行目: | ||
} | } | ||
// アプリケーション設定を表す構造体 | |||
#[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
struct Settings { | struct Settings { | ||
theme: String, | theme: String, // テーマ ("light" または "dark") | ||
language: String, | language: String, // 言語設定 | ||
notifications: bool, | notifications: bool, // 通知の有効/無効 | ||
} | } | ||
// 通知メッセージを表す構造体 | |||
#[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
struct Notification { | struct Notification { | ||
id: String, | id: String, // 通知の一意識別子 | ||
message: String, | message: String, // 通知メッセージ本文 | ||
timestamp: String, | timestamp: String, // タイムスタンプ (ISO 8601形式) | ||
} | } | ||
// アプリケーション全体の状態を管理する構造体 | |||
#[derive(Debug, Default)] | #[derive(Debug, Default)] | ||
struct AppState { | struct AppState { | ||
user: Option<User>, | user: Option<User>, // 現在のユーザー (未ログインならNone) | ||
settings: Settings, | settings: Settings, // アプリケーション設定 | ||
notifications: Vec<Notification>, | notifications: Vec<Notification>, // 通知リスト | ||
} | } | ||
// Settings構造体のデフォルト値を定義 | |||
impl Default for Settings { | impl Default for Settings { | ||
fn default() -> Self { | fn default() -> Self { | ||
| 708行目: | 742行目: | ||
} | } | ||
// アプリケーション状態全体を取得するコマンド | |||
#[tauri::command] | #[tauri::command] | ||
fn get_full_state(state: State<'_, Mutex<AppState>>) -> serde_json::Value { | fn get_full_state(state: State<'_, Mutex<AppState>>) -> serde_json::Value { | ||
let state = state.lock().unwrap(); | let state = state.lock().unwrap(); // Mutexをロック | ||
// 状態をJSONとして返す | |||
serde_json::json!({ | serde_json::json!({ | ||
"user": state.user, | "user": state.user, | ||
| 718行目: | 754行目: | ||
} | } | ||
// ユーザ情報を更新するコマンド | |||
#[tauri::command] | #[tauri::command] | ||
fn update_user(state: State<'_, Mutex<AppState>>, user: User) { | fn update_user(state: State<'_, Mutex<AppState>>, user: User) { | ||
let mut state = state.lock().unwrap(); | let mut state = state.lock().unwrap(); // Mutexをロック (可変参照) | ||
state.user = Some(user); | state.user = Some(user); // ユーザ情報を設定 | ||
} | } | ||
// 設定を部分的に更新するコマンド | |||
#[tauri::command] | #[tauri::command] | ||
fn update_settings( | fn update_settings( | ||
state: State<'_, Mutex<AppState>>, | state: State<'_, Mutex<AppState>>, | ||
settings: PartialSettings, | settings: PartialSettings, // 部分的な設定データ | ||
) -> Settings { | ) -> Settings { | ||
let mut state = state.lock().unwrap(); | let mut state = state.lock().unwrap(); | ||
// 各フィールドがSomeの場合のみ更新 | |||
if let Some(theme) = settings.theme { | if let Some(theme) = settings.theme { | ||
state.settings.theme = theme; | state.settings.theme = theme; | ||
| 739行目: | 778行目: | ||
state.settings.notifications = notifications; | state.settings.notifications = notifications; | ||
} | } | ||
state.settings.clone() | state.settings.clone() // 更新後の設定を返す | ||
} | } | ||
// 部分的な設定データ (全フィールドがOptional) | |||
#[derive(Deserialize)] | #[derive(Deserialize)] | ||
struct PartialSettings { | struct PartialSettings { | ||
| 749行目: | 789行目: | ||
} | } | ||
// 新しい通知を追加するコマンド | |||
#[tauri::command] | #[tauri::command] | ||
fn add_notification( | fn add_notification( | ||
state: State<'_, Mutex<AppState>>, | state: State<'_, Mutex<AppState>>, | ||
message: String, | message: String, // 通知メッセージ | ||
) -> Notification { | ) -> Notification { | ||
let mut state = state.lock().unwrap(); | let mut state = state.lock().unwrap(); | ||
// 新しい通知を作成 | |||
let notification = Notification { | let notification = Notification { | ||
id: uuid::Uuid::new_v4().to_string(), | id: uuid::Uuid::new_v4().to_string(), // UUIDを生成 | ||
message, | message, | ||
timestamp: chrono::Utc::now().to_rfc3339(), | timestamp: chrono::Utc::now().to_rfc3339(), // 現在時刻をISO形式で設定 | ||
}; | }; | ||
state.notifications.push(notification.clone()); | state.notifications.push(notification.clone()); // 通知リストに追加 | ||
notification | notification // 作成した通知を返す | ||
} | } | ||
// アプリケーションのエントリーポイント | |||
#[cfg_attr(mobile, tauri::mobile_entry_point)] | #[cfg_attr(mobile, tauri::mobile_entry_point)] | ||
pub fn run() { | pub fn run() { | ||
Builder::default() | Builder::default() | ||
.manage(Mutex::new(AppState::default())) | .manage(Mutex::new(AppState::default())) // 状態を登録 | ||
.invoke_handler(tauri::generate_handler![ | .invoke_handler(tauri::generate_handler![ // コマンドを登録 | ||
get_full_state, | get_full_state, | ||
update_user, | update_user, | ||