A query string is the part of a URL after the ?. It carries extra information for the page or the server, such as a search term, a filter, or a page number, as key=value pairs joined by &. It runs to the end of the URL, or to a #, which starts the fragment, the part that points to a place within the page. Everything in it is text: page=2 carries the string "2". Here's a shop's search URL, https://shop.example/search?q=fish+%26+chips&tag=sale&tag=new&page=2, taken apart:
| Pair | Key | Value |
|---|---|---|
q=fish+%26+chips | q | fish & chips. Each + stands for a space, and %26 is an & that belongs to the value. |
tag=sale | tag | sale |
tag=new | tag | new. A key can appear more than once; that's a common way to send a list. |
page=2 | page | 2, as text |
The value fish & chips couldn't go in as it is: its & would be read as the start of a new pair, and a URL can't contain spaces. A # in a value would end the query too. Percent-encoding writes such a character as % followed by two hexadecimal digits, the value of its byte: a space is byte 32, or 20 in hex, so it becomes %20. A character outside ASCII (the basic English letters, digits, and symbols) is first turned into its bytes in UTF-8, the standard text encoding, so é, which takes two bytes, becomes %C3%A9. A % itself becomes %25. JavaScript's encodeURIComponent encodes one key or value this way, and decodeURIComponent reverses it:
encodeURIComponent("fish & chips"); // "fish%20%26%20chips"
encodeURIComponent("1+1=2"); // "1%2B1%3D2"
encodeURIComponent("café"); // "caf%C3%A9"
decodeURIComponent("caf%C3%A9"); // "café"
decodeURIComponent("fish+%26+chips"); // "fish+&+chips": it leaves + alone
Spaces have a second spelling. When an HTML form sends its fields in a query string, it writes each space as +, and most code that reads query strings, URLSearchParams included, reads every + as a space. So a real plus sign has to travel as %2B. encodeURIComponent writes spaces as %20, which every reader understands.
You meet query strings in search pages, filters, pagination, and tracking links, and you add them to fetch requests. Browsers and Node.js have URLSearchParams, also available as url.searchParams on a URL object, which reads and writes them for you.
Write createRouter(routes), the part of a single-page app's router that decides which page a URL shows. routes is an array of { path, handler } objects. It returns an object with one method, resolve(url).
Paths are compared segment by segment, splitting on /. Empty segments don't count, so /users/42, /users/42/, and users/42 are the same path, and / and "" both have no segments.
In a route's path, a plain segment such as users matches only the same text. A segment that starts with :, such as :id, matches any one segment and captures it as a param named id. Params are decoded with decodeURIComponent.
A * as the last segment of a route's path matches the rest of the URL's path, even when nothing is left. It captures the rest as params["*"]: the remaining segments joined with /, not decoded.
resolve(url) ignores everything from the first ? or # in the URL. It tries the routes in order and calls the handler of the first one that matches as handler(params), where params is an object of the captured params ({} when there are none). It returns what the handler returns, or null when no route matches.
const router = createRouter([
{ path: "/", handler: () => "home" },
{ path: "/users/new", handler: () => "new user form" },
{ path: "/users/:id", handler: ({ id }) => "user " + id },
{ path: "/files/*", handler: (params) => "file " + params["*"] },
{ path: "*", handler: () => "not found" },
]);
router.resolve("/"); // "home"
router.resolve("/users/new"); // "new user form"
router.resolve("/users/42?tab=posts"); // "user 42"
router.resolve("/users/ada%20l/"); // "user ada l"
router.resolve("/files/docs/a.txt"); // "file docs/a.txt"
router.resolve("/users/42/edit"); // "not found"
createRouter([]).resolve("/"); // null
const router = createRouter([{ path: "/users/:id", handler: (params) => params }]);
expect(router.resolve("/users/42")).toEqual({ id: "42" });
const router = createRouter([{ path: "/posts/:postId/comments/:commentId", handler: (params) => params }]);
expect(router.resolve("/posts/7/comments/99")).toEqual({ postId: "7", commentId: "99" });
const router = createRouter([{ path: "/users/:id", handler: () => "user" }]);
expect(router.resolve("/users/ada")).toBe("user");
expect(router.resolve("/users")).toBeNull();
expect(router.resolve("/users/ada/edit")).toBeNull();
expect(router.resolve("/people/ada")).toBeNull();
expect(router.resolve("/admin/users/ada")).toBeNull();
const router = createRouter([{ path: "/users/:id", handler: (params) => params }]);
expect(router.resolve("/users/42?tab=posts")).toEqual({ id: "42" });
expect(router.resolve("/users/42#bio")).toEqual({ id: "42" });
expect(router.resolve("/users/42/?tab=posts#bio")).toEqual({ id: "42" });
const router = createRouter([
{ path: "/", handler: () => "home" },
{ path: "/about/", handler: () => "about" },
]);
expect(router.resolve("/")).toBe("home");
expect(router.resolve("")).toBe("home");
expect(router.resolve("/about")).toBe("about");
expect(router.resolve("about/")).toBe("about");
expect(router.resolve("//about")).toBe("about");
const routes = [
{ path: "/users/new", handler: () => "new user form" },
{ path: "/users/:id", handler: ({ id }) => "user " + id },
];
expect(createRouter(routes).resolve("/users/new")).toBe("new user form");
expect(createRouter([...routes].reverse()).resolve("/users/new")).toBe("user new");
const router = createRouter([
{ path: "/files/*", handler: (params) => params },
{ path: "*", handler: () => "fallback" },
]);
expect(router.resolve("/files/docs/a.txt")).toEqual({ "*": "docs/a.txt" });
expect(router.resolve("/files/my%20docs/a.txt/")).toEqual({ "*": "my%20docs/a.txt" });
expect(router.resolve("/files")).toEqual({ "*": "" });
expect(router.resolve("/images/a.png")).toBe("fallback");
expect(router.resolve("/")).toBe("fallback");
const router = createRouter([{ path: "/tags/:tag", handler: ({ tag }) => tag }]);
expect(router.resolve("/tags/caf%C3%A9%20au%20lait")).toBe("café au lait");
expect(router.resolve("/tags/a%2Fb")).toBe("a/b");
const handler = spy(() => 0);
const router = createRouter([{ path: "/about", handler }]);
expect(router.resolve("/about")).toBe(0);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({});
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Client-side router. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.