Basic State Management

Course: JavaScript Intermediate

Lesson 19: Basic State Management

Level

Intermediate

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Track application state in a JavaScript object
  • Update state through dedicated functions rather than direct mutation
  • Trigger a UI re-render whenever state changes

Frontend Development Context

State is the data that determines what the user sees at any given moment — which tab is active, whether a modal is open, how many items are in the cart, what the current search query is. Managing this data in a predictable, organized way is one of the most important skills in frontend development.

Before you use React's useState, Vue's reactive, or any external library like Redux, it helps to understand the underlying concept: state is a central object, changes go through controlled update functions, and the UI is re-drawn whenever state changes. This mental model is the foundation of every state management approach you will encounter in modern frontend frameworks.

Explanation

The simplest form of state management is storing all UI-relevant data in one object (the state object) and writing functions that update specific parts of it. When state changes, a render function reads the current state and redraws the relevant part of the UI to match.

Separating reads (what the UI shows) from writes (how state changes) keeps your code organized. Instead of scattering changes to individual DOM elements throughout your code, you funnel all changes through state, then let the render function handle the DOM. This is called a unidirectional data flow.

Rendering from state means the DOM always reflects exactly what is in the state object. When you add an item, you update state and re-render. When you remove an item, you update state and re-render. The render function does not need to know what changed — it just re-reads state and rebuilds the UI.

Pure re-renders (clearing and rebuilding the container on each change) are simple and sufficient for small apps. For larger apps, frameworks optimize this with a virtual DOM or reactive data bindings, but the concept is identical.

Keeping state in one place also makes debugging easier — you can inspect the state object at any point to see exactly what the app thinks is true.

Code Example

This example manages a simple task list with state: an array of tasks, a filter setting, and an item count. The UI always reflects the current state.

// Centralized state object
const state = {
  tasks: [],
  filter: "all",    // "all" | "active" | "completed"
  nextId: 1
};

// State update functions — all changes go through these
function addTask(text) {
  state.tasks.push({ id: state.nextId++, text, completed: false });
  render();
}

function toggleTask(id) {
  const task = state.tasks.find(t => t.id === id);
  if (task) {
    task.completed = !task.completed;
    render();
  }
}

function setFilter(filter) {
  state.filter = filter;
  render();
}

// Derived data — computed from current state
function getVisibleTasks() {
  if (state.filter === "active") return state.tasks.filter(t => !t.completed);
  if (state.filter === "completed") return state.tasks.filter(t => t.completed);
  return state.tasks;
}

// Render function — reads state, rebuilds UI
function render() {
  const list = document.querySelector("#taskList");
  const count = document.querySelector("#taskCount");
  const visible = getVisibleTasks();

  list.innerHTML = "";
  visible.forEach(task => {
    const li = document.createElement("li");
    li.textContent = task.text;
    li.style.textDecoration = task.completed ? "line-through" : "none";
    li.addEventListener("click", () => toggleTask(task.id));
    list.appendChild(li);
  });

  const remaining = state.tasks.filter(t => !t.completed).length;
  count.textContent = `${remaining} tasks remaining`;
}

// Initial render and test data
addTask("Learn state management");
addTask("Build a todo app");
addTask("Read about React useState");
toggleTask(1); // mark first task as complete
setFilter("active"); // show only active tasks

Code Explanation

state is a single object holding everything the UI needs: the task list, the active filter, and a counter for generating unique IDs. Having one place for all data makes the app predictable — if the UI looks wrong, you inspect state and find the source of truth.

addTask, toggleTask, and setFilter are the only functions that modify state. After each change, they call render(). This ensures the UI is always in sync with the data — you never have to worry about updating the DOM manually in multiple places.

getVisibleTasks computes a derived value from state — which tasks to show based on the current filter. Computed/derived values are not stored in state; they are recalculated on every render from the raw state data.

render() clears #taskList and rebuilds it entirely from the current state. It attaches click listeners to each item by calling toggleTask with the task's ID. While rebuilding the entire DOM on every change is not optimal for large lists, it is simple, correct, and easy to understand.

Pattern Highlights

Positive Pattern: Route all state changes through dedicated update functions that call render(). This makes every state transition intentional and traceable — you can add logging inside each function to track changes.

⚠️ Neutral Note: A full re-render on every state change is simple and sufficient for small UIs. As the app grows, you can optimize by only updating the parts of the DOM that actually changed — which is exactly what React and other frameworks do.

Negative Pattern: Do not update the DOM directly in response to events without going through state. Bypassing state creates drift between what the UI shows and what the state says, making bugs very hard to reproduce.

Common Mistakes

Mistake 1: Updating the DOM directly instead of updating state and re-rendering

Changing the UI immediately in an event handler without updating state.

button.addEventListener("click", () => {
  document.querySelector("#count").textContent = "1 task remaining"; // hardcoded!
});

The DOM is now out of sync with state. The next render will overwrite this with whatever state actually says. Always update state first, then let render() handle the DOM.

Mistake 2: Duplicating state in the DOM

Reading data back from the DOM to determine what to do next.

const currentCount = parseInt(document.querySelector("#count").textContent);
// Using DOM as the source of truth instead of state

The DOM should be a reflection of state, not the source of truth. Read your data from the state object, not from DOM element text or attributes.

Mistake 3: Storing derived values in state instead of computing them

Saving a computed result (like a count) in state and trying to keep it in sync manually.

state.remainingCount = 3; // stored in state

function toggleTask(id) {
  // now you have to remember to update state.remainingCount too
}

Values that can be calculated from other state (like remaining count) should be computed in the render or a helper function, not stored separately. Stored derived values get out of sync.

Quick Recap

  • Keep all UI-relevant data in one central state object so there is a single source of truth
  • Update state through dedicated functions, then call a render function to sync the UI to the new state
  • Derived values (counts, filtered lists) should be calculated at render time, not stored in state
  • The render function should always rebuild the UI entirely from state — not patch it based on assumptions about what changed

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01