Reactでグローバル変数を使うリスク|状態管理の基本

Reactでグローバル変数を使用することのリスクと問題点を詳しく解説。適切な状態管理手法(useState、useContext、Redux)への移行方法と、保守性の高いコードの書き方を初心者向けに説明します。

Learning Next 運営
28 分で読めます

みなさん、Reactで開発していてこんな悩みを抱えたことはありませんか?

「コンポーネント間でデータを共有したくて、グローバル変数を使ってしまった」
「変数を更新したのに、なぜか画面が更新されない」
「なんでコンポーネントが再レンダリングされないの?」

グローバル変数は手軽で便利に見えますが、実はReactでは大きな落とし穴があります。 知らずに使ってしまうと、バグの原因になったり、アプリの動作が不安定になったりします。

この記事では、Reactでグローバル変数を使うリスクと、正しい状態管理の方法をお教えします。 安定したReactアプリを作るための基本を、一緒に学んでいきましょう。

グローバル変数って何?基本を理解しよう

まずは、グローバル変数とは何かを理解しましょう。 意外と知らないうちに使ってしまっている人も多いんです。

JavaScriptでのグローバル変数

グローバル変数は、プログラム全体からアクセスできる変数のことです。

// グローバル変数の例
let globalCounter = 0;
let globalUser = null;
let globalSettings = {
  theme: 'light',
  language: 'ja'
};

// どこからでもアクセス可能
function incrementCounter() {
  globalCounter++;
}

function updateUser(userData) {
  globalUser = userData;
}

このようにファイルの一番上で宣言した変数は、アプリのどこからでも参照・変更できます。 一見便利そうですが、Reactでは問題が発生します。

Reactでグローバル変数を使った時の問題

Reactでグローバル変数を使うと、以下のような問題が起こります。

  • 画面が更新されない: データが変わっても表示が変わらない
  • どこで変更されたかわからない: デバッグが困難
  • テストが書きにくい: 動作の確認が難しい
  • 予期しない動作: 思わぬところで値が変わる

これらの問題により、アプリの品質が大幅に下がってしまいます。

実際に起こる問題を見てみよう

具体的なコード例で、どんな問題が起こるかを見てみましょう。 きっと、「あっ、これやったことある」と思う方もいるはずです。

問題のあるコード例

// ❌ 問題のあるグローバル変数の使用
let globalUserData = null;
let globalCartItems = [];

// ユーザー情報コンポーネント
function UserProfile() {
  // グローバル変数を直接参照
  if (!globalUserData) {
    return <div>ログインしてください</div>;
  }
  
  return (
    <div>
      <h2>{globalUserData.name}</h2>
      <p>{globalUserData.email}</p>
    </div>
  );
}

// ログインコンポーネント
function LoginForm() {
  const handleLogin = (userData) => {
    // グローバル変数を直接変更
    globalUserData = userData;
    
    // ⚠️ ここが問題!コンポーネントは再レンダリングされない!
    console.log('ログイン完了');
  };
  
  return (
    <form onSubmit={handleLogin}>
      {/* フォームの内容 */}
    </form>
  );
}

// ショッピングカートコンポーネント
function ShoppingCart() {
  const addItem = (item) => {
    // グローバル変数を直接変更
    globalCartItems.push(item);
    
    // ⚠️ 画面に反映されない!
  };
  
  return (
    <div>
      <h3>カート ({globalCartItems.length})</h3>
      {/* カートの内容が更新されない */}
    </div>
  );
}

このコードを実行すると、以下のような問題が起こります。

  • ログインしても画面が変わらない
  • カートに商品を追加しても表示が変わらない
  • 手動でページを更新しないと正しく表示されない

なぜ画面が更新されないの?

Reactが画面を更新(再レンダリング)するのは、以下の場合だけです。

  • State が変更された時
  • Props が変更された時
  • 親コンポーネントが再レンダリングされた時

グローバル変数の変更は、これらの条件に当てはまりません。 そのため、データは変わっているのに画面は古いままという状態になってしまいます。

簡単に言うと、Reactはグローバル変数の変更を検知できないんです。

正しい状態管理の方法

では、どうすれば良いのでしょうか? Reactには、データの変更を正しく画面に反映させる方法があります。

useStateで状態を管理しよう

最も基本的な解決方法は、useStateを使うことです。

// ✅ useState を使用した正しい状態管理
function App() {
  const [user, setUser] = useState(null);
  const [cartItems, setCartItems] = useState([]);
  
  const handleLogin = (userData) => {
    setUser(userData); // 再レンダリングが発生!
  };
  
  const addToCart = (item) => {
    setCartItems(prev => [...prev, item]); // 再レンダリングが発生!
  };
  
  return (
    <div>
      <UserProfile user={user} />
      <LoginForm onLogin={handleLogin} />
      <ShoppingCart 
        items={cartItems} 
        onAddItem={addToCart} 
      />
    </div>
  );
}

各コンポーネントも、propsでデータを受け取るように変更します。

function UserProfile({ user }) {
  if (!user) {
    return <div>ログインしてください</div>;
  }
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function LoginForm({ onLogin }) {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 認証処理(簡略化)
    const userData = { 
      name: username, 
      email: `${username}@example.com` 
    };
    onLogin(userData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="ユーザー名"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="パスワード"
      />
      <button type="submit">ログイン</button>
    </form>
  );
}

function ShoppingCart({ items, onAddItem }) {
  return (
    <div>
      <h3>カート ({items.length})</h3>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item.name} - ¥{item.price}</li>
        ))}
      </ul>
      <button onClick={() => onAddItem({ 
        name: 'サンプル商品', 
        price: 1000 
      })}>
        商品を追加
      </button>
    </div>
  );
}

このようにuseStateを使うことで、データの変更が正しく画面に反映されます。

データの流れがわかりやすい

useState を使った方法では、データの流れが明確になります。

  1. 親コンポーネントでstateを管理
  2. 子コンポーネントにpropsでデータを渡す
  3. 子コンポーネントからコールバック関数でデータを更新

この流れがあることで、「どこでデータが変更されるか」が一目でわかります。

useContextで複雑な状態を管理しよう

アプリが大きくなってくると、たくさんのコンポーネント間でデータを共有したくなります。 そんな時は、useContextを使いましょう。

Contextを使った状態管理

まず、Contextを作成します。

import React, { createContext, useContext, useState } from 'react';

// Context の作成
const UserContext = createContext();
const CartContext = createContext();

// ユーザー用のProvider
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const login = (userData) => {
    setUser(userData);
  };
  
  const logout = () => {
    setUser(null);
  };
  
  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

// カート用のProvider
function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  
  const addItem = (item) => {
    setItems(prev => [...prev, item]);
  };
  
  const removeItem = (itemId) => {
    setItems(prev => prev.filter(item => item.id !== itemId));
  };
  
  const clearCart = () => {
    setItems([]);
  };
  
  return (
    <CartContext.Provider value={{ 
      items, 
      addItem, 
      removeItem, 
      clearCart 
    }}>
      {children}
    </CartContext.Provider>
  );
}

使いやすくするために、カスタムフックを作ります。

// カスタムフック
function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  return context;
}

function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within CartProvider');
  }
  return context;
}

アプリケーション全体を設定

Providerでアプリ全体をラップします。

function App() {
  return (
    <UserProvider>
      <CartProvider>
        <Header />
        <Main />
        <Footer />
      </CartProvider>
    </UserProvider>
  );
}

各コンポーネントでContextを使用

どのコンポーネントからでも、必要なデータにアクセスできます。

// ヘッダーコンポーネント
function Header() {
  const { user, logout } = useUser();
  const { items } = useCart();
  
  return (
    <header>
      <h1>オンラインストア</h1>
      {user ? (
        <div>
          <span>こんにちは、{user.name}さん</span>
          <button onClick={logout}>ログアウト</button>
        </div>
      ) : (
        <LoginButton />
      )}
      <div>カート: {items.length}個</div>
    </header>
  );
}

// 商品一覧コンポーネント
function ProductList() {
  const { addItem } = useCart();
  
  const products = [
    { id: 1, name: '商品A', price: 1000 },
    { id: 2, name: '商品B', price: 1500 },
    { id: 3, name: '商品C', price: 2000 }
  ];
  
  return (
    <div>
      <h2>商品一覧</h2>
      {products.map(product => (
        <div key={product.id}>
          <span>{product.name} - ¥{product.price}</span>
          <button onClick={() => addItem(product)}>
            カートに追加
          </button>
        </div>
      ))}
    </div>
  );
}

この方法だと、コンポーネントがどんなに深い階層にあっても、簡単にデータにアクセスできます。

状態管理ライブラリを使ってみよう

さらに大きなアプリでは、専用の状態管理ライブラリを使うと便利です。 ここでは、使いやすいZustandを紹介します。

Zustandを使った状態管理

まず、インストールします。

npm install zustand

ストアを作成します。

// store.js
import { create } from 'zustand';

// ユーザー管理ストア
const useUserStore = create((set) => ({
  user: null,
  login: (userData) => set({ user: userData }),
  logout: () => set({ user: null }),
  updateProfile: (updates) => set((state) => ({ 
    user: { ...state.user, ...updates } 
  }))
}));

// カート管理ストア
const useCartStore = create((set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ 
    items: [...state.items, item] 
  })),
  removeItem: (itemId) => set((state) => ({ 
    items: state.items.filter(item => item.id !== itemId) 
  })),
  clearCart: () => set({ items: [] }),
  getTotalPrice: () => {
    const { items } = get();
    return items.reduce((total, item) => total + item.price, 0);
  }
}));

export { useUserStore, useCartStore };

コンポーネントで使用します。

// コンポーネントでの使用
function UserProfile() {
  const { user, logout } = useUserStore();
  
  if (!user) {
    return <div>ログインしてください</div>;
  }
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={logout}>ログアウト</button>
    </div>
  );
}

function CartSummary() {
  const { items, getTotalPrice, clearCart } = useCartStore();
  
  return (
    <div>
      <h3>カート合計</h3>
      <p>商品数: {items.length}</p>
      <p>合計金額: ¥{getTotalPrice()}</p>
      <button onClick={clearCart}>カートを空にする</button>
    </div>
  );
}

Zustandは軽量で使いやすく、Reactの初心者でも簡単に使えます。

パフォーマンスも考えてみよう

正しい状態管理をすることで、アプリのパフォーマンスも向上させることができます。

不要な再レンダリングを防ぐ

React.memoを使って、不要な再レンダリングを防げます。

// React.memo を使用した最適化
const UserProfile = React.memo(function UserProfile({ user }) {
  console.log('UserProfile rendered');
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
});

const CartItem = React.memo(function CartItem({ item, onRemove }) {
  console.log(`CartItem ${item.id} rendered`);
  
  return (
    <div>
      <span>{item.name} - ¥{item.price}</span>
      <button onClick={() => onRemove(item.id)}>削除</button>
    </div>
  );
});

状態を分離してパフォーマンス向上

関連のない状態は分離することで、必要な部分だけが再レンダリングされます。

function App() {
  const [user, setUser] = useState(null);
  const [cartItems, setCartItems] = useState([]);
  const [theme, setTheme] = useState('light'); // 独立した状態
  
  // ユーザー情報の変更はカートに影響しない
  const updateUser = (userData) => {
    setUser(userData); // UserProfile のみ再レンダリング
  };
  
  // カートの変更はユーザー情報に影響しない
  const addToCart = (item) => {
    setCartItems(prev => [...prev, item]); // CartItem のみ再レンダリング
  };
  
  return (
    <div className={`app theme-${theme}`}>
      <UserProfile user={user} />
      <CartList items={cartItems} onAddItem={addToCart} />
      <ThemeSelector theme={theme} onThemeChange={setTheme} />
    </div>
  );
}

このように状態を適切に分離することで、変更が必要な部分だけが更新されます。

useMemoとuseCallbackで最適化

重い処理やコールバック関数も最適化できます。

function ExpensiveComponent({ items, filters }) {
  // 重い計算の結果をメモ化
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => {
      return filters.every(filter => filter(item));
    });
  }, [items, filters]);
  
  // コールバック関数のメモ化
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
    // クリック処理
  }, []);
  
  return (
    <div>
      {filteredItems.map(item => (
        <ItemCard 
          key={item.id} 
          item={item} 
          onClick={handleItemClick}
        />
      ))}
    </div>
  );
}

これらの最適化により、アプリがスムーズに動作するようになります。

デバッグとテストが簡単になる

正しい状態管理をすることで、デバッグやテストも楽になります。

デバッグしやすくなる

// ✅ 状態の変更が追跡しやすい
function UserManager() {
  const [user, setUser] = useState(null);
  
  const updateUser = (userData) => {
    console.log('User updated:', { before: user, after: userData });
    setUser(userData);
  };
  
  return (
    <div>
      {/* React Developer Tools で状態を確認可能 */}
      <UserProfile user={user} onUpdate={updateUser} />
    </div>
  );
}

// ❌ グローバル変数は追跡が困難
let globalUser = null;

function updateGlobalUser(userData) {
  console.log('Global user updated'); // 変更前の値がわからない
  globalUser = userData; // どこから変更されたかわからない
}

React Developer Toolsを使えば、stateの変更を簡単に確認できます。

テストが書きやすくなる

// ✅ 状態管理コンポーネントのテスト
import { render, screen, fireEvent } from '@testing-library/react';
import UserProfile from './UserProfile';

test('ユーザー情報が正しく表示される', () => {
  const mockUser = { name: '田中太郎', email: 'tanaka@example.com' };
  
  render(<UserProfile user={mockUser} />);
  
  expect(screen.getByText('田中太郎')).toBeInTheDocument();
  expect(screen.getByText('tanaka@example.com')).toBeInTheDocument();
});

test('ログアウトボタンをクリックするとコールバックが呼ばれる', () => {
  const mockUser = { name: '田中太郎', email: 'tanaka@example.com' };
  const mockOnLogout = jest.fn();
  
  render(<UserProfile user={mockUser} onLogout={mockOnLogout} />);
  
  fireEvent.click(screen.getByText('ログアウト'));
  expect(mockOnLogout).toHaveBeenCalled();
});

propsとして渡された値をテストするのは、グローバル変数をテストするよりもずっと簡単です。

既存のコードを移行する方法

すでにグローバル変数を使っているプロジェクトを、段階的に移行する方法をご紹介します。

段階的な移行手順

1. 現在のグローバル変数を特定

// 現在のグローバル変数をリストアップ
let globalUser = null;
let globalCart = [];
let globalSettings = {};

2. Contextを作成

const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(globalUser); // 初期値として使用
  const [cart, setCart] = useState(globalCart);
  const [settings, setSettings] = useState(globalSettings);
  
  return (
    <AppContext.Provider value={{
      user, setUser,
      cart, setCart,
      settings, setSettings
    }}>
      {children}
    </AppContext.Provider>
  );
}

3. 段階的にコンポーネントを移行

// まずは1つのコンポーネントから移行
function UserProfile() {
  const { user } = useContext(AppContext); // Context を使用
  
  return (
    <div>
      {user ? <h2>{user.name}</h2> : <p>ログインしてください</p>}
    </div>
  );
}

4. グローバル変数への依存を削除

すべてのコンポーネントを移行したら、グローバル変数を削除します。

この方法なら、一度にすべてを変更する必要がなく、安全に移行できます。

まとめ:正しい状態管理で快適な開発を!

Reactでのグローバル変数の問題と、正しい状態管理の方法について詳しく解説しました。

重要なポイント

  1. グローバル変数はReactで再レンダリングを引き起こさない
  2. useStateで基本的な状態管理を行う
  3. useContextで複雑な状態を管理する
  4. 状態管理ライブラリで大規模アプリに対応

正しい状態管理のメリット

  • 画面が正しく更新される
  • デバッグが簡単になる
  • テストが書きやすくなる
  • パフォーマンスが向上する
  • コードが理解しやすくなる

今日から始められること

  1. 既存のグローバル変数を見つける
  2. useState に置き換えてみる
  3. useContext を試してみる
  4. React Developer Tools でstate を確認する

最初は複雑に感じるかもしれませんが、慣れてくると正しい状態管理の方がずっと楽になります。 グローバル変数の誘惑に負けず、Reactの仕組みを活用した開発を心がけましょう。

ぜひ実際のプロジェクトでこれらの手法を試してみてください。 きっと、より安定したReactアプリが作れるようになりますよ!

関連記事