Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR-safe ID generation #7517

Open
illright opened this issue May 8, 2022 · 10 comments
Open

SSR-safe ID generation #7517

illright opened this issue May 8, 2022 · 10 comments

Comments

@illright
Copy link

illright commented May 8, 2022

Describe the problem

There are many cases where an application needs to connect DOM nodes with IDs:

  • form fields and labels (for)
  • accessible names (aria-labelledby)
  • more presented here

Given that these IDs do not need any semantic meaning, and given that humans are pretty terrible at coming up with app-level unique IDs, this is a job wonderfully suited for a framework. Taking SSR into consideration, we cannot simply use counter-based solutions or randomly generated IDs.

Describe the proposed solution

React 18 shipped its new useId() hook which allows utilizing all the component tree knowledge of the framework and using that to generate IDs that are stable across SSR and unique across component instances.

I would like to see a similar thing in Svelte. It could look like this (createInstanceId):

<!-- sign-in-form.svelte -->
<script lang="ts">
  import { createInstanceId } from 'svelte';
  
  const id = createInstanceId();
  const usernameID = `${id}-username`;
  const passwordID = `${id}-password`;
</script>

<label for={usernameID}>Username</label>
<input id={usernameID} />

<label for={passwordID}>Password</label>
<input id={passwordID} type="password" />

Alternatives considered

Variable

The ID could come from a compiler-populated instance-specific variable:

<label for={$$id}>Username</label>
<input id={$$id} />

Another name could be $$instanceId, to reflect its instance-dependency. This isn't perfect though, as it still doesn't communicate clearly enough that this isn't a component-specific variable, but rather an instance-specific one. A function call communicates that much better, similarly to createEventDispatcher()

Userland solution

I have used naive ID-generation solutions in the past (namely, lukeed/uid) and they didn't seem to cause any visible issues, but it's very likely that I simply haven't run into a use-case where a mismatch between the client and the server would be a problem.

Other userland solutions involve contexts, which is not great for developer experience, and might be problematic for asynchronous rendering (see reactwg/react-18#111 (reply in thread))

Importance

would make my life easier

Relevant mentions

#6932

@CanRau
Copy link

CanRau commented Oct 12, 2022

Yes please was just looking for this as forms might be used more than once on the same page.
I'm using SvelteKit.

@kevmodrome
Copy link
Contributor

To solve this you could use SubtleCrypto.digest() and create a hash that would be unique. As data you could pass in an object, the props that you take into a component, or something else that you know is unique.

@illright
Copy link
Author

@kevmodrome this doesn't scale well. Form components usually either don't take props or take complex objects, whose identity depends on their location in memory, and is subject to change. Also this would mean that two components with the same props would get the same ID. The success of React's useId is that is uses the knowledge of the component tree, which is stable and unique across SSR. This is something that only the framework knows internally

@lxhom
Copy link

lxhom commented Mar 2, 2023

utilizing all the component tree knowledge of the framework and using that to generate IDs that are stable across SSR and unique across component instances.

The success of React's useId is that is uses the knowledge of the component tree, which is stable and unique across SSR

In theory, yes, but a bunch of edge cases even break with React's useId. If the structure is only slightly different on server and client, you have to either delete a few or generate new ones. For example, if you want to render a list of n elements where n is the screen height divided by 100, or 5 when SSRing. This is a bit simplified and could be fixed, but you'd sometimes have more elements and sometimes less elements. And at this point you have to guesstimate, and this isn't an implementation issue, but a general issue with the concept of hydration consistency, because hydration consistency is ironically not always consistent. You'd have to completely prohibit all SSR checks, which would do way more harm than good. Also, this is not just a theoretical thing, exactly those mismatches with useId happen on prod on an app I know.

TL;DR It's complicated to implement: It is theoretically impossible, we either have to skip some IDs or guess some IDs.

@karimfromjordan
Copy link

Is there a reproducible example of how this could currently create problems in SvelteKit and if not @illright could you create one?

@stephane-vanraes
Copy link
Contributor

Considering the id is not sent along with the rest of the data when submitting the form I doubt there is a way that this information goes back to the server, pretty sure this is solving a problem that does not exist except in theory.

@halostatue
Copy link

Considering the id is not sent along with the rest of the data when submitting the form I doubt there is a way that this information goes back to the server, pretty sure this is solving a problem that does not exist except in theory.

This has nothing to do with form submission, but with using the separated-tag approach for <input id=…> and <label for=…> tags. If a form has multiple instances of a single component (say, you can add multiple instances of an address because people have more than one address), then having stable-dynamic IDs for the input tags so they can be referenced in the label tags is absolutely required.

It’s even more required for various aria-* attributes which expect DOM references or DOM reference lists. The original request indicates both of these cases plus points to a discussion from the ReactWG where multiple use cases are listed explicitly.

And, frankly, I need this in Svelte proper for ARIA as I’m not using SvelteKit.

@stephane-vanraes
Copy link
Contributor

I look through the examples given on the linked issue and all of them require consistency of ids within the context of the browser, none of them require consistency across the server-client boundary.

If the server renders this

<label for="1234">
<input id="1234">

and Svelte hydrates this to

<label for="5678">
<input id="5678">

Everything still works as expected because the ids are consistent within their context. This is something that can be achieved with any of the already mentioned methods, just generate the id one place and pass it to both components (it is exactly the same as you would do in React).

React introduced useId to fix a specific problem with hydration with their framework that does not appear in Svelte (which has other issues but hydration)

So far I have not seen any example where it is of vital importance that both ids are the same on client and server.

@gersomvg
Copy link

gersomvg commented Aug 2, 2023

It's not clear to me whether a hydration mismatch is a big issue in Svelte (I'm coming from a React world)?

Mismatch is pretty much guaranteed when doing SSR, because:

  • libs that generate a unique hash do so independently on the server and the client
  • libs that do count++ have to keep track of count, and the count will persist over multiple user requests on the server, given the count is simply stored as a let in a module.

The solution for this is to put a function for getting the count in a store that is initialised during render of the top-most +layout.svelte.

uid.store.js

import { setContext, getContext } from 'svelte'
import { readable, type Readable, get } from 'svelte/store'
import context from './context'

type GetCount = () => number
type UIDStore = Readable<GetCount>

export function initUIDStore() {
	let count = 0
	const uidStore: UIDStore = readable(() => ++count)
	setContext(context.uid, uidStore)
}

export function initUIDGenerator() {
	const uidStore = getContext<UIDStore>(context.uid)
	let getCount: GetCount
	if (!uidStore) {
		console.warn(
			`getContext(${context.uid}) returned undefined. You need to call initUIDStore() in the root page component.`,
		)
		getCount = () => 1
	} else {
		getCount = get(uidStore)
	}
	return (prefix: string) => `${prefix}_${getCount()}`
}

+layout.svelte (root)

<script>
	import { initUIDStore } from '$lib/uid-store'

	initUIDStore()
</script>

some-component.svelte

<script lang="ts">
	import { initUIDGenerator } from '$lib/uid-store'

	const uid = initUIDGenerator()
	const tabId = uid('tab')
	const panelId = uid('tabpanel')
</script>

💡 The above will work flawlessly, unless you start introducing deliberate mismatches like rendering something on the client but not on the server.

Happy to hear feedback on this, whether this makes sense at all. Even if hydration mismatch isn't a performance issue in Svelte, having a counter on the server that doesn't reset between visits is still an indirect way of exposing your website's visitor count.

@probablykasper
Copy link

@gersomvg It's nice for a random color generator. That could be solved by generating the color in a in +page.server.ts, but I decided to disable SSR instead of creating the extra file/logic for it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants