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

Unique component identifier #1137

Closed
andreypopp opened this issue Feb 20, 2014 · 51 comments
Closed

Unique component identifier #1137

andreypopp opened this issue Feb 20, 2014 · 51 comments

Comments

@andreypopp
Copy link
Contributor

Both react-async and react-multiplayer need components to have unique identifiers.

In case of react-async — to maintain a mapping from a component to its state and make it serialisable (to send it over the wire, ES6 Map with components as keys can't be used), in case of react-multiplayer — to have an unique URL per component instance.

Currently both of the libraries use this._rootNodeID + ',' + this._mountDepth. Should this be added to a public API — getComponentID() or something?

@syranide
Copy link
Contributor

this._rootNodeID + ',' + this._mountDepth is redundant even, this._rootNodeID is enough. However, the unique ID is an implementation detail AFAIK, it's likely that it will be removed in the future if/when we ever let go of innerHTML. Also, it's possible that a concatenated ID won't be available soon, but each node will only hold it's own relative ID and possibly a monotonic unique ID for innerHTML (again, implementation detail).

Also this._rootNodeID is only unique at a single point in time, it's not unique over time. I'm unsure why this can't be solved for example via a Mixin that creates a unique ID for the component by just incrementing a global counter.

@sophiebits
Copy link
Collaborator

@syranide _rootNodeID can be shared by multiple composite components, correct?

This seems like something that's fragile -- for cases where you only need a unique ID on one client but don't need it to be consistent across clients, you can make your own autoincrementing counter. For cases where you want the IDs to be consistent across clients, I don't think you can rely on this._rootNodeID because rendering components in a different order (or with server rendering, on different servers) will cause the node IDs to be different -- when you need this I think you really just want to force the person using a mixin to specify a unique key.

@syranide
Copy link
Contributor

@spicyj _rootNodeID must be unique at any point in time, but it only describes the hiearchy of indices and keys, so once a component is removed, the _rootNodeID is likely to be immediately assumed by another component taking it's place.

Anyway, @andreypopp messages me in the chat and it seemed like the Mixin approach was a good solution.

@sophiebits
Copy link
Collaborator

@syranide I mean if you have two composite components nested with no DOM node in between then they share a rootNodeID:

var markup = this._renderedComponent.mountComponent(
rootID,
transaction,
mountDepth + 1
);

@syranide
Copy link
Contributor

@spicyj Ah right, right, _rootNodeID emphasis on Node as in DOMNode. Excellent point, they're only unique in the DOM (loosely speaking).

@syranide
Copy link
Contributor

@spicyj Can close this (this was solved on IRC btw).

@laser
Copy link

laser commented Apr 23, 2015

@syranide @spicyj - For those of us who weren't on IRC - what was the resolution?

@syranide
Copy link
Contributor

@laser IIRC simply that it was there were better ways to approach the problem that doesn't rely on internals. I have a faint memory of him simply generating a unique ID for each component on mount and using that instead, but I could be way off.

@laser
Copy link

laser commented Apr 23, 2015

@syranide Ah, okay. Thanks for clarifying.

My team is trying to come up with an approach for including a view-specific token with our actions such that errors (say, failing server-side validation) can be linked back to originating views w/out introducing granular, view-specific stores. The approach was suggested by @jingc here. I'd love to be able to rely on a unique id set by React instead of having to generate something myself, if possible.

Take care,

Erin

@sophiebits
Copy link
Collaborator

@laser Yeah, if all you need is a unique ID for each component then you can make a counter yourself and increment it for each new component. (We try to avoid adding functionality to React if it can be easily replicated in component code.)

@laser
Copy link

laser commented Apr 23, 2015

@spicyj Cool, makes sense.

Without thinking too hard about it, I could imagine something like:

class BaseComponent extends React.Component {
  constructor(props) {
    super(props);
    this.componentId = SomeLib.createComponentId();
  }
}

@lemiii
Copy link

lemiii commented May 8, 2015

I don't necessarily like what i'm doing since it's hacky and has a chance of collisions when you have multiple instances on the same page, but I set the state of the component with a random id, and then use that.

getInitalState: function() {
  return { 
    id: Math.floor(Math.random() * 0xFFFF) 
  }
},

getId: function(name) {
  return name + this.state.id;
},

render: function() {
  return (
    <div>
      <input type="checkbox" id={this.id('agreeCheckbox')} />
      <label htmlFor={this.id('agreeCheckbox')}>I agree</label>
    </div>
  );
}

@davidgilbertson
Copy link
Contributor

A simple counter and component-specific string works just fine for me.

import React, {Component} from 'react';

let count = 0;

class InputUi extends Component {
    constructor(props) {
        super(props);

        this.guid = 'input-ui-' + count++;
    }

    render() {
        return (
            <span>
                <label htmlFor={this.guid}>
                    {this.props.label}
                </label>

                <input
                    id={this.guid}
                    />
            </span>
        );
    }
}

export default InputUi;

Is there a scenario where this won't work?

@syranide
Copy link
Contributor

@davidgilbertson Server-rendering, it will work in some trivial setups but for everything beyond that will cause count to become out-of-sync between client and server and cause markup mismatch.

@davidgilbertson
Copy link
Contributor

I thought that might be the case, but I've got 49 components and a few hundred instances nested up to 6-7 levels deep which I would consider non-trivial.

And it works fine.

Is there a specific scenario that comes to mind where the initial client-side markup rendered by ReactDom.render() won't match the markup generated by ReactDOMServer.renderToString()?

(Webpack's hot swapping will make them out of sync but I don't care about that.)

@omerts
Copy link

omerts commented Apr 13, 2016

React 15 has nulled many _rootNodeId, any other ideas? Generating ids isn't a good solution for us, since we need a unique id for 3rd party components as well, and wrapping a 3rd party components is something we want to avoid.

@sophiebits
Copy link
Collaborator

You could use a WeakMap and store an ID for each instance. _rootNodeID was never public API and we no longer needed a unique ID on composite components so we got rid of it. Depending on your use case, perhaps you can refactor your API so that the third-party components are wrapped and your wrapper can generate an ID.

@omerts
Copy link

omerts commented Apr 14, 2016

@spicyj We are trying to make redux-devtools' time travelling feature to actually replay inner component states. So our use case, is being able to inject a state object into a component, only by ID, since instances get created/disposed during the time travel. So bottom line, our map consists of comoponentID -> componentState, and we use ID to locate the current component instance. This worked for us, until React 15 (we used some internals though).

@ricardosoeiro
Copy link

We were also relying on _rootNodeID as an identifier to cache component state on componentWillUnmount, so that we can restore it for all components when the user presses the back button, on componentWillMount.

This is an important use case for us and this ID was a perfect fit. We knew we were taking a risk using internals, but still... is there any reliable alternative? We thought about generating some kind of component xpath by traversing the hierarchy, but it doesn't seem very performant to do for several components.

@gaearon
Copy link
Collaborator

gaearon commented Apr 17, 2016

@ricardosoeiro The approach you describe is very fragile and not really supported. Have you looked into using something like Flux or Redux instead? They support exactly this use case, but in a different and more explicit way.

@omerts
Copy link

omerts commented Apr 17, 2016

@gaearon Do flux/redux support injecting inner component state? Thats our need, and i think @ricardosoeiro's too. We have solved our problem by travesing up the tree and building an id from the type of elements up the tree. I have the code at work, and ill post it tomorrow.

@gaearon
Copy link
Collaborator

gaearon commented Apr 17, 2016

@omerts No, they are meant as external state containers. Is there any reason you can’t convert your components to use external state rather than try to inject local state into them?

@omerts
Copy link

omerts commented Apr 17, 2016

@gaearon mainly bcs of 3rd party controls

@gaearon
Copy link
Collaborator

gaearon commented Apr 17, 2016

@omerts Usually you don’t want to depend on third-party components that don’t offer a “controlled” API like value + onChange props. Is this definitely not the case for components you rely on?

@ricardosoeiro
Copy link

@gaearon: why would we want to convert external components and then maintain them? This is a generic problem that calls for a generic approach... I'm not saying this should be supported out of the box, but I've seen this use case pop up several times, so it deserves at least a fair discussion :)

@ricardosoeiro
Copy link

@omerts does your approach rely on other internals to traverse the component tree? I'm curious to see your code

@gaearon
Copy link
Collaborator

gaearon commented Apr 17, 2016

I'm not saying this should be supported out of the box, but I've seen this use case pop up several times, so it deserves at least a fair discussion :)

I agree, and this is being discussed here: #4595.

I’m just saying that, until something like this is supported, it is better to rely on proven solutions that work well (even if they require components to be controlled) rather than on internals that break with every release, and are likely to break completely after some work on incremental reconciler (#6170) or bundling React as a flat file (#6351).

@ricardosoeiro
Copy link

An api to obtain an opaque identifier, as suggested in #3932, would solve this use case perfectly...

@coryhouse
Copy link
Contributor

I'd love to see a deterministic, unique component identifier added to React. Our QA team needs consistent IDs as hooks for automated UI testing. So we're currently adding IDs everywhere manually. I suspect we're not alone. @omerts solution looks promising, but a built in solution would likely perform better and more reliably.

@jwietelmann
Copy link

I would like to throw in on this to lobby for a built-in solution. The roll-your-own counter solution, which I have used for years, requires a manual per-request reset on the server side in order to make isomorphic apps work properly. It feels completely acceptable in the context of writing a full app, if a bit confusing the first time you encounter the issue. But it becomes a huge pain when writing a DOM component library.

Consider the following: http://foundation.zurb.com/sites/docs/forms.html#checkboxes-and-radio-buttons. Now consider that I want to write a component package for NPM that encapsulates this into a nicer API. Here are some of my awful choices:

  • Bottle it up with a counter and live with the fact that my lib won't work in an isomorphic app (unacceptable)
  • Make end-users bring their own counter or unique ID and provide them to every component as props (unnecessarily verbose, places more requirements on composing library components into additional reusable ones)
  • Force the end-user of your components into a specific fluxish lib (just to get unique DOM IDs in a simple UI library? not worth it)

There's one halfway decent option that I can come up with, which I still don't love: Create a package that includes a UniqueIDProvider component whose descendants receive the counter function via context. This could be fairly elegant if you could install it via NPM with some convenience functions for decorating components to receive the context a la react-redux.

But I still think it's a little bit absurd not to include out-of-the-box support for something that comes up this frequently in the DOM.

@kromit
Copy link

kromit commented Oct 24, 2016

2 years after the issue were closed there obviously still demand for a solution and discussion about it. I do not see any reason why the issue should not be reopened.

@bayov
Copy link

bayov commented Feb 11, 2017

Please reopen this issue. It can be easily implemented, and is a mandatory feature.

@veob
Copy link

veob commented Feb 14, 2017

If one uses underscore.js, they can use .uniqueId method. Works both in node and browser.

@coryhouse
Copy link
Contributor

@veob That's not deterministic. A deterministic id requires considering the entire component tree to assure it only changes when markup changes.

@austinhinderer
Copy link

Jumping in as another user who is deeply interested in this feature. Not every environment allows you to isolate your testing to NodeJS/React for integration testing only. These deterministic, unique IDs are extremely important for building larger applications in an enterprise setting.

@klaygomes
Copy link

3 years and nothing yet? What a shame!

@jpfiorilla
Copy link

I used this._reactInternalInstance._debugID for unique classNames for a fancy scrollmagic/gsap effect and when i deployed the whole site shat itself lol

@OutThisLife
Copy link

+1

@sfrieson
Copy link
Contributor

I have a solution that's working pretty well so far. I've been generating sequential ids with a simple utility, and living with the checksums for the time being. This is inspired by what @jwietelmann said above about resetting on a per-request basis but is maybe worth saying again. As he said, this not as much help if you're working on a component library, but works best when you're creating the entire app.

// utility to generate ids
let current = 0

export default function generateId (prefix) {
  return `${prefix || 'id'}-${current++}`
}

export function resetIdCounter () { current = 0 }

And then in a root component I call the resetCounter function. On the server, this clears the id from memory each time there's a new server render. In the browser, it doesn't really reset anything, but that's fine.

Working well so far.

@mtrabelsi
Copy link

mtrabelsi commented Sep 8, 2017

Actually it's not that complicated to get a unique ID:

  
constructor(props) {
      super(props);
      this.id = new Date().getTime();
  }

As for CSS selector needs the first letter to be a character you can prefix it with an 'id-' on the render method :

  render() {
      return (<div id={`id-${this.id}`}>
                           ...
              </div>);
  }

Works like a charm, don't wait for React - they have their own policy and list of future pending , maybe will never be implemented :)

@nhunzaker
Copy link
Contributor

@mtrabelsi That approach is dangerous because multiple components may be created in under a millisecond. The time resolution isn't high enough.

Still, the technique of assigning ids in the constructor is how I've approached this in the past. However I've used a uuid library for it, and usually prepend a prefix to make the id contextual:

constructor(props) {
    super(props);
    this.id = "thing-" + uuid()
}

This isn't perfect, and I still like to generate an ID based on props when I can, but it accomplishes the task if prop-based identifiers fail.

@jquense
Copy link
Contributor

jquense commented Sep 8, 2017

uuid's in constructors work fine if you don't need SSR, other tho it's likely the id's will mismatch between server and client :/

@omerts
Copy link

omerts commented Sep 8, 2017

@mtabelsi @nhunzaker, both solutions will not create a persistent id, especially if a certain component is mounted/unmounted across renders (even moving between tabs might unmount a component), not to say between refreshes.

@nhunzaker
Copy link
Contributor

@omerts Correct. It definitely does not fix the issue.

I think this could be a really great feature. Unfortunately I don't really understand enough about Fiber to make it happen, but would happily assist anyone trying to figure this out.

@sophiebits
Copy link
Collaborator

We should probably agree on whether we want this and how it should behave before considering implementations (unless it's just for a proof of concept).

@nhunzaker
Copy link
Contributor

@sophiebits Ah sorry, I shouldn't get ahead of myself! I'd love to figure out some resolution here.

@sebmarkbage
Copy link
Collaborator

This issue isn't about IDs. It's about whether it's a good idea to preserve state across serialization and if the use cases for that in turn can't be modeled better. Don't get stuck on the ID thing. If serialization is a good idea maybe it's better we just build that in.

@vorillaz
Copy link

vorillaz commented Apr 1, 2018

I prefer using HOCs where needed in order to easily manipulate nested components.

import React from 'react';
let idx = 0;
const uuid = () => idx++;

export default Wrapped => props => {
  const { id, ...rest } = props;
  const uniqueId = id ? id : `id-${uuid()}`;
  return <Wrapped {...rest} id={uniqueId} />;
};

@ruucm
Copy link

ruucm commented Jul 2, 2018

For 2 Days research, I got a solution for this headache problem.

Here is my solution

Make Unique Id by props in React

@Exifers
Copy link

Exifers commented Jul 3, 2022

Here is a POC, but unfortunately if this hook is called multiple times in the same component, it gives different ids.

let id = 0;

const useMountId = () => {
  const ref = useRef<string>();
  
  if (!ref.current) {
    ref.current = (++id).toString(10);
  }

  return ref.current;  
}

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

No branches or pull requests