Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Next.js, using random properties without triggering "did not match on server/client" error

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?

like image 970
Cully Avatar asked Jan 29 '20 22:01

Cully


2 Answers

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>
    )
}
like image 178
Cully Avatar answered Nov 18 '22 04:11

Cully


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.

like image 31
panta82 Avatar answered Nov 18 '22 05:11

panta82