-
Notifications
You must be signed in to change notification settings - Fork 558
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
Conversation
Very nice, this is something very much needed, but can this be achieved by a single call to class Checkbox extends React.Component {
id = React.createId();
render () {
return [
<input id={this.id} />,
<label htmlFor={this.id} />,
];
}
} |
@streamich I don't see If this is a reference to 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.
@n8mellis Sorry, I did not write it up well, what I was asking if the two methods |
@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 That however is a personal preference and would be open to arguments in favor of something different. |
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:
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). |
This is highly desirable! I dropped automatic id generation for 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' Spontaneous idea:
Result:
// edit:
Result:
|
Could definitely use something like this. For now I use a context with a 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 }; |
What does building this into React achieve compared to @ryanflorence's solution? |
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 |
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. |
Why can't community standardize on a single "provider" for this?
Yep that's what I'm thinking. |
I think that React itself should unopinionated about generating and handling |
@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? |
@ryanflorence: @probablyup is right that auto-incrementers break when rendering is async or for some reason happens in different order on server and client. |
It would be really useful to have something like this: import { useId } from "react";
function Component() {
const id = useId();
return <div id={id} />;
} |
@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? |
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:
Here is a short sample on how // 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! |
This feature is not possible. You just can't use just use Solution:
<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 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. |
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 And ofc random id breaks with SSR. I believe there's a strong motivation behind the approach I take:
And now:
There's definitely sth missing here. Solution could be 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. |
@r00dY please don't do it. You don't need it.
|
It looks like an implementation has been proposed: facebook/react#17322 |
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. |
@gaearon - could you provide some clarification, please.
|
Would you mind opening an issue about this?
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.)
Also worth discussing in an issue. |
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. So the only problem left is with two reacts on one page. The standard solution for this is |
@theKashey Let's discuss current implementation in a React issue. This would be a bit easier for us to track. |
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 |
Thus we have to talk about |
@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? |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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
?
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 |
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. |
An update: reactwg/react-18#111 |
Thank you for the proposal and discussion. We've merged #212 which includes It fulfills the same role as this RFC, so let's close this one.
|
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