Apollo Client の Reactive Variables - GraphQL でグローバル状態管理

モダンな Web 開発において、GraphQL を活用したアプリケーションが増える中で、クライアント側の状態管理は重要な課題となっています。Apollo Client が提供する Reactive Variables は、この課題に対する革新的な解決策です。
従来の Redux や Context API といった複雑な状態管理ライブラリに代わって、GraphQL エコシステム内で統一された状態管理を実現できます。Apollo Client とシームレスに統合された Reactive Variables を使うことで、開発者はより効率的で保守性の高いアプリケーションを構築できるでしょう。
背景
GraphQL とクライアント状態管理の課題
GraphQL を採用した開発プロジェクトでは、サーバーから取得したデータとクライアント固有の状態を適切に管理する必要があります。
typescript// 従来のアプローチでは複数の状態管理が混在
const App = () => {
// Apollo Clientによるサーバー状態
const { data } = useQuery(GET_USERS);
// Context APIによるクライアント状態
const { theme, setTheme } = useContext(ThemeContext);
// useStateによるローカル状態
const [isModalOpen, setIsModalOpen] = useState(false);
};
このような状況では、データの流れが複雑になり、デバッグや保守が困難になってしまいます。GraphQL で管理されるサーバー状態と、クライアント独自の状態が異なる仕組みで管理されることで、開発者の認知負荷が増大します。
現代の Web アプリケーションでは、認証状態、UI 設定、フィルター条件など、多様なクライアント状態を適切に管理する必要があります。これらの状態が GraphQL キャッシュと統合されていないと、データの整合性やパフォーマンスに問題が生じる可能性があるのです。
従来の状態管理ライブラリとの比較
Redux や Zustand、Context API などの従来の状態管理ライブラリには、それぞれ特徴と課題があります。
ライブラリ | メリット | デメリット |
---|---|---|
Redux | 予測可能な状態変更、優れた DevTools | 複雑な設定、大量のボイラープレート |
Context API | React に標準搭載、シンプルな設定 | パフォーマンス問題、プロバイダー地獄 |
Zustand | 軽量、TypeScript 親和性 | GraphQL との統合が別途必要 |
typescript// Reduxの複雑な設定例
const store = configureStore({
reducer: {
auth: authSlice.reducer,
ui: uiSlice.reducer,
apollo: apolloSlice.reducer, // Apollo Clientとの同期が必要
},
});
// Context APIのプロバイダー地獄
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<ApolloProvider client={client}>
{/* 実際のアプリケーション */}
</ApolloProvider>
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
これらのライブラリを使用する場合、Apollo Client との連携には追加の実装が必要になります。また、異なる状態管理パターンが混在することで、コードベースの一貫性が損なわれる可能性もあります。
Apollo Client における状態管理の進化
Apollo Client は、GraphQL クライアントとしての機能だけでなく、包括的な状態管理ソリューションとして進化してきました。
typescript// Apollo Client 2.x時代のローカル状態管理
const GET_CART_ITEMS = gql`
query GetCartItems {
cartItems @client {
id
name
price
}
}
`;
// Apollo Client 3.0でのReactive Variables
const cartItemsVar = makeVar([]);
const GET_CART_ITEMS = gql`
query GetCartItems {
cartItems @client
}
`;
Apollo Client 3.0 で導入された Reactive Variables は、この進化の集大成です。GraphQL スキーマとの統合、型安全性の向上、そして直感的な API によって、開発者体験が大幅に改善されました。
バージョン 2.x 時代のローカルリゾルバーは設定が複雑でしたが、Reactive Variables では最小限のコードで状態管理を実現できます。
課題
Redux や Context API の複雑さ
従来の状態管理ライブラリは、規模の拡大とともに複雑さが増大する傾向があります。
typescript// Reduxでの認証状態管理の複雑さ
interface AuthState {
user: User | null;
isLoading: boolean;
error: string | null;
}
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
isLoading: false,
error: null,
} as AuthState,
reducers: {
loginStart: (state) => {
state.isLoading = true;
state.error = null;
},
loginSuccess: (state, action) => {
state.isLoading = false;
state.user = action.payload;
},
loginFailure: (state, action) => {
state.isLoading = false;
state.error = action.payload;
},
},
});
// アクションクリエーター、セレクター、ミドルウェアなども必要
このような複雑さは、以下の問題を引き起こします。開発者が新しい機能を追加する際に、多くのファイルを編集する必要があり、ミスが発生しやすくなります。また、テストの複雑さも増大し、品質保証が困難になってしまうのです。
Context API を使用した場合も、プロバイダーの階層が深くなり、パフォーマンスの問題が生じることがあります。
GraphQL キャッシュとローカル状態の分離問題
Apollo Client で GraphQL データを管理しつつ、別の仕組みでローカル状態を管理する場合、データの整合性に問題が生じる可能性があります。
以下の図は、分離された状態管理によるデータフローの複雑さを示しています。
mermaidflowchart TD
Component[コンポーネント] --> Apollo[Apollo Client キャッシュ]
Component --> Redux[Redux Store]
Component --> Context[Context API]
Apollo --> Server[(GraphQL Server)]
Redux --> LocalStorage[Local Storage]
Context --> SessionStorage[Session Storage]
style Component fill:#e1f5fe
style Apollo fill:#f3e5f5
style Redux fill:#fff3e0
style Context fill:#e8f5e8
この分離により、以下の問題が発生します:
- データの同期問題: サーバーデータの更新時に、関連するローカル状態が同期されない
- デバッグの困難さ: 複数の状態管理システムを跨いだデータ追跡が複雑
- パフォーマンスの劣化: 不要な再レンダリングや重複する状態更新
typescript// データ同期の問題例
const UserProfile = () => {
// Apollo Clientから取得するユーザーデータ
const { data: userData } = useQuery(GET_USER);
// Reduxで管理するUI状態
const theme = useSelector((state) => state.ui.theme);
// ユーザー情報が更新された時、テーマ設定も同期する必要がある
useEffect(() => {
if (userData?.user?.preferences?.theme) {
dispatch(setTheme(userData.user.preferences.theme));
}
}, [userData]); // 手動で同期が必要
};
状態同期とパフォーマンスの課題
複数の状態管理システムを使用する場合、状態の同期とパフォーマンスが大きな課題となります。
typescript// パフォーマンス問題の例
const Dashboard = () => {
// 複数の状態を監視
const apolloData = useQuery(GET_DASHBOARD_DATA);
const reduxState = useSelector(selectDashboardState);
const [localState, setLocalState] = useState({});
// 状態変更時の副作用が複雑に絡み合う
useEffect(() => {
// Apollo データの変更時
if (apolloData.data) {
setLocalState((prev) => ({ ...prev, loaded: true }));
dispatch(updateDashboardMetrics(apolloData.data));
}
}, [apolloData.data]);
useEffect(() => {
// Redux状態の変更時
if (reduxState.filterChanged) {
apolloData.refetch();
}
}, [reduxState.filterChanged]);
};
このような複雑な状態同期は、メモリリークや不要な再計算を引き起こし、アプリケーションのパフォーマンスを著しく低下させる可能性があります。
解決策
Reactive Variables の仕組み
Apollo Client の Reactive Variables は、リアクティブプログラミングの概念を取り入れた革新的な状態管理ソリューションです。
typescriptimport { makeVar, useReactiveVar } from '@apollo/client';
// Reactive Variableの作成
const themeVar = makeVar<'light' | 'dark'>('light');
const userPreferencesVar = makeVar({
language: 'ja',
notifications: true,
autoSave: false,
});
Reactive Variables の核となる仕組みは、値の変更を自動的に検知し、関連するコンポーネントに変更を伝播することです。この仕組みにより、開発者は手動での状態同期を気にすることなく、宣言的に UI を構築できます。
以下の図は、Reactive Variables の動作フローを示しています:
mermaidflowchart LR
makeVar[makeVar で変数作成] --> Component[コンポーネント]
Component --> useReactiveVar[useReactiveVar フック]
useReactiveVar --> Subscribe[変更の監視]
Update[値の更新] --> Notify[変更通知]
Notify --> Rerender[自動再レンダリング]
style makeVar fill:#e3f2fd
style Component fill:#f3e5f5
style Update fill:#fff3e0
style Rerender fill:#e8f5e8
この仕組みにより、状態の変更が発生した際に、関連するすべてのコンポーネントが自動的に更新されます。
Apollo Client との統合メリット
Reactive Variables が Apollo Client と統合されることで、従来の状態管理では実現困難だった多くのメリットが得られます。
typescript// Apollo Clientとの完全な統合
import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Reactive Variableをクエリフィールドとして使用
currentTheme: {
read() {
return themeVar();
},
},
userPreferences: {
read() {
return userPreferencesVar();
},
},
},
},
},
});
この統合により、以下のメリットが実現されます:
- 統一されたクエリ体験: GraphQL クエリと同じ方法でローカル状態にアクセス
- Apollo DevTools でのデバッグ: サーバーデータとローカル状態を同じツールで確認
- キャッシュとの協調: Apollo Client のキャッシングメカニズムとの完全な統合
typescript// GraphQLクエリでローカル状態とサーバー状態を同時取得
const GET_DASHBOARD_DATA = gql`
query GetDashboardData {
# サーバーから取得するデータ
user {
id
name
email
}
# Reactive Variableから取得するローカル状態
currentTheme @client
userPreferences @client
}
`;
const Dashboard = () => {
const { data, loading, error } = useQuery(
GET_DASHBOARD_DATA
);
// サーバーデータとローカル状態を統一的に扱える
return (
<div className={`dashboard ${data?.currentTheme}`}>
<h1>Welcome, {data?.user?.name}</h1>
<Settings preferences={data?.userPreferences} />
</div>
);
};
宣言的な状態管理アプローチ
Reactive Variables を使用することで、命令的な状態管理から宣言的な状態管理へとパラダイムが変わります。
typescript// 従来の命令的アプローチ
const ThemeToggle = () => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
// 他のコンポーネントに通知する必要がある
window.dispatchEvent(
new CustomEvent('themeChanged', { detail: newTheme })
);
};
return (
<button onClick={toggleTheme}>Current: {theme}</button>
);
};
typescript// Reactive Variablesを使った宣言的アプローチ
const themeVar = makeVar('light');
const ThemeToggle = () => {
const theme = useReactiveVar(themeVar);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
themeVar(newTheme); // これだけで全体に変更が伝播される
};
return (
<button onClick={toggleTheme}>Current: {theme}</button>
);
};
宣言的なアプローチでは、**「何を変更するか」に焦点を当て、「どのように変更を伝播するか」**はフレームワークに委ねます。これにより、コードがより簡潔になり、バグの発生確率も大幅に減少します。
具体例
基本的な Reactive Variables の実装
まず、最もシンプルな Reactive Variable の実装から始めましょう。
typescriptimport { makeVar } from '@apollo/client';
// 基本的なReactive Variableの作成
const counterVar = makeVar(0);
const messageVar = makeVar('Hello, Apollo!');
const isLoadingVar = makeVar(false);
これらの変数は、値の読み書きが可能な関数として動作します。
typescript// 値の読み取り
console.log(counterVar()); // 0
console.log(messageVar()); // "Hello, Apollo!"
// 値の更新
counterVar(10);
messageVar('Updated message');
isLoadingVar(true);
実際の React コンポーネントでの使用例を見てみましょう。
typescriptimport React from 'react';
import { useReactiveVar } from '@apollo/client';
const Counter = () => {
const count = useReactiveVar(counterVar);
const isLoading = useReactiveVar(isLoadingVar);
const increment = () => {
counterVar(count + 1);
};
const decrement = () => {
counterVar(count - 1);
};
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
export default Counter;
この実装では、useReactiveVar
フックを使って変数の値を監視しています。変数の値が変更されると、コンポーネントが自動的に再レンダリングされます。
useReactiveVar フックの活用
useReactiveVar
フックは、Reactive Variables をコンポーネント内で使用するための主要なインターフェースです。
typescriptimport { makeVar, useReactiveVar } from '@apollo/client';
// ユーザー設定用のReactive Variable
interface UserSettings {
theme: 'light' | 'dark';
language: 'en' | 'ja';
fontSize: number;
}
const userSettingsVar = makeVar<UserSettings>({
theme: 'light',
language: 'ja',
fontSize: 14,
});
設定を管理するコンポーネントの実装例です。
typescriptconst SettingsPanel = () => {
const settings = useReactiveVar(userSettingsVar);
const updateTheme = (theme: 'light' | 'dark') => {
userSettingsVar({
...settings,
theme,
});
};
const updateLanguage = (language: 'en' | 'ja') => {
userSettingsVar({
...settings,
language,
});
};
const updateFontSize = (fontSize: number) => {
userSettingsVar({
...settings,
fontSize,
});
};
return (
<div
className={`settings-panel theme-${settings.theme}`}
>
<h3>設定パネル</h3>
<div>
<label>テーマ:</label>
<select
value={settings.theme}
onChange={(e) =>
updateTheme(e.target.value as 'light' | 'dark')
}
>
<option value='light'>ライト</option>
<option value='dark'>ダーク</option>
</select>
</div>
<div>
<label>言語:</label>
<select
value={settings.language}
onChange={(e) =>
updateLanguage(e.target.value as 'en' | 'ja')
}
>
<option value='ja'>日本語</option>
<option value='en'>English</option>
</select>
</div>
<div>
<label>フォントサイズ: {settings.fontSize}px</label>
<input
type='range'
min='12'
max='24'
value={settings.fontSize}
onChange={(e) =>
updateFontSize(Number(e.target.value))
}
/>
</div>
</div>
);
};
この設定変更は、他のコンポーネントからも即座に反映されます。
typescriptconst Header = () => {
const settings = useReactiveVar(userSettingsVar);
return (
<header
className={`header theme-${settings.theme}`}
style={{ fontSize: `${settings.fontSize}px` }}
>
<h1>
{settings.language === 'ja'
? 'マイアプリ'
: 'My App'}
</h1>
</header>
);
};
複雑な状態管理パターンの実装
より高度な使用例として、ショッピングカートの状態管理を実装してみましょう。
typescript// 商品の型定義
interface Product {
id: string;
name: string;
price: number;
image: string;
}
// カートアイテムの型定義
interface CartItem extends Product {
quantity: number;
}
// カート状態の型定義
interface CartState {
items: CartItem[];
isOpen: boolean;
total: number;
}
カート用の Reactive Variable とヘルパー関数を作成します。
typescriptconst cartVar = makeVar<CartState>({
items: [],
isOpen: false,
total: 0,
});
// カート操作のヘルパー関数
const cartHelpers = {
addItem: (product: Product) => {
const currentCart = cartVar();
const existingItem = currentCart.items.find(
(item) => item.id === product.id
);
let newItems: CartItem[];
if (existingItem) {
newItems = currentCart.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
newItems = [
...currentCart.items,
{ ...product, quantity: 1 },
];
}
const newTotal = newItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
cartVar({
...currentCart,
items: newItems,
total: newTotal,
});
},
removeItem: (productId: string) => {
const currentCart = cartVar();
const newItems = currentCart.items.filter(
(item) => item.id !== productId
);
const newTotal = newItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
cartVar({
...currentCart,
items: newItems,
total: newTotal,
});
},
updateQuantity: (productId: string, quantity: number) => {
const currentCart = cartVar();
if (quantity <= 0) {
cartHelpers.removeItem(productId);
return;
}
const newItems = currentCart.items.map((item) =>
item.id === productId ? { ...item, quantity } : item
);
const newTotal = newItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
cartVar({
...currentCart,
items: newItems,
total: newTotal,
});
},
toggleCart: () => {
const currentCart = cartVar();
cartVar({
...currentCart,
isOpen: !currentCart.isOpen,
});
},
clearCart: () => {
cartVar({
items: [],
isOpen: false,
total: 0,
});
},
};
カートコンポーネントの実装例です。
typescriptconst ShoppingCart = () => {
const cart = useReactiveVar(cartVar);
if (!cart.isOpen) {
return (
<button
className='cart-toggle'
onClick={cartHelpers.toggleCart}
>
カート ({cart.items.length})
</button>
);
}
return (
<div className='cart-overlay'>
<div className='cart-panel'>
<div className='cart-header'>
<h3>ショッピングカート</h3>
<button onClick={cartHelpers.toggleCart}>
×
</button>
</div>
<div className='cart-items'>
{cart.items.length === 0 ? (
<p>カートは空です</p>
) : (
cart.items.map((item) => (
<div key={item.id} className='cart-item'>
<img src={item.image} alt={item.name} />
<div className='item-details'>
<h4>{item.name}</h4>
<p>¥{item.price}</p>
<div className='quantity-controls'>
<button
onClick={() =>
cartHelpers.updateQuantity(
item.id,
item.quantity - 1
)
}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() =>
cartHelpers.updateQuantity(
item.id,
item.quantity + 1
)
}
>
+
</button>
</div>
</div>
<button
onClick={() =>
cartHelpers.removeItem(item.id)
}
className='remove-item'
>
削除
</button>
</div>
))
)}
</div>
{cart.items.length > 0 && (
<div className='cart-footer'>
<div className='total'>
合計: ¥{cart.total.toLocaleString()}
</div>
<button className='checkout-button'>
レジに進む
</button>
<button
className='clear-cart'
onClick={cartHelpers.clearCart}
>
カートを空にする
</button>
</div>
)}
</div>
</div>
);
};
以下の図は、複雑な状態管理パターンでのデータフローを示しています:
mermaidflowchart TD
ProductList[商品一覧] --> AddItem[商品追加]
AddItem --> CartVar[cartVar 状態更新]
CartVar --> CartComponent[カートコンポーネント]
CartVar --> HeaderBadge[ヘッダーバッジ]
CartVar --> CheckoutButton[チェックアウト]
CartComponent --> UpdateQuantity[数量変更]
CartComponent --> RemoveItem[商品削除]
UpdateQuantity --> CartVar
RemoveItem --> CartVar
style CartVar fill:#e1f5fe
style ProductList fill:#f3e5f5
style CartComponent fill:#fff3e0
この実装では、カート状態の変更が複数のコンポーネントに自動的に反映されます。状態の一貫性が保たれ、手動での同期処理が不要になります。
まとめ
Apollo Client の Reactive Variables は、GraphQL を活用したモダンな Web アプリケーション開発において、状態管理の複雑さを大幅に軽減する革新的なソリューションです。
従来の Redux や Context API といった状態管理ライブラリが抱えていた課題を解決し、以下の大きなメリットを提供します。まず、Apollo Client との完全な統合により、サーバー状態とクライアント状態を統一的に管理できるようになります。これにより、データの整合性が保たれ、開発者の認知負荷が大幅に軽減されるでしょう。
宣言的な状態管理アプローチにより、コードの可読性と保守性が向上します。状態の変更を伝播する仕組みが自動化されるため、手動での同期処理が不要になり、バグの発生確率も大幅に減少します。
最小限の設定で高機能な状態管理を実現できるため、開発効率が向上し、チーム全体の生産性向上にもつながります。TypeScript との親和性も高く、型安全性を保ちながら開発を進められる点も大きな魅力です。
GraphQL を採用したプロジェクトにおいて、Reactive Variables は状態管理の新しいスタンダードとなる可能性を秘めています。従来の複雑な状態管理から脱却し、よりシンプルで効率的な開発体験を実現するために、ぜひ Reactive Variables の導入を検討してみてください。
関連リンク
- article
Apollo Client の Reactive Variables - GraphQL でグローバル状態管理
- article
Apollo Client の状態管理完全攻略 - Cache とローカル状態の使い分け
- article
ゼロから始める Apollo 開発環境 - セットアップから初回クエリまで
- article
Apollo で GraphQL Schema 設計 - スケーラブルな API 構築術
- article
Apollo vs Relay - GraphQL クライアント選択の決定版
- article
GraphQL 初心者が知るべき Apollo エコシステム全体像
- article
Apollo Client の Reactive Variables - GraphQL でグローバル状態管理
- article
Apollo で GraphQL Schema 設計 - スケーラブルな API 構築術
- article
Apollo vs Relay - GraphQL クライアント選択の決定版
- article
GraphQL 初心者が知るべき Apollo エコシステム全体像
- article
Apollo Server 完全ガイド - GraphQL API を最速で構築する方法
- article
5 分で理解する Apollo Client - React 開発者のための GraphQL 入門
- article
Astro と Tailwind CSS で美しいデザインを最速実現
- article
shadcn/ui のコンポーネント一覧と使い方まとめ
- article
Apollo Client の Reactive Variables - GraphQL でグローバル状態管理
- article
Remix でデータフェッチ最適化:Loader のベストプラクティス
- article
ゼロから始める Preact 開発 - セットアップから初回デプロイまで
- article
Zod で配列・オブジェクトを安全に扱うバリデーションテクニック
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- blog
失敗を称賛する文化はどう作る?アジャイルな組織へ生まれ変わるための第一歩
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来