feat(ui): implement deep linking in CodeTabs via URL hash - #9159
moshams272 wants to merge 5 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ovflowd
left a comment
There was a problem hiding this comment.
We want to avoid having the CodeTabs being a client-component in all cases. We could use a wrapper that uses use client specifically for when these groupIds or whatnot are passed, ands we can do the logic of what gets used on shiki I guess?
That said per the repo guidelines it is an absolute no-go using winodw APIs from within components, not to mention adding so many event listeners (1 per CodeTab) is going to be an absolute nightmare for the browser.
Got it, I saw the I think Augustin's bug happened because of the React Hydration Error in the |
We might need to revisit why BaseCodeBox uses "use client" but still we should thrive for reducing client components as much as possible :) |
I reached for a fair solution. We use It's an initial thinking for now. |
In terms of should window APIs be behind safe Hooks, yes. But in terms of, should we have god knows how many Event Listeners because they're tied to one CodeBox? No. You might want to think of a React Context Provider, with one hook that keeps an eye on Hash changes. Or you might defer it to a HoC, so that on the apps, such as Next.js and doc-kit we defer the implementation of this, for example, Next.js has a Router/Navigation API, you could inject results on the CodeBox... I guess it's more of a thinking of "who should be responsible for listening to this" you could argue it should be the Component itself, because he's the one interested on this, that said, it's only interested on the scenarios where it should even care abou that. IMO, the Component should subscribe to the changes aka ask to the parent component or context provider "has the hash changed?" ... yet that the responsibility of actually setting up the Event Listener shouldn't be part of the Component's responsibility.... As "Window" or even a Hook that "listens to Window" is something unbeknown to the Component. The Component's "world" should always be anything that is inside said Component. That's a very common mistake some React Components do on, for example, Modals. Where they let the Modal itself be the one observing if anyone clicked outside the Modal, whereas that should never be the Modal's responsibility. So think a bit of how to approach this, both in a way that is properly designed and that can be reused (ie: if this is a prop/callback to the Component, you defer the implementation details (be it window APIs, be it Next.js APIs...) to the consumer) and how to make it reliable and performant. I hope my explanation helps! |
Screencast.from.2026-09-16.02-04-21.webmI hope I did it right :) |
|
that seem cool but on safari and vercel preview I cannot having them working |
Yeah, I'll summarize what I did:
So we haven't been added To actually see it works, a |
| import { useSyncExternalStore } from 'react'; | ||
|
|
||
| const listeners = new Set<() => void>(); | ||
| let isListening = false; |
There was a problem hiding this comment.
nit: can we not use a top level let? Use React APIs such a memo() or cache()
|
|
||
| import { useSyncExternalStore } from 'react'; | ||
|
|
||
| const listeners = new Set<() => void>(); |
There was a problem hiding this comment.
Same here, listeners itself should be a top-level cache, not a top level const
| }; | ||
| }; | ||
|
|
||
| const getSnapshot = () => |
There was a problem hiding this comment.
We shouldn't use window APIs on ui-components -- use context providers that UI Components use (I guess there's a global one? Like client-context or server-context?) -- otherwise probably have that being passed? I'm unsure if UI components should have environment specific wars. Maybe instead of window.location use at least globalThis or self or have Location be passed from downstream?
There was a problem hiding this comment.
Ah you already have a Provider, have the Provider have a prop of location={Location}
There was a problem hiding this comment.
(and ensure doc-kit and Next.js can pass that with their Navigation/Router APIs natively, or instead of Location they pass History, idk
|
|
||
| import type { FC, PropsWithChildren } from 'react'; | ||
|
|
||
| export const HashProvider: FC<PropsWithChildren> = ({ children }) => { |
There was a problem hiding this comment.
nit: Have Location be passed to the HashProvider ;)
| }, [rawLanguages, rawDisplayNames]); | ||
| }, [rawLanguages, rawDisplayNames, groupId]); | ||
|
|
||
| const Component = groupId ? CodeTabsWithHash : CodeTabs; |
There was a problem hiding this comment.
This shouldn't be decided here IMO, but on our rehype shiki plugin. The name of the component to be used (CodeTabs) is passed there, so we can do the right pass directly from there, which reduces verbosity. This also removes the need of regular CodeTabs having an optional Group ID param
| return { | ||
| key: `${language}-${index}`, | ||
| label, | ||
| id: |
| const [activeTab, setActiveTab] = useState(defaultValue); | ||
| const [prevHash, setPrevHash] = useState(hash); | ||
|
|
||
| if (hash !== prevHash) { |
There was a problem hiding this comment.
This shouldn't be on the root of a Component, that's a big no-go. Use an Effect.

Description
Implement deep linking in
CodeTabscomponent by using the active tab state via the URL hash.In Details
groupIdprop to generate unique anchor IDs across multiple CodeTabs instances on the same page in format:tab-{groupId}-{language}-{index}, falling back to React'suseId()when groupId is not provided.useState()to set the correct initial tab without a double render.history.replaceStateto avoid polluting the browser historyValidation
After building, I checked the tabs by clicking on them, and then the URL hash changed.
Related Issues
Fixes #9140
Check List
pnpm formatto ensure the code follows the style guide.pnpm testto check if all tests are passing.pnpm buildto check if the website builds without errors.