State Driven View Management

State-Driven View Management

In the previous lesson, you set up a global store using useReducer and Context. That store gives us state and dispatch anywhere in the app. Today, you will use that setup to control which screen is visible — without React Router. This is a small but powerful pattern: the UI screen is just state (ui.screen), and navigation is just dispatching an action to change that state.

Rendering Screens From Global State (App root)

// src/App.jsx
import React from 'react';
import { GameProvider, useGameContext } from './context/GameContext';
import GameScreen from './components/GameScreen';
import MenuScreen from './components/MenuScreen';
import ResultsScreen from './components/ResultsScreen';
import SettingsScreen from './components/SettingsScreen';
import './App.css';

// Provide fallback components if not present (for teaching/demo continuity)
function FallbackComponent({ name }) {
  return <div style={{ padding: 40, textAlign: 'center', color: '#888' }}>{name} not implemented</div>;
}

// If these components are not implemented, use fallback for demo/runnable code
const SafeGameScreen = GameScreen || (() => <FallbackComponent name="GameScreen" />);
const SafeMenuScreen = MenuScreen || (() => <FallbackComponent name="MenuScreen" />);
const SafeResultsScreen = ResultsScreen || (() => <FallbackComponent name="ResultsScreen" />);
const SafeSettingsScreen = SettingsScreen || (() => <FallbackComponent name="SettingsScreen" />);

function AppContent() {
  const { state } = useGameContext();

  const renderScreen = () => {
    switch (state.ui.screen) {
      case 'menu':
        return <SafeMenuScreen />;
      case 'game':
        return <SafeGameScreen />;
      case 'results':
        return <SafeResultsScreen />;
      case 'settings':
        return <SafeSettingsScreen />;
      default:
        return <SafeMenuScreen />;
    }
  };

  return (
    <div className="app">
      {renderScreen()}
    </div>
  );
}

function App() {
  return (
    <GameProvider>
      <AppContent />
    </GameProvider>
  );
}

export default App;

Explanation:

  • GameProvider wraps the app so all children can access state and dispatch (from the previous lesson’s store).
  • AppContent reads state.ui.screen using useGameContext and picks which screen to render with a simple switch. No routing library is needed.
  • The SafeX components are fallbacks so the app can run even if a screen is missing. In your real project, you will have the actual components.
  • Changing state.ui.screen triggers a re-render, which switches the visible screen.

Navigating by Dispatching Actions (Screens)

// src/components/MenuScreen.jsx
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './MenuScreen.css';

export default function MenuScreen() {
  const { dispatch } = useGameContext();

  return (
    <div className="menu-screen">
      <div className="menu-container">
        <h1 className="game-title">Game Menu</h1>
        <div className="menu-actions">
          <button
            className="large-btn"
            onClick={() => dispatch({ type: 'SET_SCREEN', payload: 'game' })}
          >
            Start Game
          </button>
          <button
            className="large-btn"
            onClick={() => dispatch({ type: 'SET_SCREEN', payload: 'settings' })}
          >
            Settings
          </button>
        </div>
      </div>
    </div>
  );
}

Explanation:

  • This component does not know about routing. It simply dispatches SET_SCREEN with a target screen.
  • App.jsx responds by rendering the correct screen. This keeps navigation logic centralized and UI components simple.
// src/components/GameScreen.jsx
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './GameScreen.css';

export default function GameScreen() {
  const { dispatch } = useGameContext();

  return (
    <div className="game-screen">
      <div className="game-container">
        <h1>Game Screen</h1>
        <p>The game is running...</p>
        <button
          onClick={() => dispatch({ type: 'SET_SCREEN', payload: 'results' })}
        >
          Finish Game
        </button>
        <button
          onClick={() => dispatch({ type: 'SET_SCREEN', payload: 'menu' })}
          className="quit-btn"
        >
          Quit to Menu
        </button>
      </div>
    </div>
  );
}

Explanation:

  • The game screen also uses dispatch to move to results or back to the menu.
  • Again, there are no direct imports of other screens here — just a state change request. That keeps coupling low and makes testing easier.

Tip: ResultsScreen and SettingsScreen follow the same pattern, each dispatching SET_SCREEN to go where they need.

Summary and What Comes Next

You now control navigation through state alone:

  • App renders a screen based on state.ui.screen.
  • Screens navigate by dispatching SET_SCREEN.
  • No router, minimal coupling, and one source of truth.

This is the core of state-driven view management. You are ready to try it yourself — head to the practice section to wire up interactions and see smooth, reducer-powered navigation in action.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal