The little icon in the browser tab is just a <link rel="icon"> in the document <head>, and single-page apps sometimes want to change it at runtime — a red dot when there are unread messages, a different logo per workspace, a "recording" indicator. useFavicon does that imperatively: give it a URL and it points the tab's icon there, creating the <link> if the page doesn't already have one.
Implement useFavicon(href). In an effect, find the existing <link rel="icon"> (or create and append one), then set its href to the given URL. Re-run when href changes, and reuse the same element rather than piling up new <link> tags.
function useFavicon(href) {
// side-effect only; returns nothing.
}
function Inbox({ unread }) {
useFavicon(unread ? '/favicon-alert.png' : '/favicon.png');
// tab icon swaps as `unread` flips
}
useFavicon('/logo.png'); // creates <link rel="icon" href="/logo.png"> if absent
head for an existing link[rel~='icon']; only build a new one if none is there.href changes, mutate the same <link>'s href; don't append a fresh link each time or the head fills with duplicates.useEffect, keyed on href.You'll run an effect that finds the page's <link rel="icon"> — creating and appending one if it's missing — and sets its href, re-running whenever the URL changes.
The favicon isn't React state; it's a DOM node in <head> that the browser reads to paint the tab icon. To change it at runtime you reach into the DOM imperatively: locate the icon <link>, or make one if the page never declared it, and update its href. The one trap is reuse — a new URL should update the existing link, not append another, or you'll leave a trail of stale <link rel="icon"> tags in the head.
There's one slot in the head for the tab icon. Your job each time href changes: get a handle to that slot's <link> (find it, or create-and-insert it once), then write the new URL into it. Because it's a DOM side effect, it lives in useEffect keyed on href; because you always look the element up (or reuse the one you made), you never create duplicates.
The quick version creates a link every time:
function useFaviconNaive(href) {
useEffect(() => {
const link = document.createElement('link');
link.rel = 'icon';
link.href = href;
document.head.appendChild(link); // a NEW link on every href change
}, [href]);
}
This "works" visually — the browser tends to honor the last icon link — but it appends a fresh <link rel="icon"> on every href change and never removes the old ones, so the head accumulates dead tags. It also ignores any favicon the page shipped with in its HTML. The fix is to look for an existing icon link first and reuse it.
const { useEffect } = require('react');
function useFavicon(href) {
useEffect(() => {
if (!href) return; // nothing to point at
// Reuse an existing icon link, or create one exactly once.
let link = document.head.querySelector("link[rel~='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.setAttribute('href', href);
}, [href]);
}
module.exports = { useFavicon };
The effect is keyed on href, so it runs on mount and whenever the URL changes. It queries head for link[rel~='icon'] — the ~= matches rel values like "shortcut icon" too — and only builds a new <link rel="icon"> when the page has none, appending it a single time. Then it sets href on that one element. Every later change finds the same link (whether the one you created or the one in the page's HTML) and just rewrites its href, so the head never accumulates duplicate icon tags. The early return on an empty href avoids creating an icon that points nowhere.
Mount useFavicon('/inbox.png') on a page whose HTML already has <link rel="icon" href="/default.png">, then flip href to /inbox-alert.png:
querySelector finds the page's existing <link rel="icon">. No new element is created. setAttribute('href', '/inbox.png') — the tab icon becomes /inbox.png./inbox-alert.png — dependency changed, effect re-runs; querySelector returns the same link; its href is rewritten to /inbox-alert.png. Still one icon link in the head.querySelector returns null, so the effect creates a <link rel="icon">, appends it once, and sets its href; subsequent changes reuse it.rel~='icon' — a plain rel='icon' selector misses "shortcut icon"; the ~= attribute selector matches either.useEffect, not the render body, so it runs after commit and honors href changes.href="" points the icon at the current page and can trigger a spurious request.useDocumentTitle's restore.href gives per-view icons with no image files.document is undefined on the server; in a real app guard the DOM access, though effects only run in the browser so the body is already client-only.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
The little icon in the browser tab is just a <link rel="icon"> in the document <head>, and single-page apps sometimes want to change it at runtime — a red dot when there are unread messages, a different logo per workspace, a "recording" indicator. useFavicon does that imperatively: give it a URL and it points the tab's icon there, creating the <link> if the page doesn't already have one.
Implement useFavicon(href). In an effect, find the existing <link rel="icon"> (or create and append one), then set its href to the given URL. Re-run when href changes, and reuse the same element rather than piling up new <link> tags.
function useFavicon(href) {
// side-effect only; returns nothing.
}
function Inbox({ unread }) {
useFavicon(unread ? '/favicon-alert.png' : '/favicon.png');
// tab icon swaps as `unread` flips
}
useFavicon('/logo.png'); // creates <link rel="icon" href="/logo.png"> if absent
head for an existing link[rel~='icon']; only build a new one if none is there.href changes, mutate the same <link>'s href; don't append a fresh link each time or the head fills with duplicates.useEffect, keyed on href.You'll run an effect that finds the page's <link rel="icon"> — creating and appending one if it's missing — and sets its href, re-running whenever the URL changes.
The favicon isn't React state; it's a DOM node in <head> that the browser reads to paint the tab icon. To change it at runtime you reach into the DOM imperatively: locate the icon <link>, or make one if the page never declared it, and update its href. The one trap is reuse — a new URL should update the existing link, not append another, or you'll leave a trail of stale <link rel="icon"> tags in the head.
There's one slot in the head for the tab icon. Your job each time href changes: get a handle to that slot's <link> (find it, or create-and-insert it once), then write the new URL into it. Because it's a DOM side effect, it lives in useEffect keyed on href; because you always look the element up (or reuse the one you made), you never create duplicates.
The quick version creates a link every time:
function useFaviconNaive(href) {
useEffect(() => {
const link = document.createElement('link');
link.rel = 'icon';
link.href = href;
document.head.appendChild(link); // a NEW link on every href change
}, [href]);
}
This "works" visually — the browser tends to honor the last icon link — but it appends a fresh <link rel="icon"> on every href change and never removes the old ones, so the head accumulates dead tags. It also ignores any favicon the page shipped with in its HTML. The fix is to look for an existing icon link first and reuse it.
const { useEffect } = require('react');
function useFavicon(href) {
useEffect(() => {
if (!href) return; // nothing to point at
// Reuse an existing icon link, or create one exactly once.
let link = document.head.querySelector("link[rel~='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.setAttribute('href', href);
}, [href]);
}
module.exports = { useFavicon };
The effect is keyed on href, so it runs on mount and whenever the URL changes. It queries head for link[rel~='icon'] — the ~= matches rel values like "shortcut icon" too — and only builds a new <link rel="icon"> when the page has none, appending it a single time. Then it sets href on that one element. Every later change finds the same link (whether the one you created or the one in the page's HTML) and just rewrites its href, so the head never accumulates duplicate icon tags. The early return on an empty href avoids creating an icon that points nowhere.
Mount useFavicon('/inbox.png') on a page whose HTML already has <link rel="icon" href="/default.png">, then flip href to /inbox-alert.png:
querySelector finds the page's existing <link rel="icon">. No new element is created. setAttribute('href', '/inbox.png') — the tab icon becomes /inbox.png./inbox-alert.png — dependency changed, effect re-runs; querySelector returns the same link; its href is rewritten to /inbox-alert.png. Still one icon link in the head.querySelector returns null, so the effect creates a <link rel="icon">, appends it once, and sets its href; subsequent changes reuse it.rel~='icon' — a plain rel='icon' selector misses "shortcut icon"; the ~= attribute selector matches either.useEffect, not the render body, so it runs after commit and honors href changes.href="" points the icon at the current page and can trigger a spurious request.useDocumentTitle's restore.href gives per-view icons with no image files.document is undefined on the server; in a real app guard the DOM access, though effects only run in the browser so the body is already client-only.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.