Routing is the job of reading the URL and deciding which view to render. In this lesson we implement both a hash router and a History API router with nothing but JavaScript. Start with the core hash router, then treat History API and navigation guards as optional extensions once the basics feel solid.
Key terms
- Route: An object that pairs a URL with the component that should render for it.
- Hash router: A simple router that reads the hash fragment such as
#hometo swap views. - History API: Browser APIs like
pushState,replaceState, andpopstatethat let you control the address bar. - Navigation guard: A function that runs before entering a route to allow or block access.
- popstate event: Fires when users press Back/Forward so you can re-render the current URL.
Core ideas
- Route (URL-to-view mapping): An object that binds a URL to a component or render function.
- Hash router (
#-based navigation): Switches views using the value after#. Requires no server config, great for static hosting. - History API (address bar control): Uses
pushStateand thepopstateevent to change URLs without reloading, keeping clean paths. - Navigation guard (entry condition): Runs checks such as authentication or unsaved changes before letting the user in.
Visualizing the routing flow
nav[User click]
router[Router]
guard[Navigation guard]
view[Component renderer]
history[History/hash state]
nav -> router: "Link click"
router -> guard: "Guard check"
guard -> router: "Allow or block"
router -> history: "pushState/hash"
router -> view: "renderRoute()"
history -> router: "popstate/hashchange"
Read the diagram as “user clicks → router inspects the URL → ask the guard if needed → update the URL and render → use popstate/hashchange to render again when going back.” Every router follows this order.
Building the router: code examples
Start with the hash router that swaps the view based on the hash fragment. Before writing real code, quickly see when location.hash changes.
window.addEventListener("hashchange", () => console.log(location.hash));
location.hash = "#tasks";
➡️ Remember that changing the hash triggers an event without a full reload, and the implementation below will feel natural.
const routes = {
home: () => `<section><h2>Home</h2><p>Welcome!</p></section>`,
tasks: () => `<section><h2>Tasks</h2><div id="tasks-root"></div></section>`,
stats: () => `<section><h2>Stats</h2><div id="stats-root"></div></section>`,
};
const outlet = document.querySelector("#app");
if (!outlet) {
throw new Error("Cannot find the #app container.");
}
function renderRoute(name) {
const view = routes[name];
outlet.innerHTML = view ? view() : `<p>That page does not exist.</p>`;
}
function handleHashChange() {
const hash = location.hash.replace("#", "") || "home";
renderRoute(hash);
}
window.addEventListener("hashchange", handleHashChange);
handleHashChange();
Hash routers take only a few lines. Each time the URL changes, hashchange fires. In other words, “a route equals a hash string plus a template function.”
Next up is the History API router, which keeps the address bar in sync without reloading the entire page.
const historyRoutes = [
{ path: "/", render: () => `<h2>Home</h2>` },
{ path: "/tasks", render: () => `<h2>Tasks</h2>` },
{
path: "/settings",
guard: () => confirm("Move to the settings page?"),
render: () => `<h2>Settings</h2>`,
},
];
function matchRoute(pathname) {
return historyRoutes.find((route) => route.path === pathname);
}
function navigate(pathname) {
const route = matchRoute(pathname);
if (!route) {
outlet.innerHTML = `<p>404 Not Found</p>`;
return;
}
if (route.guard && !route.guard()) return;
history.pushState({}, "", pathname);
outlet.innerHTML = route.render();
}
window.addEventListener("popstate", () => {
const route = matchRoute(location.pathname);
outlet.innerHTML = route ? route.render() : `<p>404 Not Found</p>`;
});
document.querySelectorAll("[data-link]").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const target = event.currentTarget;
if (!(target instanceof HTMLAnchorElement)) return;
navigate(target.getAttribute("href") ?? "/");
});
});
The History API router updates the address bar without a full refresh. The popstate event fires when the user presses Back/Forward.
Add loading feedback to show progress between route changes.
const loadingBar = document.querySelector(".router-progress");
async function navigateWithLoading(pathname) {
loadingBar?.classList.add("is-active");
await navigate(pathname);
loadingBar?.classList.remove("is-active");
}
Loading indicators help users stay patient during transitions. Treat this as an optional enhancement once the base navigation is stable.
Why it matters
- Thoughtful URLs make bookmarking, social sharing, and search indexing easier.
- Framework routers abstract hash or History routers; understanding the baseline helps you adapt quickly elsewhere.
- Writing your own navigation guards unlocks login flows, gated dashboards, and similar access control features.
Practice
- Core: Implement a hash router and connect
#home,#tasks, and#statsviews. - Extension: Build the History API version and bind navigation links via the
data-linkattribute. - Debugging: Ensure invalid paths render a 404 view and log when
popstatefires. - Done: Both routers switch at least three views correctly, and guards block navigation based on your condition.
Wrap-up
Routing knowledge lets you design page structures freely. Next time we revisit performance fundamentals so even large screens stay fast through measurement and tuning.
💬 댓글
이 글에 대한 의견을 남겨주세요