Lesson 28 of 54

Variables and State Management

Minimizing Variable Scope

The larger a variable's scope, the more things can go wrong.

A global variable can be modified by any code anywhere in the program. A local variable can only be modified within a few lines. The local variable is dramatically easier to reason about.

Minimizing scope is about reducing cognitive load: the less context you need to understand a variable, the easier the code is to read and debug.

The Principle

A variable should be declared in the smallest possible scope and as close as possible to its first use.

Why Close to First Use?

Javascript
// BAD: Declaration far from use
function processOrder(order) {
  let discount = 0;           // Declared here
  let shippingCost = 0;
  let taxRate = 0;
  
  validateOrder(order);        // 20 lines of code
  calculateItems(order);       // ...
  applyPromotions(order);      // ...
  
  discount = calculateDiscount(order);  // Used here, 20 lines later
  // ...
}

// GOOD: Declaration close to use
function processOrder(order) {
  validateOrder(order);
  calculateItems(order);
  applyPromotions(order);
  
  const discount = calculateDiscount(order);  // Declared where used
  // ...
}

When the declaration is close to use, the reader doesn't have to scroll up to understand the variable.

Block Scope: let and const

JavaScript has three ways to declare variables, each with different scope:

var: Function Scope (Avoid)

Javascript
function example() {
  if (true) {
    var x = 1;  // Hoisted to function scope
  }
  console.log(x);  // 1 - x leaks out of the block
}

let and const: Block Scope (Prefer)

Javascript
function example() {
  if (true) {
    let x = 1;   // Block-scoped
    const y = 2;
  }
  console.log(x);  // ReferenceError - x doesn't exist here
}

Rule: Use const by default. Use let only when you need to reassign. Never use var.

Eliminate Global Variables

Global variables are the largest possible scope. They can be accessed and modified from anywhere, making bugs hard to trace.

The Problem

Javascript
// globals.js
let currentUser = null;
let settings = {};

// auth.js
function login(user) {
  currentUser = user;  // Modifies global
}

// settings.js
function updateSettings(newSettings) {
  settings = { ...settings, ...newSettings };  // Modifies global
}

// somewhere.js
function doSomething() {
  if (currentUser) {  // Reading global - who set this? When?
    // ...
  }
}

When something goes wrong with currentUser, you have to search the entire codebase.

The Solution: Explicit Dependencies

Javascript
// userService.js
export class UserService {
  private currentUser: User | null = null;
  
  login(user: User) {
    this.currentUser = user;
  }
  
  getCurrentUser(): User | null {
    return this.currentUser;
  }
}

// somewhere.js
function doSomething(userService: UserService) {
  const user = userService.getCurrentUser();
  if (user) {
    // ...
  }
}

Now:

  • The dependency is explicit
  • State is encapsulated
  • Testing is easy (inject a mock)

Module Scope for Singletons

Sometimes you need shared state. Module scope is better than global scope:

Javascript
// config.js
let config = null;

export function initConfig(settings) {
  if (config) throw new Error('Config already initialized');
  config = Object.freeze(settings);
}

export function getConfig() {
  if (!config) throw new Error('Config not initialized');
  return config;
}

The state is hidden inside the module. Only initConfig and getConfig can access it.

Loop Variables

Declare loop variables in the smallest scope possible:

Javascript
// BAD: Variable scope too large
let i;
for (i = 0; i < items.length; i++) {
  process(items[i]);
}
// i is still accessible here - but why would you need it?

// GOOD: Variable scoped to loop
for (let i = 0; i < items.length; i++) {
  process(items[i]);
}
// i is not accessible here

Reducing Scope in Practice

Before: Wide Scope

Javascript
function processOrders(orders) {
  let totalRevenue = 0;
  let totalItems = 0;
  let processedOrders = [];
  let errors = [];
  
  for (const order of orders) {
    try {
      const result = processOrder(order);
      processedOrders.push(result);
      totalRevenue += result.total;
      totalItems += result.items.length;
    } catch (e) {
      errors.push({ order, error: e });
    }
  }
  
  return { processedOrders, totalRevenue, totalItems, errors };
}

All variables declared at the top, modifiable throughout the function.

After: Narrow Scope

Javascript
function processOrders(orders) {
  const results = orders.map(order => {
    try {
      return { success: true, result: processOrder(order) };
    } catch (error) {
      return { success: false, order, error };
    }
  });
  
  const processedOrders = results
    .filter(r => r.success)
    .map(r => r.result);
  
  const errors = results
    .filter(r => !r.success);
  
  const totalRevenue = processedOrders
    .reduce((sum, o) => sum + o.total, 0);
  
  const totalItems = processedOrders
    .reduce((sum, o) => sum + o.items.length, 0);
  
  return { processedOrders, totalRevenue, totalItems, errors };
}

Each variable is derived in a focused way. No mutation, no wide scope.

The "Live Time" Metric

"Live time" is the number of lines between a variable's first and last use. Minimize it.

Javascript
// BAD: Long live time
const multiplier = getMultiplier();  // Line 1
// ... 50 lines of other code ...
const result = value * multiplier;   // Line 52

// GOOD: Short live time
// ... 50 lines of other code ...
const multiplier = getMultiplier();  // Line 51
const result = value * multiplier;   // Line 52

Short live time means less context to track.

Span: Keep References Close

If you use a variable multiple times, cluster those uses:

Javascript
// BAD: Uses spread throughout function
const config = loadConfig();
doA();
useConfig(config);  // First use
doB();
doC();
useConfig(config);  // Second use, 5 lines later
doD();
useConfig(config);  // Third use, 7 lines later

// BETTER: Cluster uses
const config = loadConfig();
useConfig(config);
useConfig(config);
useConfig(config);
doA();
doB();
doC();
doD();

Key insight: Small scope means less to track. Declare variables close to where you use them. Prefer const and let over var. Avoid globals—pass dependencies explicitly. The less a variable can be accessed, the easier it is to understand.