17

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?

Cully
  • 6,427
  • 4
  • 36
  • 58

3 Answers3

16

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>
    )
}
Cully
  • 6,427
  • 4
  • 36
  • 58
3

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.

panta82
  • 2,763
  • 3
  • 20
  • 38
  • Oh interesting! That's a really good idea! So you've used it and it works? – Cully Jun 08 '21 at 23:07
  • 1
    Yes, but only in development so far. I have no long term experience or insight into downsides (if there are any) yet. – panta82 Jun 09 '21 at 08:50
  • @Cully unfortunately, it doesn't work. Next.js backend doesn't recompile the file with random generator on refresh. So the frontend get all new values, and backend keeps the same. – panta82 Jun 10 '21 at 18:48
  • The idea is really great, though. It seems like it would be possible to make it work somehow. – Cully Jun 10 '21 at 19:48
  • 1
    I'll keep fiddling with it. Maybe I'll figure it out. – panta82 Jun 11 '21 at 15:01
  • I ended up using a solution like this on a project. I generated the seed on the server, passed it down to the client, and both the client and server used the same seed to generate random numbers. It worked great. – Cully Oct 07 '22 at 16:09
-1

getServerSideProps() could work.

https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props#using-getserversideprops-to-fetch-data-at-request-time

It can do some logic in the server and then pass it to the client. So you can make a random ID consistent on the server and client.

function Page({ randNum }) {
  return <div>{randNum}</div>;
}

// This gets called on every request
export async function getServerSideProps() {
  // Get random number
  randNum = Math.floor(Math.random() * 1000);

  // Pass data to the page via props
  return { props: { randNum } };
}

export default Page;
  • 1
    Not a bad idea. Though for the case mentioned in the question, it would be a pain to use `getServerSideProps` to generate ids for a bunch of components. – Cully Jul 23 '22 at 03:00