Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

Reactの条件付きレンダリングでのちらつき問題を解決する完全ガイド

記事のサムネイル
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

はじめに:Reactの条件付きレンダリングとちらつき問題

Reactでアプリケーションを開発していると、条件付きレンダリングを使う場面は非常に多いでしょう。ユーザーの状態に応じてコンポーネントを表示・非表示にしたり、データの読み込み状態に応じて異なるUI要素を表示したりすることは、モダンなWebアプリケーションでは当たり前の実装です。

しかし、こうした条件付きレンダリングを実装すると、画面が一瞬ちらつく「フリッカリング」と呼ばれる問題に悩まされることがあります。例えば以下のようなコードを書いたことはありませんか?

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error(error);
        setLoading(false);
      });
  }, [userId]);

  if (loading) {
    return <LoadingSpinner />;
  }

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      {/* その他のユーザー情報 */}
    </div>
  );
}

このコードは一見問題なさそうに見えますが、userIdが変更されるたびに、ローディングスピナーが表示され、データが読み込まれた後に実際のコンテンツが表示されます。これにより、UIが一瞬ちらつき、ユーザー体験が悪化してしまいます。特に高速なネットワーク環境では、この「ちらつき」が目立って不自然に感じられます。

「百聞は一見にしかず」という言葉がありますが、UI開発においても同様です。ユーザーにとって視覚的な一貫性は非常に重要であり、ちらつきのあるUIはプロフェッショナルさに欠けている印象を与えてしまいます。

この記事では、Reactでの条件付きレンダリングにおけるちらつき問題の原因を探り、それを解決するための様々なテクニックを紹介します。useEffectuseLayoutEffectの適切な使い分け、メモ化を活用したパフォーマンス最適化、SSRアプリケーションでの特別な考慮事項など、実践的な解決策を詳しく解説していきます。

おすすめの書籍

useLayoutEffectとuseEffectの重要な違いとベストプラクティス

Reactでちらつき問題を解決するにあたって、まず理解すべきなのはuseEffectuseLayoutEffectの違いです。一見すると似ているこの2つのフックですが、実行タイミングに重要な違いがあります。

実行タイミングの違い

useEffectuseLayoutEffectの主な違いは、実行タイミングにあります。

  • useEffect: 非同期的に実行され、ブラウザがDOMを描画(ペイント)した後に実行されます。つまり、ユーザーは更新されたUIを見てから、エフェクトの処理が実行されます。
  • useLayoutEffect: 同期的に実行され、DOMの変更後、ブラウザが画面を描画する前に実行されます。つまり、ユーザーに見える変更が行われる前にエフェクトの処理が完了します。

この違いを図にすると以下のようになります:

React更新 → DOM変更 → [useLayoutEffect実行] → ブラウザ描画 → [useEffect実行]

どちらを使うべきか?

基本的にはuseEffectを優先して使うことをお勧めします。理由は以下の通りです:

  1. useLayoutEffectは同期的に実行されるため、重い処理を行うとブラウザの描画をブロックし、パフォーマンスに悪影響を与える可能性があります。
  2. ほとんどの副作用処理(データフェッチ、サブスクリプションの設定など)は非同期的で構いません。

しかし、以下のケースではuseLayoutEffectの使用を検討すべきです:

  1. DOMの測定と変更: 要素のサイズやポジションを測定し、それに基づいてレイアウトを調整する場合
  2. ちらつきを防ぐ必要がある場合: ユーザーに見えるレイアウトシフトやちらつきを防ぎたい場合
  3. モーダルやツールチップの配置: 要素の位置に基づいて配置を決める必要がある場合

コード例:ツールチップの位置調整

ツールチップをボタンの上下どちらかに表示するケースを考えてみましょう。useEffectを使用すると、一瞬ツールチップが誤った位置に表示され、その後正しい位置に移動するため、ちらつきが発生します。

// useEffectを使った場合(ちらつきが発生する)
function Tooltip({ children, text }) {
  const [position, setPosition] = useState('bottom');
  const tooltipRef = useRef(null);
  const buttonRef = useRef(null);

  useEffect(() => {
    if (tooltipRef.current && buttonRef.current) {
      const buttonRect = buttonRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      
      // ウィンドウの上部に近い場合は下に、そうでなければ上に表示
      if (buttonRect.top < tooltipRect.height + 20) {
        setPosition('bottom');
      } else {
        setPosition('top');
      }
    }
  }, []);

  return (
    <div className="tooltip-container">
      <button ref={buttonRef}>{children}</button>
      <div 
        ref={tooltipRef}
        className={`tooltip ${position}`}
      >
        {text}
      </div>
    </div>
  );
}

このコードでは、ブラウザが初期位置でツールチップを描画した後で位置調整が行われるため、ちらつきが発生します。一方、useLayoutEffectを使用すると:

// useLayoutEffectを使った場合(ちらつきが発生しない)
function Tooltip({ children, text }) {
  const [position, setPosition] = useState('bottom');
  const tooltipRef = useRef(null);
  const buttonRef = useRef(null);

  useLayoutEffect(() => {
    if (tooltipRef.current && buttonRef.current) {
      const buttonRect = buttonRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      
      // ウィンドウの上部に近い場合は下に、そうでなければ上に表示
      if (buttonRect.top < tooltipRect.height + 20) {
        setPosition('bottom');
      } else {
        setPosition('top');
      }
    }
  }, []);

  return (
    <div className="tooltip-container">
      <button ref={buttonRef}>{children}</button>
      <div 
        ref={tooltipRef}
        className={`tooltip ${position}`}
      >
        {text}
      </div>
    </div>
  );
}

useLayoutEffectを使用することで、ブラウザが画面を描画する前に位置調整が行われるため、ユーザーはツールチップが正しい位置に表示されるのを見るだけで、ちらつきは発生しません。

SSRでの注意点

Server-Side Rendering(SSR)を使用する場合、useLayoutEffectは問題を引き起こす可能性があります。なぜなら、サーバー側にはレイアウト情報がないためです。SSRを使用する場合は、次のような対応が必要です:

  1. useEffectを使用する(サーバー側では実行されない)
  2. コンポーネントをクライアントのみのコンポーネントとしてマークする
  3. ハイドレーション後にのみレイアウト計算を行うよう実装する

このトピックについては、後のセクションで詳しく説明します。

おすすめの書籍

ちらつき問題を解決するための5つの効果的な戦略

条件付きレンダリングによるちらつき問題を解決するために、以下の5つの効果的な戦略を紹介します。状況に応じて最適な方法を選びましょう。

1. ロードデータをキャッシュして使用する

データ取得時のちらつきを防ぐ最も効果的な方法の一つは、以前に取得したデータを一時的にキャッシュして表示することです。

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const prevUserRef = useRef(null);

  useEffect(() => {
    // 前回のユーザーデータを保存
    if (user) {
      prevUserRef.current = user;
    }

    setLoading(true);
    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error(error);
        setLoading(false);
      });
  }, [userId]);

  // ローディング中でも以前のデータがあればそれを表示
  if (loading && !prevUserRef.current) {
    return <LoadingSpinner />;
  }

  const displayUser = loading ? prevUserRef.current : user;

  return (
    <div className="user-profile">
      <h2>{displayUser.name}</h2>
      <p>{displayUser.email}</p>
      {loading && <div className="overlay-loader">更新中...</div>}
    </div>
  );
}

このアプローチでは、新しいデータを読み込んでいる間も以前のデータを表示し続けることで、コンポーネント全体の表示・非表示によるちらつきを防ぎます。小さなローダーやスケルトンUIを重ねて表示することで、更新中であることをユーザーに伝えることもできます。

2. トランジションを使用する(React 18+)

React 18では、useTransitionフックが導入され、UIの更新に優先順位をつけることができるようになりました。これを使用して、ちらつきを抑えながらUIを更新する方法を見てみましょう。

import { useState, useTransition } from 'react';

function TabContainer() {
  const [activeTab, setActiveTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const handleTabChange = (tab) => {
    // トランジションでステート更新を低優先度にする
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      <nav>
        <button 
          onClick={() => handleTabChange('home')}
          className={activeTab === 'home' ? 'active' : ''}
        >
          ホーム
        </button>
        <button 
          onClick={() => handleTabChange('profile')}
          className={activeTab === 'profile' ? 'active' : ''}
        >
          プロフィール
        </button>
        <button 
          onClick={() => handleTabChange('settings')}
          className={activeTab === 'settings' ? 'active' : ''}
        >
          設定
        </button>
      </nav>
      
      {/* isPendingを使って現在のコンテンツをフェードアウトさせることも可能 */}
      <div className={isPending ? 'content pending' : 'content'}>
        {activeTab === 'home' && <HomeTab />}
        {activeTab === 'profile' && <ProfileTab />}
        {activeTab === 'settings' && <SettingsTab />}
      </div>
    </div>
  );
}

useTransitionを使用することで、タブ切り替えなどのUIの更新をスムーズにできます。低優先度の更新としてマークされた状態変更は、より重要なユーザー入力やアニメーションを妨げずに処理されます。

3. CSSトランジションを活用する

ちらつきが気になる場合、CSSトランジションを使用して要素の表示・非表示をスムーズにすることもできます。React TransitionGroupライブラリや、単純なCSSアニメーションを使用する方法があります。

import { useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './transitions.css'; // CSSトランジションのスタイル

function AlertMessage({ show, message }) {
  return (
    <CSSTransition
      in={show}
      timeout={300}
      classNames="alert"
      unmountOnExit
    >
      <div className="alert">
        {message}
      </div>
    </CSSTransition>
  );
}

// 対応するCSS
// .alert-enter { opacity: 0; transform: scale(0.9); }
// .alert-enter-active { opacity: 1; transform: scale(1); transition: opacity 300ms, transform 300ms; }
// .alert-exit { opacity: 1; }
// .alert-exit-active { opacity: 0; transform: scale(0.9); transition: opacity 300ms, transform 300ms; }

CSSトランジションを使用すると、要素の表示・非表示の切り替えをアニメーション化でき、唐突な変化によるちらつき感を軽減できます。

4. 条件付きレンダリングの最適化

条件によって完全に異なるコンポーネントを表示するのではなく、共通部分は維持しつつ、変更が必要な部分だけを条件付きレンダリングするよう設計しましょう。

// 良くない例(全体が切り替わる)
function UserView({ user, isEditing }) {
  if (isEditing) {
    return <UserEditForm user={user} />;
  }
  
  return <UserProfile user={user} />;
}

// 改善例(共通部分を維持)
function UserView({ user, isEditing }) {
  return (
    <div className="user-container">
      <header className="user-header">
        <h1>{user.name}</h1>
      </header>
      
      <div className="user-content">
        {isEditing ? (
          <UserEditForm user={user} />
        ) : (
          <UserProfileDetails user={user} />
        )}
      </div>
      
      <footer className="user-footer">
        <LastUpdated date={user.updatedAt} />
      </footer>
    </div>
  );
}

このアプローチでは、ヘッダーとフッターは常に表示されたままで、コンテンツ部分だけが切り替わります。これにより、画面全体がちらつく問題を軽減できます。

5. 遅延初期化パターンを使用する

初期値の計算が重い場合、遅延初期化パターンを利用して不要な再計算を避けることもちらつき防止に有効です。

function ExpensiveComponent({ data }) {
  // 初期値の計算が重い場合
  const [processedData, setProcessedData] = useState(() => {
    // この計算は初回レンダリング時だけ実行される
    return expensiveDataProcessing(data);
  });

  // dataが変わった時だけ再計算
  useEffect(() => {
    setProcessedData(expensiveDataProcessing(data));
  }, [data]);

  return (
    <div>
      {/* processedDataを使った表示 */}
      {processedData.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

useStateの引数に関数を渡すことで、初期値の計算を初回レンダリング時だけに行うことができます。これにより、不要な再計算によるパフォーマンスの低下を防ぎ、レンダリングの安定性を高めることができます。

上記の5つの戦略を状況に応じて適切に組み合わせることで、Reactアプリケーションでのちらつき問題を効果的に解決できます。特に重要なのは、ユーザー体験を第一に考え、唐突な表示の切り替えではなく、スムーズなトランジションを心がけることです。

「良いUIは、ユーザーに気づかれないほど自然であるべきだ」という言葉があるように、ちらつきのないスムーズなUIを実現することは、プロフェッショナルなWebアプリケーション開発において非常に重要です。

おすすめの書籍

メモ化テクニックでパフォーマンスを最適化する

Reactのちらつき問題は、不要な再レンダリングが原因で発生することも多いです。メモ化(Memoization)テクニックを使って、コンポーネントのレンダリングを最適化し、パフォーマンスを向上させる方法を見ていきましょう。

React.memo によるコンポーネントのメモ化

子コンポーネントが親コンポーネントのレンダリングによって不必要に再レンダリングされることがあります。React.memoを使うと、propsが変更されない限り子コンポーネントの再レンダリングをスキップできます。

// メモ化されていないコンポーネント(親の再レンダリングで必ず再レンダリングされる)
function ExpensiveList({ items }) {
  console.log('ExpensiveList rendering');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// メモ化されたコンポーネント(propsが変わらなければ再レンダリングされない)
const MemoizedExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('MemoizedExpensiveList rendering');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

たとえば以下のような親コンポーネントがあったとします:

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [items] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      {/* count が変わるたびに再レンダリングされる */}
      <ExpensiveList items={items} />
      {/* items が変わらなければ再レンダリングされない */}
      <MemoizedExpensiveList items={items} />
    </div>
  );
}

ExpensiveListcountの変更ごとに再レンダリングされますが、MemoizedExpensiveListitemsが変わらない限り再レンダリングされません。

useMemo によるコストの高い計算結果のメモ化

useMemoフックを使用すると、コストの高い計算結果をメモ化して、依存関係が変更された場合にのみ再計算するようにできます。

function DataGrid({ data, filter }) {
  // filterが変わるたびに毎回フィルタリング計算が行われる(非効率)
  const filteredData = data.filter(item => item.name.includes(filter));

  return (
    <div>
      {filteredData.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

// 改善バージョン
function OptimizedDataGrid({ data, filter }) {
  // dataまたはfilterが変わったときだけフィルタリング計算が行われる
  const filteredData = useMemo(() => {
    console.log('Filtering data...');
    return data.filter(item => item.name.includes(filter));
  }, [data, filter]);

  return (
    <div>
      {filteredData.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

useMemoはコストの高い計算結果をキャッシュすることで、不要な再計算を防ぎます。依存配列(この例では[data, filter])の値が変わったときだけ再計算が行われます。

useCallback による関数のメモ化

Reactでは、コンポーネントが再レンダリングされるたびに、そのコンポーネント内で定義されている関数も再作成されます。子コンポーネントにこれらの関数をpropsとして渡すと、関数の参照が毎回変わるため、子コンポーネントが不必要に再レンダリングされることがあります。

useCallbackを使うと、依存関係が変更されない限り、関数のインスタンスを保持できます。

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // この関数は ParentComponent が再レンダリングされるたびに再作成される
  const handleClick = () => {
    console.log('Button clicked!');
  };
  
  // この関数は依存関係が変わらない限りは同じインスタンスが保持される
  const memoizedHandleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      {/* count が変わるたびに handleClick が変わるので再レンダリングされる */}
      <Button onClick={handleClick} label="非最適化ボタン" />
      {/* handleClick が変わらないので再レンダリングされない */}
      <Button onClick={memoizedHandleClick} label="最適化ボタン" />
    </div>
  );
}

// Buttonコンポーネントは React.memo でメモ化されていると仮定
const Button = React.memo(function Button({ onClick, label }) {
  console.log(`${label} rendering`);
  return <button onClick={onClick}>{label}</button>;
});

オブジェクトの依存関係に注意する

Reactの重要な注意点として、オブジェクトや配列はJavaScriptでは参照で比較されることを理解しておく必要があります。つまり、新しい参照が作成されるたびに、Reactはそれらを異なるものとして扱います。

function BadExample() {
  // 毎回のレンダリングで新しいオブジェクトが作成される
  const options = { key: 'value' };
  
  return <MemoizedComponent options={options} />;
  // MemoizedComponent は React.memo で包まれていても毎回再レンダリングされる
}

function GoodExample() {
  // optionsはuseMemoでメモ化されている
  const options = useMemo(() => ({ key: 'value' }), []);
  
  return <MemoizedComponent options={options} />;
  // MemoizedComponent は再レンダリングされない
}

カスタムフックで複雑なロジックをカプセル化する

複雑なロジックはカスタムフックにカプセル化すると、コードがすっきりし、メモ化の管理も簡単になります。

// カスタムフックでフィルタリングロジックをカプセル化
function useFilteredData(data, filter) {
  return useMemo(() => {
    return data.filter(item => item.name.includes(filter));
  }, [data, filter]);
}

// 使用例
function DataGrid({ data, filter }) {
  const filteredData = useFilteredData(data, filter);
  
  return (
    <div>
      {filteredData.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

メモ化の注意点

メモ化は万能薬ではなく、使いすぎると逆効果になることもあります:

  1. 過剰な最適化に注意: 単純なコンポーネントや計算では、メモ化のオーバーヘッドの方が大きくなる場合があります。
  2. 依存配列の管理: 依存配列を正確に設定しないと、古い値を参照し続ける「ステイルクロージャ」問題が発生する可能性があります。
  3. デバッグの複雑さ: メモ化によりコードの挙動追跡が複雑になることがあります。

「プレマチュア・オプティマイゼーション(早すぎる最適化)は諸悪の根源」という格言があるように、まずは問題が発生している部分を特定し、必要な箇所だけを最適化することが重要です。React DevTools Profilerを使用して、どのコンポーネントが不必要に再レンダリングされているかを特定することをお勧めします。

適切なメモ化テクニックを使用することで、不要な再レンダリングを防ぎ、アプリケーションのパフォーマンスを向上させることができます。これにより、画面のちらつきも軽減され、より滑らかなユーザー体験を提供できるでしょう。

おすすめの書籍

SSRアプリケーションでの特別な考慮事項

Server-Side Rendering(SSR)を使用するReactアプリケーション(Next.js、Gatsby、Remixなど)では、ちらつき問題に関する特別な考慮が必要です。SSRの仕組みとちらつき問題の関連性を理解し、効果的な解決策を見ていきましょう。

SSRにおけるちらつき問題の原因

SSRアプリケーションでは、主に以下の3つの理由からちらつき問題が発生します:

  1. ハイドレーション不一致: サーバーでレンダリングされたHTMLとクライアントで初期化されるReactの状態が一致しない場合
  2. useLayoutEffectの挙動: SSR環境ではuseLayoutEffectが適切に機能しない
  3. レイアウト情報の欠如: サーバー側ではブラウザのレイアウト情報(ウィンドウサイズ、要素の位置など)が利用できない

特にuseLayoutEffectの問題は、Next.jsやGatsbyなどのSSRフレームワークを使用する際によく遭遇します。サーバー側ではレイアウト情報がないため、useLayoutEffectは正しく実行できず、警告が表示されたり、ハイドレーション不一致が発生したりします。

SSR環境での解決策

1. クライアントサイドのみのコンポーネント

Next.js 13以降では、「use client」ディレクティブを使用して、コンポーネントをクライアントサイドのみでレンダリングするよう指定できます。

'use client';

import { useLayoutEffect, useState } from 'react';

function ClientOnlyComponent() {
  const [height, setHeight] = useState(0);
  
  useLayoutEffect(() => {
    setHeight(window.innerHeight);
  }, []);
  
  return <div>ウィンドウの高さ: {height}px</div>;
}

export default ClientOnlyComponent;

他のフレームワークでは、同様の概念を実現するために以下のようなパターンを使用できます:

import { useEffect, useState } from 'react';

function ClientOnly({ children, fallback = null }) {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return isClient ? children : fallback;
}

// 使用例
function MyPage() {
  return (
    <div>
      <h1>マイページ</h1>
      <ClientOnly fallback={<div>読み込み中...</div>}>
        <ComponentWithLayoutEffect />
      </ClientOnly>
    </div>
  );
}

2. useIsomorphicLayoutEffect パターン

SSR対応のために、useLayoutEffectuseEffectを環境に応じて切り替えるカスタムフックを作成する方法もあります:

import { useLayoutEffect, useEffect } from 'react';

// サーバーサイドレンダリング時は useEffect、
// クライアントサイドレンダリング時は useLayoutEffect を使用する
const useIsomorphicLayoutEffect = typeof window !== 'undefined' 
  ? useLayoutEffect 
  : useEffect;

// 使用例
function ResponsiveComponent() {
  const [width, setWidth] = useState(0);
  
  useIsomorphicLayoutEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    handleResize();
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return <div>ウィンドウの幅: {width}px</div>;
}

このパターンは、SSR時にはuseEffect(サーバーでは実行されない)を使い、クライアント側ではuseLayoutEffectを使うことで、警告を回避しつつ最適なパフォーマンスを実現します。

3. ハイドレーション後のレイアウト計算

ハイドレーション後にのみレイアウト計算を行い、それまでは仮の表示をするアプローチも効果的です:

function ModalWithPosition() {
  const [isHydrated, setIsHydrated] = useState(false);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const targetRef = useRef(null);
  
  // ハイドレーションの検出
  useEffect(() => {
    setIsHydrated(true);
  }, []);
  
  // ハイドレーション後にレイアウト計算
  useLayoutEffect(() => {
    if (isHydrated && targetRef.current) {
      const rect = targetRef.current.getBoundingClientRect();
      setPosition({
        top: rect.bottom + window.scrollY,
        left: rect.left + window.scrollX
      });
    }
  }, [isHydrated]);
  
  return (
    <>
      <button ref={targetRef}>ターゲット</button>
      <div 
        className="modal" 
        style={{ 
          position: 'absolute',
          top: position.top,
          left: position.left,
          visibility: isHydrated ? 'visible' : 'hidden'
        }}
      >
        モーダルコンテンツ
      </div>
    </>
  );
}

このアプローチでは、ハイドレーションが完了するまでモーダルを非表示にし、レイアウト計算が完了してから表示します。これにより、位置の調整によるちらつきを防止できます。

4. マウント状態の検出とスケルトンUI

SSRアプリケーションでは、ハイドレーション中のちらつきを軽減するために、スケルトンUIやプレースホルダーを活用する戦略も効果的です:

function DataTable({ data }) {
  const [isMounted, setIsMounted] = useState(false);
  
  useEffect(() => {
    setIsMounted(true);
  }, []);
  
  if (!isMounted) {
    return <TableSkeleton rows={5} />;
  }
  
  return (
    <table>
      <thead>
        <tr>
          <th>名前</th>
          <th>年齢</th>
          <th>職業</th>
        </tr>
      </thead>
      <tbody>
        {data.map(person => (
          <tr key={person.id}>
            <td>{person.name}</td>
            <td>{person.age}</td>
            <td>{person.job}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// スケルトンUIコンポーネント
function TableSkeleton({ rows = 3 }) {
  return (
    <table className="skeleton-table">
      <thead>
        <tr>
          <th><div className="skeleton-cell" /></th>
          <th><div className="skeleton-cell" /></th>
          <th><div className="skeleton-cell" /></th>
        </tr>
      </thead>
      <tbody>
        {Array.from({ length: rows }).map((_, i) => (
          <tr key={i}>
            <td><div className="skeleton-cell" /></td>
            <td><div className="skeleton-cell" /></td>
            <td><div className="skeleton-cell" /></td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

このテクニックでは、マウント前(ハイドレーション中)はスケルトンUIを表示し、マウント後に実際のデータを表示します。スケルトンUIは実際のUIと同じ大きさと構造を持つようにデザインすることで、レイアウトシフトを防止できます。

SSRフレームワーク固有の最適化

Next.js

Next.jsでのち らつき防止には、以下のテクニックが効果的です:

// app/components/dynamic-content.jsx
'use client';

import { useState, useEffect } from 'react';

export default function DynamicContent() {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  if (!isClient) {
    return <div className="placeholder">Loading...</div>;
  }
  
  return (
    <div>
      {/* クライアントサイドでのみ必要な計算やレイアウト */}
      Window width: {window.innerWidth}px
    </div>
  );
}

また、Next.js 13以降のApp Routerでは、<Suspense>とstreaming SSRを活用することで、ハイドレーションによるちらつきを軽減できます:

// app/page.jsx
import { Suspense } from 'react';
import DynamicContent from './components/dynamic-content';

export default function Page() {
  return (
    <div>
      <h1>マイページ</h1>
      <Suspense fallback={<div className="placeholder">Loading...</div>}>
        <DynamicContent />
      </Suspense>
    </div>
  );
}

SSRアプリケーションでのちらつき問題解決には、フレームワークが提供する機能をうまく活用し、ハイドレーションの過程を考慮した設計が重要です。環境に応じてレンダリング戦略を切り替え、ユーザー体験を最適化しましょう。

「ユーザーはレンダリングプロセスには興味がなく、最終的なUI体験だけを気にする」ということを心に留めておきましょう。この観点から、SSRアプリケーションでも滑らかなトランジションを実現するための工夫が必要です。

おすすめの書籍

まとめ:ユーザー体験を向上させるスムーズなUI実装のために

ここまでReactアプリケーションでのちらつき問題の原因と解決策について詳しく見てきました。最後に、これらの知識を実際のプロジェクトで活用するポイントをまとめます。

ちらつき問題解決のための基本原則

  1. 変更は最小限に: UIの更新は必要最小限にとどめ、共通の要素は再レンダリングしないようにする
  2. トランジションを活用: 唐突な変化ではなく、スムーズなトランジションを使用してUI変更を自然に見せる
  3. 事前計画: ローディング状態やエラー状態など、あらゆる状態を事前に計画し、ユーザー体験を考慮した設計を行う
  4. レイアウトシフトを避ける: 要素の位置や大きさが突然変わることを避け、CLS(Cumulative Layout Shift)を最小限に抑える

チェックリスト:ちらつきのないUI実装の実現

以下のチェックリストを使って、あなたのReactアプリケーションがちらつき問題に対処できているか確認してみましょう:

  • DOMの測定と変更を行う場合はuseLayoutEffectを適切に使用している
  • データフェッチ時にキャッシュや前回のデータを適切に利用している
  • コンポーネントの不要な再レンダリングをReact.memoで防いでいる
  • 重い計算はuseMemoでメモ化している
  • イベントハンドラはuseCallbackで最適化している
  • CSSトランジションを適切に活用している
  • SSRアプリケーションでは環境に応じた適切なレンダリング戦略をとっている
  • スケルトンUIやプレースホルダーを活用している

今後の学習のために

Reactは常に進化しており、ちらつき問題に対する新しいアプローチも登場しています。以下のトピックは、さらに深く学ぶ価値があります:

  • React 18の新機能(Concurrent Mode、Suspense、Transitions)
  • React Server Components
  • React Query や SWR などのデータフェッチライブラリ
  • アクセシビリティとちらつきの関係

「障害は魅力的なユーザー体験への道の障害物ではなく、それを向上させるための機会である」というデザインの賢者の言葉があります。ちらつき問題も同様に、よりスムーズで直感的なUIを構築するための学びの機会と捉えましょう。

適切なテクニックを使って、ユーザーにストレスを感じさせない、自然に使えるUIを提供することは、プロフェッショナルなReact開発者の重要なスキルです。この記事で紹介した方法を実践し、あなたのアプリケーションのユーザー体験を向上させてください。

最後に、パフォーマンス最適化はバランスが重要です。過度な最適化よりも、ユーザーにとって価値のある機能と体験を提供することを第一に考えましょう。ユーザーがアプリケーションを使用しているときに、「このアプリは本当にスムーズだ」と感じてもらえれば、それはあなたの成功です。

「良いデザインは目立たない。良いデザインとは透明であり、ユーザーがコンテンツに集中できるようにすることだ」- ジョナサン・アイブ

おすすめの書籍

おすすめコンテンツ