DOM, Events, APIs, and Storage
Many JavaScript projects combine four browser skills:
- read and update the DOM
- respond to events
- fetch data from APIs
- save data in browser storage
The challenge is not knowing each skill separately.
The challenge is connecting them in a predictable way.
The Browser App Flow
A common flow looks like this:
User action -> event handler -> state update -> render -> optional save or API requestExample:
searchForm.addEventListener("submit", async (event) => {
event.preventDefault();
const query = searchInput.value.trim();
if (!query) {
showError("Enter a search term.");
return;
}
await searchRepositories(query);
});The event handler should be readable.
Move detailed logic into named functions.
DOM Rendering
Rendering means turning state into visible HTML.
function renderTasks(tasks) {
taskList.innerHTML = "";
for (const task of tasks) {
const li = document.createElement("li");
li.textContent = task.title;
if (task.completed) {
li.classList.add("completed");
}
taskList.append(li);
}
}For beginner projects, clearing and re-rendering a small list is usually fine.
For very large lists, you may need a more careful update strategy.
Event Delegation
If a list has many buttons, event delegation can be cleaner than attaching a listener to every button.
taskList.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button) {
return;
}
const taskId = button.dataset.taskId;
const action = button.dataset.action;
if (action === "delete") {
deleteTask(taskId);
}
if (action === "toggle") {
toggleTask(taskId);
}
renderTasks(state.tasks);
});The buttons can include data attributes:
<button data-action="delete" data-task-id="task-1">Delete</button>This is useful when list items are created dynamically.
API Integration
API projects need loading, success, and error states.
async function loadUsers() {
state.isLoading = true;
state.error = null;
render();
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
state.users = await response.json();
} catch (error) {
state.error = "Could not load users.";
} finally {
state.isLoading = false;
render();
}
}Notice the order:
- Show loading.
- Try the request.
- Store successful data or an error.
- Stop loading.
- Render the result.
Storage Integration
Storage projects need save and load boundaries.
const STORAGE_KEY = "project-tasks";
function saveTasks(tasks) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function loadTasks() {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : [];
}Call loadTasks() during startup.
Call saveTasks() after state changes that should persist.
function addTask(title) {
state.tasks = [
...state.tasks,
{ id: crypto.randomUUID(), title, completed: false }
];
saveTasks(state.tasks);
renderTasks(state.tasks);
}Forms and Validation
Validate input before changing state.
function getExpenseInput() {
const description = descriptionInput.value.trim();
const amount = Number(amountInput.value);
if (!description) {
return { error: "Description is required." };
}
if (!Number.isFinite(amount) || amount <= 0) {
return { error: "Amount must be greater than 0." };
}
return {
value: {
description,
amount
}
};
}This keeps validation separate from state updates.
Best Practices
- Keep event handlers short and readable.
- Render from state, not from guesses about the DOM.
- Use loading, error, empty, and success states for API projects.
- Validate forms before updating state.
- Save to storage after successful state changes.
- Use event delegation for dynamic lists.
Common Mistakes
- Calling
response.json()withoutawait. - Forgetting to handle failed API responses.
- Saving invalid form input.
- Updating the DOM but not the state.
- Updating state but forgetting to render.
- Adding duplicate event listeners every time the UI renders.
Summary
Browser projects connect events, state, rendering, APIs, and storage.
Keep the flow clear: handle the user action, validate input, update state, save or fetch when needed, and render the current state.