I'd like to randomly generate the id
property of form inputs, to prevent them from potentially conflicting with other inputs with the same id
. This could happen if I have two login forms on the same page, each with an email
field. The reason I want/need to set the id
property is so that I can set the for
property on the label
corresponding to that input. The problem is that this randomly generated id is different on the server and the client, and so next.js throws an error. Here's some code:
function uniqueId() {
let first = (Math.random() * 46656) | 0
let second = (Math.random() * 46656) | 0
first = ('000' + first.toString(36)).slice(-3)
second = ('000' + second.toString(36)).slice(-3)
return first + second
}
const Login = () => {
const [emailId] = useState(uniqueId())
return (
<form>
<label htmlFor={emailId}>Email</label>
<input id={emailId} name='email' type='email' />
</form>
)
}
This is the error I get:
Warning: Prop 'htmlFor' did not match. Server: "email-txdmls" Client: "email-htte8e"
Any idea how to generate a random id that's consistent on the server/client? Or maybe a different way of doing it without random ids?
UPDATE: React 18 added the useId hook that will likely work for this. I haven't tried it but it looks like it's basically a drop-in replacement for the code in this answer.
I found a workaround to this. I'm not sure if it's a great solution (see explanation below). Seems like a lot of trouble just to essentially suppress a warning message. Still very curious to hear alternate solutions. Honestly even a way to tell next.js to ignore the difference and not issue a warning would work fine (it doesn't matter that the ids differ on SSR and client).
So what I did is generate the id in a useEffect
hook. The problem is that initial server-side rendered HTML doesn't have an id
on the input. It's not until all the JS is processed that it gets an id. Not ideal.
const Login = () => {
const [emailId, setEmailId] = useState(null)
useEffect(() => {
setEmailId(uniqueId())
}, [])
return (
<form>
<label htmlFor={emailId}>Email</label>
<input id={emailId} name='email' type='email' />
</form>
)
}
It should be noted that the id will be null
on the first render. In this example it isn't an issue since the purpose is mostly to associate a label with an input, which will happen quickly enough on the second render. However, if you're using this idea in another situation, just keep it in mind.
If you want to encapsulate this into a custom hook, and clean up your component a bit:
const useUniqueId = () => {
const [id, setId] = useState(null)
useEffect(() => {
setId(uniqueId())
}, [])
return id
}
const Login = () => {
const emailId = useUniqueId()
const nameId = useUniqueId()
return (
<form>
<label htmlFor={nameId}>Name</label>
<input id={nameId} name='name' type='text' />
<label htmlFor={emailId}>Email</label>
<input id={emailId} name='email' type='email' />
</form>
)
}
My solution was to use a seeded random number generator instead of Math.random()
. Since I use the same seed on both frontend and backend, they both end up getting the same ID-s.
// https://stackoverflow.com/a/47593316/2405595
function createRandomSeedGenerator(str) {
let h = 1779033703 ^ str.length;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return () => {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
// https://stackoverflow.com/a/47593316/2405595
function createDeterministicRandom(seedString) {
const generateSeed = createRandomSeedGenerator(seedString);
let a = generateSeed();
let b = generateSeed();
let c = generateSeed();
let d = generateSeed();
return () => {
a >>>= 0;
b >>>= 0;
c >>>= 0;
d >>>= 0;
var t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}
const deterministicRandomNumber = createDeterministicRandom(process.env.NODE_ENV);
function uniqueId() {
let first = (deterministicRandomNumber() * 46656) | 0
let second = (deterministicRandomNumber() * 46656) | 0
first = ('000' + first.toString(36)).slice(-3)
second = ('000' + second.toString(36)).slice(-3)
return first + second
}
Of course, you should NOT do this if you need random numbers for security purposes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With