Skip to content

Instantly share code, notes, and snippets.

@sqren

sqren/useComponentId.js

Last active Mar 6, 2021
Embed
What would you like to do?
React hook for getting a unique identifier for a component
import { useRef } from 'react';
let uniqueId = 0;
const getUniqueId = () => uniqueId++;
export function useComponentId() {
const idRef = useRef(getUniqueId());
return idRef.current;
}
@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Jan 17, 2020

Interesting idea.
Is there a reason why you use ref? Because getUniqueId will be called on every component render/update, that uses the hook.
I useMemo for that, since its callback only called once.

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Jan 17, 2020

Used it for this btw. Thanks for the inspiration:

import {useCallback, useMemo, useState} from "react";

let id = 0;
const getKey = () => id++;

export function useSessionState<TState>(initialState: TState): [TState, (state: TState) => void] {
  const key = `__session_state__${useMemo(getKey, [])}`;
  const storedState = sessionStorage.getItem(key);

  if (storedState) {
    try {
      initialState = JSON.parse(storedState);
    } catch (e) {
      console.error(e);
    }
  }

  const [state, setState] = useState(initialState);
  const storeState = useCallback((state: TState) => {
    setState(state);
    sessionStorage.setItem(key, JSON.stringify(state));
  }, [key]);

  return [state, storeState];
}
@macabeus

This comment has been minimized.

Copy link

@macabeus macabeus commented Feb 22, 2020

@AKomn I think that this approach is simpler and enough:

import { useMemo } from 'react'

let idCounter = 0

const useUniqueId = (prefix: string): string => {
  const id = useMemo(() => idCounter++, [prefix])
  return `${prefix}${id}`
}

export default useUniqueId

I was also inspired by lodash code.


I really hate the unaries operators foo++ and ++foo, but I think that this is a rare case that it could be useful.
Otherwise:

useMemo(() => (idCounter += 1, idCounter), [prefix])
useMemo(
  () => {
    idCounter += 1
    return idCounter
  },
  [prefix]
)
@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Feb 25, 2020

@macabeus I am not quite getting. You just posted the same thing, only creating the callback for no reason every time the function is called, saying its simpler?

@macabeus

This comment has been minimized.

Copy link

@macabeus macabeus commented Feb 25, 2020

@AKMM As long I understand, useMemo is called just when the requirements is changed. So, in this expression:

useMemo(() => idCounter++, [prefix])

the idCounter will be increments only when prefix is changed, not every time like you said.

And, yes, this code is simpler. Using just 7 lines you can do everything that the 20 lines does.
And it uses fewer browser's features, such as localStorage.

useMemo fixes the problem that you said about useRef.

@TrevorBurnham

This comment has been minimized.

Copy link

@TrevorBurnham TrevorBurnham commented Feb 25, 2020

Going by Dan Abramov's comment here, it sounds like the useRef implementation was on the right track. You can avoid calling getUniqueId() on every render by using a conditional on idRef.current:

let uniqueId = 0;
const getUniqueId = () => uniqueId++;

export function useComponentId() {
  const idRef = useRef(null);
  if (idRef.current === null) {
    idRef.current = getUniqueId()
  }
  return idRef.current;
}
@macabeus

This comment has been minimized.

Copy link

@macabeus macabeus commented Feb 25, 2020

But using useRef for everything else that isn't a component is strange, at least for me.
Is more semantic and clear just using useMemo instead of useRef and on new line if (ref.current === null) {, that is almost the same of useMemo.

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Feb 26, 2020

@macabeus I think you did not understand my initial post. I posted a use case plus counter and you posted counter, telling its simpler. It does not make any sense. When I subtract the use case from my initial post, then you have posted the same counter I did, except you create the arrow function each time, while I don't - minor disadvantage, but no benefits, so what is the point. The creation of the arrow function has nothing to do with the arrow function being called once in this specific example. Its two different questions.

@TrevorBurnham
Thanks for the link. With the fix, it does make sense. I wanted to either point out an error in the gist (which it is as it seems) or get new insight in something I might not knew there. Got it via side-channel from you :). Conclusion: I guess useMemo is more generic and does argument iteration to check against the dependencies, so checking manually with an simple weak null check and useRef is more efficient. Makes sense :)

@macabeus

This comment has been minimized.

Copy link

@macabeus macabeus commented Feb 26, 2020

@akomm Okay, now I understand your point. Thank you for reply =]

@mbelsky

This comment has been minimized.

Copy link

@mbelsky mbelsky commented Mar 20, 2020

Hey,

There is an alternative implementation with useState hook:

let uniqueId = 0;

export function useComponentId() {
  const [componentId] = useState(() => uniqueId++);
  return componentId;
}
@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 24, 2020

@mbelsky You can do that, but your example will increment the ID on each render call, even thought no new ID is required. You can fix it making the initialValue a calback () => uniqueId++. I'd create a meaningful hook at this point :)

Also see: https://twitter.com/dan_abramov/status/1099842565631819776?s=20 regarding useRef & useState

@mbelsky

This comment has been minimized.

Copy link

@mbelsky mbelsky commented Mar 24, 2020

@akomm I've tested my solution with this demo: https://codesandbox.io/s/determined-goldstine-qj761 A component rerenders there on click event and uniqueId stays same.

How should I change this demo to see behavior that you described?

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 25, 2020

@mbelsky in your example, its the componentId which is the same, not uniqueId.

@mbelsky

This comment has been minimized.

Copy link

@mbelsky mbelsky commented Mar 25, 2020

@akomm could you explain please what the difference between them and how I can reproduce the issue that you described above?

but your example will increment the ID on each render call, even thought no new ID is required

I'm still not understanding what wrong with this solution

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 25, 2020

just log the uniqueId to console in useComponentId and see if it is the same? You tell uniqueId is the same and post an example where you render the componentId. :)

@mbelsky

This comment has been minimized.

Copy link

@mbelsky mbelsky commented Mar 25, 2020

@akomm got it, thanks. I've updated originally post & demo. btw there is another interesting issue: uniqueId-s are odd numbers now. I'll try to find a solution for that

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 25, 2020

@mbelsky

Your proposal worked. Its just a question if you want to increment the id forever on each render or not. The solution would be, as I proposed above, to replace uniqueId++ with () => uniqueId++.

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 25, 2020

@mbelsky

codesandbox is flaky there, hence your odd numbers. Try it locally, it should work. I used to use codepen, then codesandbox came out and was better. Liked it. But recently it got more and more flaky. A pure bugfeast. Might be there bundler configuration or life reload that messes it up. IF you do some tests, you notice on life reload its rendered twice.

@mbelsky

This comment has been minimized.

Copy link

@mbelsky mbelsky commented Mar 25, 2020

codesandbox is flaky there, hence your odd numbers. Try it locally, it should work

I'll try, thank you!

@VWSCoronaDashboard8

This comment has been minimized.

Copy link

@VWSCoronaDashboard8 VWSCoronaDashboard8 commented Oct 21, 2020

For anyone landing here using Typescript, it is generally advised to avoid the use of null. It simplifies types, and once you try to stop using it you realize you really don't need it 95% of the time because the JS already gives you undefined to work with.

So here's a slight adaptation:

let uniqueId = 0;
const getUniqueId = () => uniqueId++;

export function useComponentId() {
  const idRef = useRef<number>();
  if (idRef.current === undefined) {
    idRef.current = getUniqueId();
  }
  return idRef.current;
}
@kmurph73

This comment has been minimized.

Copy link

@kmurph73 kmurph73 commented Jan 13, 2021

Using this with Strict Mode will give you a different value on the 2nd render. I assume this is okay to use otherwise, and it's just some quirk of Strict Mode that produces differing values? I've used it in production w/o apparent issue...

Edit: looks like it's the result of having a harmless side effect: facebook/react#20826

Trying to move away from this Hook in order to be StrictMode compatible (despite it working 100% fine otherwise)... wish there was a React.useComponentName Hook...

@notthatnathan

This comment has been minimized.

Copy link

@notthatnathan notthatnathan commented Feb 24, 2021

The useRef version works in my testing, but why not just set the initial value rather than checking undefined/null and assigning? Am I missing something here?

export function useComponentId() {
  const id = useRef(getUniqueId());
  return id.current;
}

(edited, forgot .current in the return)

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Feb 27, 2021

@notthatnathan it has actually been explained here.

@notthatnathan

This comment has been minimized.

Copy link

@notthatnathan notthatnathan commented Feb 28, 2021

Oops, missed it. Thanks.

@notthatnathan

This comment has been minimized.

Copy link

@notthatnathan notthatnathan commented Mar 1, 2021

FWIW, after generating the id (I used shortid()) as the initial value (as mentioned above), it doesn't change in my testing. I don't believe the undefined check and reassignment is necessary with refs.

Also, for test snapshot support, you'll want a predictable id, maybe using a different prop from the instance.

const id = useRef(process.env.NODE_ENV === 'test' ? `test-${title}` : shortid());
@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 2, 2021

FWIW, after generating the id (I used shortid()) as the initial value (as mentioned above), it doesn't change in my testing. I don't believe the undefined check and reassignment is necessary with refs.

Why is it not needed and how do the two things relate (I don't know your shortid implementation).

Also, for test snapshot support, you'll want a predictable id, maybe using a different prop from the instance.

const id = useRef(process.env.NODE_ENV === 'test' ? `test-${title}` : shortid());

You add test-related code in a component? And also you change behavior depending on how id is used the outcome might be different in test than prod.

@notthatnathan

This comment has been minimized.

Copy link

@notthatnathan notthatnathan commented Mar 2, 2021

You add test-related code in a component? And also you change behavior depending on how id is used the outcome might be different in test than prod.

Doesn't change behavior, just changes the id. Otherwise every run of your test, you get snapshot updates (new id on mount), which defeats the purpose of snapshots.

Why is it not needed and how do the two things relate (I don't know your shortid implementation).

I'm not sure I understand the question. I'm just saying that this:

  const idRef = useRef(null);
  if (idRef.current === null) {
    idRef.current = getUniqueId()
  }
  return idRef.current;

and this

  const idRef = useRef(getUniqueId());
  return idRef.current;

both return a unique ID that doesn't change on re-render. Which makes sense, refs wouldn't be useful if the initial value changed. Log from the calling component to see what I mean.

(shortid is just an id-generating package my org uses)

updated, typo in the second code example

@akomm

This comment has been minimized.

Copy link

@akomm akomm commented Mar 4, 2021

You add test-related code in a component? And also you change behavior depending on how id is used the outcome might be different in test than prod.

Doesn't change behavior, just changes the id. Otherwise every run of your test, you get snapshot updates (new id on mount), which defeats the purpose of snapshots.

It does. You use different ID generation methods. If one of the different methods generate non-unique ID, it will work in one, but fail in the other or lead to different results, depending on how you use the generated ID.

Sorry. I don't get what you are talking about. You've just changed things replying to my initial question. Why is it null instead of undefined? Also you did not answer why the undefined check is not needed. I don't understand the problem.

@bluenote10

This comment has been minimized.

Copy link

@bluenote10 bluenote10 commented Mar 6, 2021

In cases where I don't need globally unique IDs, but rather IDs unique per component I'm using this hook:

export function useIdGenerator(): () => number {
  const ref = useRef(0);

  function getId() {
    ref.current += 1;
    return ref.current;
  }

  return getId;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment