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

Introducing an RFC for isomorphic IDs #32

Closed
wants to merge 2 commits into from

Conversation

n8mellis
Copy link

@n8mellis n8mellis commented Mar 8, 2018

This pull request introduces the first draft of an RFC to address the ability for React to support isomorphic IDs; a feature which is highly desirable for building WCAG 2.0 compliant web sites and applications.

For more information about the motivation behind this pull request, please see the following issue:

facebook/react#5867

@streamich
Copy link

streamich commented Mar 8, 2018

Very nice, this is something very much needed, but can this be achieved by a single call to React.createId()?

class Checkbox extends React.Component {
  id = React.createId();

  render () {
    return [
      <input id={this.id} />,
      <label htmlFor={this.id} />,
    ];
  }
}

@n8mellis
Copy link
Author

n8mellis commented Mar 8, 2018

@streamich I don't see React.createId() anywhere in the documentation or source code. Is this something that already exists? If so, it's hiding very well. :)

If this is a reference to generateUniqueId() in my RFC, then I should have been more clear that that was just a placeholder to a generic UID-generating function. I wasn't proposing that as an actual function name. I'll push an update that makes that more clear.

Generating the UID isn't the hard part. It's getting the UID that the server used into the client's render context.

The function `generateUniqueId()` was not meant as a proposal but rather a placeholder for any generic UUID-generating function.
@streamich
Copy link

streamich commented Mar 8, 2018

@n8mellis Sorry, I did not write it up well, what I was asking if the two methods reserveUniqueIdentifier() and reservedIdentifier() could be simplified to one method and if it could be done in constructor, to avoid calling reservedIdentifier() on every re-render in the render method.

@n8mellis
Copy link
Author

n8mellis commented Mar 8, 2018

@streamich Ah. That makes much more sense. :) My thinking was that this ID be treated differently than passing in an ID prop and thus the different approach.

I split it into two so that all the heavy lifting happens in the constructor. The reservedIdentifier method is just a property getter so it's no more expensive than any property access. I made it a function rather than a straight property accessor to make sure that the private property is obscured and that there is little risk in it accidentally being overwritten or changed in unexpected ways.

That however is a personal preference and would be open to arguments in favor of something different.

@lostpebble
Copy link

Thanks for this RFC, I think it would be extremely useful.

The main reason I need this is for deterministic incremented ids of my React Components which are rendered - so the same content is displayed both server and client side.

For example, I have "filler" images for some ad space when it's not used and I'd like to display different random images when the user loads the page. If I only had one on a page, it would be easier (as I could just use a single random seed generated in my state stores on server and hydrated in client). But I would generally have multiple Components which look something like this:

const fillerImageUrls = [
   "https://www.somewhere.com/filler-image-1.jpg",
   "https://www.somewhere.com/filler-image-2.jpg",
   "https://www.somewhere.com/filler-image-3.jpg",
   "https://www.somewhere.com/filler-image-4.jpg",
];

Component {
   render() {
     const uniqueIncrementedId = this.getUniqueIncrementedId();

     return <img src={fillerImageUrls[randomSeedFromStores + uniqueIncrementedId) % fillerImageUrls.length]}/>
   }
}

So, whereas this RFC is pushing for implementing a unique ID string hash of sorts - I'd really also like the capability of getting an incremented integer number which is unique to each component as well.

I would hope this to be incremented according to the amount of a single type of component on the page. In other words, it would not be unique between different types of components (the count should start at zero for each new type of component on the page).

@manuelbieh
Copy link

manuelbieh commented Apr 5, 2018

This is highly desirable! I dropped automatic id generation for aria-labelledby attributes in a component recently because creating the IDs based on Math.random() on a per render basis of course caused React to show a warning that the client HTML does not match the server HTML.

I couldn't figure out an easy solution to deterministically generate unique IDs without spending too much effort on this. Would be super nice to have a small helper function built-in.

I remember the good ol' data-reactid attribute from 0.14 which were unique within each mounted component. Maybe they could be revitalized for that, maybe optionally prefixed wit a custom string to increase the chance of being really unique.

Spontaneous idea:

class MyComp extends Component {
  id = this.dataReactId('optionalPrefix');
  render() {
    return (
      <div id={this.id}>...</div>
    }
  }
}

Result:

<div id="optionalPrefix_.0.0.1">...</div>

// edit:
Could be also applied to children like:

React.cloneElement(child, { id: child.dataReactId('childPrefix') });

Result:

<div id="optionalPrefix_.0.0.1">
  <div id="childPrefix_.0.0.1.0" />
</div>

@ryanflorence
Copy link

ryanflorence commented Sep 4, 2018

Could definitely use something like this.

For now I use a context with a genId() on it. The default context generates random IDs, but if you wrap the whole app in a Provider then it increments each time it's called.

import React, { createContext } from "react";

let genId = () => Math.random().toString(32).substr(2, 8);

let IdContext = createContext(genId);

// Apps can wrap their app in this to get the same IDs on the server and client
class Provider extends React.Component {
  id = 0;

  genId = () => {
    return ++this.id;
  };

  render() {
    return (
      <IdContext.Provider value={this.genId}>
        {this.props.children}
      </IdContext.Provider>
    );
  }
}

let { Consumer } = IdContext;

export { Provider, Consumer };

@gaearon
Copy link
Member

gaearon commented Sep 4, 2018

What does building this into React achieve compared to @ryanflorence's solution?

@jquense
Copy link

jquense commented Sep 4, 2018

The big thing for me is that it doesn't require that libraries ship even more Providers. I know the battle may be lost on that one, but having every ui library require it's own IdProvider component isn't great. That works fine for applications but tbh the problem isn't apps where you control the world. The problem is for react-bootstrap, who wants to ship an accessible Tablist component, which require ids. RB's solutions is to require the id prop, but folks get annnnooooyed about that, and tbh it's more onerous than needs to be for things like tooltips. On the other hand dealing with @reach/ui's IdProvider, Bootstrap's provider, react-widgets, etc. it gets annoying and out of hand p quickly.

@jquense
Copy link

jquense commented Sep 4, 2018

FWIW I actually think the new ability to set global context values may be enough to offset that annoyance since you can avoid the Provider thing.

@gaearon
Copy link
Member

gaearon commented Sep 4, 2018

Why can't community standardize on a single "provider" for this?

FWIW I actually think the new ability to set global context values may be enough to offset that annoyance since you can avoid the Provider thing.

Yep that's what I'm thinking.

@AjayPoshak
Copy link

I think that React itself should unopinionated about generating and handling id. It should be a community standard to generate and handle id. The feature to generate unique id might be abused, as people would like to use it to fix unique keys error in a component list.

@quantizor
Copy link

@ryanflorence if the component render order differs between server and client, won't this fail to match up? I think that's a possible situation with async mode on?

@maranomynet
Copy link

@ryanflorence: @probablyup is right that auto-incrementers break when rendering is async or for some reason happens in different order on server and client.

@diegohaz
Copy link

It would be really useful to have something like this:

import { useId } from "react";

function Component() {
  const id = useId();
  return <div id={id} />;
}

@TxHawks
Copy link

TxHawks commented Jan 13, 2019

@gaearon how would one use context to do this with suspence and concurrent mode?

Ciuldnt the render order be different between the server and the client?

@Merri
Copy link

Merri commented Jan 29, 2019

Last summer I started a project for providing a HOC to deal with this uniqueness issue. It hasn't had much use and I haven't pushed it as I didn't have time to finish it before today.

The issue with most proposed or shown solutions is that they often either work only client side, keep increasing forever on server side, or even leak memory. To account these issues:

  • ID service must create same IDs for the same render.
  • IDs that are no longer used must be cleared.
  • Must expose extra method so that all generated IDs can be cleared after each server render (so that IDs don't keep piling up in server).

Here is a short sample on how react-maria works:

// MyComponent.jsx
import React from 'react'
import { withUniqueId } from 'react-maria'

function MyComponent(props) {
    // uidTools is optional, but useful when you render a lot of elements that require unique IDs
    const uidTools = props.getMariaIdTools()
    const uids = uidTools.make(['hello', 'world'])

    // note: id is provided by withUniqueId; also, <MyComponent id="hi"> results into props.id = 'hi1'
    return (
        <ul id={props.id}>
            {uids.map(uid => <li key={uid}>My unique id is {uid}</li>)}
        </ul>
    )
}

export default withUniqueId({ identifier: 'my-component' })(MyComponent)

You can find source and docs at GitHub. I just hope I didn't brainfart anywhere since I haven't tested this in a production environment yet :)

As far as I can understand the good thing about this solution is that there is no need for a Provider so end user of a third party component that uses React Maria needs to do nothing extra - only use the component!

@theKashey
Copy link

This feature is not possible. You just can't use just use generateUniqueId in async or concurrent environments. And SSR.
In short - you should be able to generate the same "set" of IDs regardless of component/chunk load order. That order could be different - data for components arrive unpredictable, chunks got loaded unpredictable, async in a client, and usually sync on a server.

Solution:

  1. Store counter in a context.
  2. Read-on-mount and increment-on-read. That would provide a unique ID for a component during its lifetime. It will generate ids like 1, 2, 3, 4.
  3. Nest counter for any async children. It will generate ids like 1.1, 1.2, 1.3.
  4. As a result - every component would get stable id, regardless of its nature.
<UIDReset> // sets `counter` to 0
     <UIDFork> // `forks` counter
       <AsyncLoadedComponent> // will render something in a second
          <UIDConsumer>
            { uid => <span>{uid} is unique </span>} // will render 1.1
          </UIDConsumer>
      </UIDFork>    
    </AsyncLoadedComponent>
     <UIDFork> // `forks` counter.
         <UIDConsumer>
           { uid => <span>{uid} is unique </span>} // would render 2.1
     </UIDFork>
 </UIDReset>

So, even if the render order of AsyncLoadedComponent is not known - Id is "stable". These "forks" are the missing parts.

Meanwhile it still could be "amost" as this RFC proposes

const Form = () => {
  const uid = useUID(); // this is hook
  return (
    <>
     <label for={uid}>Email: </label>
     <input id={uid} name="email" />
    </>
  )
}

It's not a pseudo code, but https://github.com/thearnica/react-uid, I am using it for a while to pair labels and input on frontend, or generate consistent Ids between server and client.

@r00dY
Copy link

r00dY commented Mar 18, 2019

I'd love this feature. Generating unique ID really leaves a lot to be desired from developer experience point of view right now.

My use case:

I'm working on a simple library with UI component. Its CSS is dependent on props. And the CSS uses media queries which means I can't use inline styles. So I generate random ID inside the component, add class names based on this random name and add inline <style> tag.

And ofc random id breaks with SSR.

I believe there's a strong motivation behind the approach I take:

  1. I want simplest developer experience from my library. Just use the component and it should work. Without SSR or with it (like in next.js / gatsby).
  2. I don't want to enforce any specific CSS-in-JS solution for user of my lib. If someone uses emotion, I don't want him to add styled-components only to use my simple lib. Library should be definitely independent of such things.

And now:

  1. Solution with providers doesn't sound good imo, why would I force my user to wrap the app with my provider only because she wants to use my simple small library? We're going to get into a Provider hell. And developer experience (onboarding) of using my library will be very bad in SSR environments (like next.js / Gatsby).
  2. Solutions based on providers and counters suffer from async issues.
  3. Solution with using the fact that componentDidMount is called only on client side suffers from FOUC.

There's definitely sth missing here. Solution could be useUID hook or some more generic mechanism of passing data from server-side-rendered component to client-side-rendered version.

I work a lot with React in next.js / Gatsby environments (out-of-the-box SSR, SEO-critical stuff like e-commerce / websites) and I'd like to take a small lib and simply make it work without any extra hassle. And any solution that requires sth more than import component and use it is not an optimal developer experience.

@theKashey
Copy link

@r00dY please don't do it. You don't need it.
What you need:

  • generate any hash from a media query you want to apply. And that hash would be stable, and would stand anything, async stuff included.
  • use hash as className.
  • magic! you can deduplicate your styles - similar hashes mean similar rules.
  • PS: styled-components are doing exactly the same. It's a proven solution.
  • PPS: And it's much simpler, and does not require almost anything, like a Provider Hell.

@eps1lon
Copy link
Contributor

eps1lon commented Nov 10, 2019

It looks like an implementation has been proposed: facebook/react#17322

@gaearon
Copy link
Member

gaearon commented Apr 7, 2020

Thank you so much for writing this up!

We have merged an experimental implementation inspired by this proposal in facebook/react#17322 and are going to try it at Facebook. It's not using exactly the same heuristics as this proposal but should address some of the issues that would come up when combining it with other features we care about (like Progressive Hydration).

We'll report back with our findings.

@theKashey
Copy link

@gaearon - could you provide some clarification, please.
A new hook is based on a single counter variable, which is:

  • would be not reset from one server render to another
  • would be not incremented if some pieces would be not hydrated, opening the possibility to use the same ids twice on the page.
  • "micro-frontends" gonna blow up, ie two Reacts bundled separately on one page would fight with each other.

@gaearon
Copy link
Member

gaearon commented Apr 7, 2020

would be not reset from one server render to another

Would you mind opening an issue about this?

would be not incremented if some pieces would be not hydrated, opening the possibility to use the same ids twice on the page.

This doesn’t sound right to me. The client and the server intentionally use different prefixes so I don’t think they could clash. (We don’t attempt to match the IDs between client and server — instead, we correct them during hydration.)

"micro-frontends" gonna blow up, ie two Reacts bundled separately on one page would fight with each other.

Also worth discussing in an issue.

@theKashey
Copy link

Ok. So having different counters on the server and the client solves my first two questions. My bad, I've missed this point in the PR.
However, it would be still a good idea to reset the counter in test-renderer to support snapshots.

So the only problem left is with two reacts on one page.
For example we have some legacy projects, some pieces of which are being replaced by "third-party" React components, and we could not guarantee that react versions between those pieces would be the same, and share the same counter. Even if they would - modules resolution could duplicate react per-dependency.

The standard solution for this is randomize, as an explicit seed initialization, or <Randomize> component in our case, which might prefix all id inside it by some UID. It really does not matter how it does it - stuff should be just unique behind it, no matter what.
Such component would solve multi-frontend issues, as well as multi-backend or theoretical fragment cache, which is the same as multi-backend - results from different renders are somehow combined together.

@gaearon
Copy link
Member

gaearon commented Apr 7, 2020

@theKashey Let's discuss current implementation in a React issue. This would be a bit easier for us to track.

@samvv
Copy link

samvv commented Apr 15, 2020

Sorry for the noise, but is there any update on this 'experiment'? I can definitely see use-cases that would benefit from this RFC. For instance, I'm currently developing a system that could use unique IDs for every <form />, such that the server can automatically reverse-lookup the submission of an arbitrary React-generated form.

@theKashey
Copy link

Thus we have to talk about unique-unique-ids as per this RFC and (sequentially)uniquie-ids implemented in facebook/react#17322

@gaearon
Copy link
Member

gaearon commented Apr 17, 2020

@samv We haven't even checked it into the internal codebase yet. :D I think we'll have something more concrete in the coming months.

server-rendered markup be accomplished during the hydration phase before the
first DOM merge?

- Would this approach cover all use cases?
Copy link
Contributor

@eps1lon eps1lon May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RFC does not mention how (dynamic) children in composite widgets would communicate their IDs. An example for this would be the tabs widget where a [role="tabpanel"] is labelled by its corresponding [role="tab"].

Given the current implementation of useOpaqueIdentifier and the rules of hooks, we would need to create ids for each part of the widget:

function App() {
  const tabIdOne = React.unstable_useOpaqueIdentifier();
  const panelIdOne = React.unstable_useOpaqueIdentifier();
  const tabIdTwo = React.unstable_useOpaqueIdentifier();
  const panelIdTwo = React.unstable_useOpaqueIdentifier();

  return (
    <React.Fragment>
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={tabIdOne} panelId={panelIdOne} value="one">
            One
          </Tab>
          <Tab id={tabIdTwo} panelId={panelIdTwo} value="one">
            One
          </Tab>
        </div>
        <TabPanel id={panelIdOne} tabId={tabIdOne} value="one">
          Content One
        </TabPanel>
        <TabPanel id={panelIdTwo} tabId={tabIdTwo} value="two">
          Content Two
        </TabPanel>
      </Tabs>
    </React.Fragment>
  );
}

-- Full example

If we could use useOpaqueIdentifier as prefix we could avoid boilerplate by creating a single opaque identifier per widget that is suffixed by the string representation of the tab value.

The following hack only works with ReactDOM.render:

class SuffixedOpaqueIdentifier {
  constructor(opaqueIdentifier, suffix) {
    // private
    this.opaqueIdentifier = opaqueIdentifier;
    this.suffix = suffix;
  }

  toString() {
    return `${this.opaqueIdentifier}-${this.suffix}`;
  }
} 

function Tabs({ children }) {
  const id = React.unstable_useOpaqueIdentifier();
  const getTabsId = React.useCallback(
    value => {
      return new SuffixedOpaqueIdentifier(id, value);
    },
    [id]
  );

  return (
    <TabsContext.Provider value={getTabsId}>
      {children}
    </TabsContext.Provider>
  );
}

...
function useTabId(tabValue) {
  const getTabsId = React.useContext(TabsContext);
  return getTabsId(`tab-${tabValue}`);
}

-- https://codesandbox.io/s/useopaqueidentifier-as-a-prefix-for-tabs-kke2c?file=/src/App.js:1029-1596

Update: dedicated issue: facebook/react#20822

server-rendered markup be accomplished during the hydration phase before the
first DOM merge?

- Would this approach cover all use cases?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't think about using this RFC for threading before I openened facebook/react#18594.

TL;DR: What about attributes that allow multiple ids (i.e. attributes allowing IDREFS) e.g. aria-labelledby or aria-describedby?

@gaearon
Copy link
Member

gaearon commented Aug 17, 2020

Hey all, there's been some concerns so I wanted to give a small update.

We are experimenting with a Hook for this because this space intersects with some of our ongoing work on progressive hydration. However, that Hook's API is not final in any way, and we definitely want to take another read through all the concerns posted so far before sending an RFC for our version. Initially, we planned to experiment with it way earlier, but due to the pandemic people who planned to integrate it had to switch to different projects, so the work has frozen for a while and we switched to other ongoing projects.

We expect to start integrating and testing our version soon, and after gaining insights from it, we will return to this RFC and any concerns raised in it.

(Our Hook shipped in experimental release with an unstable_ prefix but as usual, it only means we're planning to test it internally, and doesn't mean that specific version would become stable.)

@gaearon
Copy link
Member

gaearon commented Aug 18, 2021

A small update that there hasn't been much changes since the last update. It's still on our list to continue experimenting with, but React 18 is still in alpha. We expect to revisit this closer to the beta or after it.

@gaearon
Copy link
Member

gaearon commented Oct 21, 2021

An update: reactwg/react-18#111

@gaearon
Copy link
Member

gaearon commented Oct 31, 2021

facebook/react#22644

@gaearon
Copy link
Member

gaearon commented Mar 29, 2022

Thank you for the proposal and discussion.

We've merged #212 which includes useId.

It fulfills the same role as this RFC, so let's close this one.

useId is available in React 18.

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

Successfully merging this pull request may close these issues.

None yet