Action Dispatch and State Updates

Action Dispatching and Immutable Updates

Welcome to the first hands-on unit of this course. We are moving from static screens to a playable experience by wiring user actions to state changes. You will build a dynamic actions panel and ensure that every update to game state is immutable and predictable. This foundation will allow us to add time-based logic and win/loss conditions in later units.

Building GameActions: Config-Driven UI and Dispatch

We will render action buttons from a configuration array and dispatch reducer actions when the player clicks them. Each action decides if it is currently enabled based on the game state. This keeps components simple and declarative.

src/components/GameActions.jsx

JSX
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './GameActions.css';

export default function GameActions() {
  const { state, dispatch } = useGameContext();

  const actions = [
    {
      name: 'Serve Coffee',
      icon: '☕',
      description: 'Quick black coffee for customers',
      cost: '1 bean, 5 energy',
      effect: '+$5, +3 satisfaction',
      enabled: state.resources.coffeeBeans >= 1 && state.resources.energy >= 5 && state.shop.queueLength >= 1,
      action: () => dispatch({ type: 'SERVE_COFFEE' }),
      class: 'primary-btn'
    },
    // Example placeholder actions to ensure the map renders at least two actions
    {
      name: 'Restock Beans',
      icon: '🫘',
      description: 'Restock your coffee beans supply',
      cost: '-$10',
      effect: '+10 beans',
      enabled: state.resources.money >= 10,
      action: () => dispatch({ type: 'RESTOCK_BEANS' }),
      class: 'secondary-btn'
    }
  ];

  return (
    <div className="game-actions">
      <h3 className="actions-title">☕ Available Actions</h3>
      <div className="actions-grid">
        {actions.map(action => (
          <div key={action.name} className="action-card">
            <div className="action-header">
              <span className="action-icon">{action.icon}</span>
              <h4>{action.name}</h4>
            </div>
            <p className="action-description">{action.description}</p>
            <button
              className={action.class}
              disabled={!action.enabled || state.timers.isPaused}
              onClick={action.action}
            >
              {action.enabled ? 'Perform' : 'Not Available'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

What this does:

  • useGameContext gives you state and dispatch from a shared context.
  • actions is a small config that drives the UI — name, icon, enabled condition, and the dispatch to fire.
  • The Perform button is disabled if the action is not allowed or the game is paused (state.timers.isPaused).
  • This keeps the component pure: it does not compute business rules; it only checks simple conditions and dispatches events.

Reducer Transactions: Guards, Costs, and Immutable Updates

The reducer is the single source of truth for game logic. It validates preconditions (guards), applies costs, and updates multiple slices of state immutably. Below are the two cases we dispatch from the actions above.

src/context/gameReducer.js

JavaScript
    case 'SERVE_COFFEE': {
      if (
        state.resources.coffeeBeans < 1 ||
        state.resources.energy < 5 ||
        state.shop.queueLength < 1
      ) {
        return state;
      }

      const earnings = 5;

      return {
        ...state,
        resources: {
          ...state.resources,
          coffeeBeans: state.resources.coffeeBeans - 1,
          energy: state.resources.energy - 5,
          money: state.resources.money + earnings,
          satisfaction: Math.min(100, state.resources.satisfaction + 3)
        },
        shop: {
          ...state.shop,
          queueLength: Math.max(0, state.shop.queueLength - 1),
          customersServed: state.shop.customersServed + 1,
          ordersCompleted: state.shop.ordersCompleted + 1,
          revenue: state.shop.revenue + earnings
        }
      };
    }
    case 'RESTOCK_BEANS': {
      if (state.resources.money < 10) {
        return state;
      }
      return {
        ...state,
        resources: {
          ...state.resources,
          money: state.resources.money - 10,
          coffeeBeans: state.resources.coffeeBeans + 10
        }
      };
    }

Key takeaways:

  • Guards prevent invalid moves (e.g., not enough beans/energy or an empty queue).
  • Costs and rewards are applied in one atomic update.
  • Immutable updates use the spread operator to copy state, resources, and shop before changing fields. This is crucial for predictable renders and time-travel/debugging patterns.

Summary and Next Steps

In this unit, you built a config-driven action panel that dispatches events and a reducer that applies guarded, immutable updates across multiple state slices. Your component stayed clean and focused on rendering, while the reducer centralized game rules.

You are ready to practice by wiring more actions and strengthening your reducer logic. Let’s head to the practice section and make the café come alive.

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