Howdy ya'll,
tl dr: please provide a way to coordinate pseudo-random identifiers across the client and server
This issue has been discussed a bit before (#1137, #4000) but I continually run into this issue, trying to build libraries that provide accessible components _by default_. The react component model generally speaking offers a big opportunity to raise the often low bar for accessibility in the library and widget world, see experiments like @ryanflorence's react-a11y.
For better or for worse the aria, and a11y API's in the browser are heavily based on using ID's to link components together. aria-labelledby
, aria-describedby
, aria-owns
,aria-activedescendent
, and so on all need's ID's. In a different world we would just generate ids where needed and move on, however server-side rendering makes that complicated, because any generated ID is going to cause a mismatch between client/server.
We've tried a few different approaches to address some of this, one is making id's required props on components that need them. That gets kinda ugly in components that need a few id's but moreso it annoys users. Its unfortunate because if we could generate deterministic id's we could just provide more accessible components by default.
The frustrating part is that the component generally has all the information it needs to just set the various aria info necessary to make the component usable with a screen reader, but are stymied by not having the user provide a bunch of globally unique ids'
So far only really reasonable approaches I've seen are @syranide's solution of a root ID store, and using _rootID
. The latter obviously has problems. The former doesn't scale well for library authors. Everyones' root App component is already wrapped in a Router, Provider, etc, having every library use their own root level ID provider is probably not super feasible and annoying to users.
It seems like the best way to do this would be if React (or a React addon) could just provide a consistent first class way to get a unique identifier for a component, even if it is just a base64 of the node's _rootID.
thanks for all the hard work everyone!
I currently solve this by generating a UID in a components constructor, which is then used as the basis for all my DOM IDs -- as seen here: https://github.com/titon/toolkit/blob/3.0/src/Component.js#L115
I then pass this UID along to all the children via contexts, which allows them to stay consistent: https://github.com/titon/toolkit/blob/3.0/src/components/accordion/Header.js#L62 I would then end up with IDs like titon-s7h1nks-accordion-header-1
and titon-s7h1nks-accordion-section-1
.
IMO, this kind of functionality doesn't really need to be part of React.
I think your solution still suffers from the exact problem I'm talking about here. Your code seems to depend on this generateUID
function which would break server rendering, since the uid generated on the server, is not going to be the one generated on the client, random numbers and all.
Pulling from #4000, which is a bit long... I believe the major criteria were:
.addons
)id
sFrom #1137,
We try to avoid adding functionality to React if it can be easily replicated in component code.
Seems it's gotten a bit beyond that now :)
@jquense Then simply write a UID generator function that returns the same value on the server or the client.
generateUID() {
return btoa(this.constructor.name);
}
@milesj thanks for the suggestions tho in this case that is only helpful if you have one instance of a component on the page.
@rileyjshaw I think that sums up the issue very succinctly, thank you. The best solution is the root store that passes ID's via context, but that's a bit cumbersome for each library to invent and provide unless there was some consensus in the community.
You're over-thinking this a bit. All the reasons you keep stating are easily fixable. Assuming that the server and client render everything in the same order (not sure why they wouldn't), just have a separate function module that generates and keeps track of things.
let idCounter = 0;
export default function generateUID(inst) {
return btoa(inst.constructor.name) + idCounter++;
}
And in the component.
import generateUID from './generateUID';
class Foo extends React.Component {
constructor() {
super();
this.uid = generateUID(this);
}
}
And if for some reason you want individually incrementing counters per component, then just use a WeakMap
in the function module.
@milesj I appreciate your suggestions and efforts at a solution but you may not be familiar enough with the problem. The simple incrementing counter doesn't work, if only because ppl don't usually render exactly the same thing on the client and server. I think you might want to read the linked issues, specifically #4000. I am well aware of the issues and the possible solutions, if there was an simple straightforward solution that worked I wouldn't be here.
I think @jquense makes a valid point about us needing a good solution here - it is currently overly painful to coordinate psudorandom identifiers for component. The problem is exacerbated when different components choose different solutions for solving the coordination problem. We see the exact same problem when components use the clock (for instance, to print relative times). We also see similar use cases for things like sharing flux stores across component libraries. Anyway, I think it would be good to find a real/supported solution for this use case.
What about React.getUniqueId(this)
which makes an id based on the key path + an incrementing counter for that key path. ReactDOMServer.renderToString could reset the hash of paths to counter ids.
Sorry if this comment is ignorant. I'm a bit of a noob, but if the key path is always unique and immutable, would we need a counter at all? So, in short:
React.getUniqueId(this[, localId])
This would return a unique id based on the key path, and the optional second argument, if provided. The function would be pure, in that it would always return the same output given the same inputs, I.E. no counter.
If more than one ID was needed within the same component, the optional second argument could be provided to differentiate between the two.
Of course if key paths aren't immutible and unique, then this obviously won't work.
@jimfb did you, or the rest of ya'll at FB have any thoughts about a potential API for this sort thing?
I might be able to PR something but not quite sure what everyone thinks the reach should be. Coordinating identifiers is a different sort of thing that coordinating a some sort of server/client cache, which is what suggests itself to me thinking about the Date or flux store use-case.
Here's what I did:
let idCounter = (typeof window !== 'undefined' && window.__ID__) || 0;
function id() {
return ++idCounter;
}
export default id;
const seed = id();
const rendered = renderToString(<DecoratedRouterContext {...renderProps}/>);
const markup = `
<div id="container">${rendered}</div>
<script type="text/javascript" charset="UTF-8">window.__ID__ = ${JSON.stringify(seed)};</script>
`;
in my components I just
import id from '../utils/id.js';
render() {
nameID = id();
return (
<div>
<label htmlFor={nameId}>Name</label>
<input id={nameId} />
</div>
);
}
I agree that it would be nice if react provided a standard way of syncing values between client and server. That being said, I don't know what the API would look like and this was pretty easy to do.
I hacked up something recently that seems to do the trick. I noticed that when rendered server-side, the DOM nodes will all have the data-reactid
attribute set on them. So my uniqueIdForComponent
function will first check the rendered DOM node to see if that attribute is set. If it is, it will simply return that. If not, then it means we weren't rendered server-side and don't have to worry about keeping them in sync so we can just use an auto-increment approach suggested before. Code looks something like this:
let index = 0;
static uniqueIdForComponent(component)
{
let node = ReactDOM.findDOMNode(component);
if (node) {
if (node.hasAttribute("data-reactid")) {
return "data-reactid-" + node.getAttribute("data-reactid");
}
}
return `component-unique-id-${index++}`;
}
There is undoubtedly a more optimized way to do this, but from recent observation, the methodology appears to be sound; at least for my usage.
@n8mellis Unfortunately findDOMNode
will throw if called during or before the first client-side render.
I don’t see what kind of IDs could be provided by React that can’t be determined by the user code.
The simple incrementing counter doesn't work, if only because ppl don't usually render exactly the same thing on the client and server
That sounds like the crux of the problem? Rendering different things on the client and on the server is not supported, at least during the first client-side render.
Nothing prevents you, however, from first rendering a subset of the app that is the same between client and server, and then do a setState()
in componentDidMount()
or later to show the parts that are client-only. There wouldn’t be mismatch in this case.
From https://github.com/facebook/react/issues/4000:
The problem I'm running into, however, is that this causes the id attributes generated client-side to mismatch what was sent down by the server (the client-side fieldCounter restarts at 0 on every load, whereas the server-side reference obviously just keeps growing).
The solution is to isolate the counter between SSR roots. For example by providing getNextID()
via context. This is what was roughly suggested in https://github.com/facebook/react/issues/4000 too.
So I think we can close this.
@gaearon I don't know that the issue is that it _can't_ be solved outside of react, but the React implementing it can go a long way towards improving the ergonomics of implementing a11y in React apps. For instance if there was a react API, we could use it in react-bootstrap, and not have to require a user to add a seemingly-to-them unnecessary umpteenth Provider component to their root, to use RB's ui components. Plus if others are going to do it thats _x_ more IdProvider
components.
Admittedly, this (for me anyway) is a DX thing, but I think that is _really_ important for a11y adjacent things. A11y on the web is already an uphill battle that most just (sadly) give up on, or indefinitely "defer" until later. Having a unified way to identify components would go a long way in react-dom to reducing one of big hurdles folks have implementing even basic a11y support, needing to invent consistent, globally unique, but semantically meaningless identifiers. It's not clear to me at this point that React is necessarily better positioned to provide said identifiers (I think in the past it was a bit more), but it certainly can help reduce friction in a way userland solutions sort of can't.
I think to progress further this would need to be an RFC.
It's kind of vague right now so it's hard to discuss.
https://github.com/reactjs/rfcs
If someone who's directly motivated to fix it (e.g. a RB maintainer :-) could think through the API, we could discuss it and maybe even find other use cases as well. (I agree it's annoying to have many providers, @acdlite also mentioned this in other context a few weeks ago.)
that's fair! I'll see if i can get some time to put a proper RFC together
@jquense I'd love to be involved in the RFC as well. I have a vested interest in seeing something like this come to fruition.
@n8mellis if you want to take the lead on it, i'd be happy to contribute. I'm not particularly rich in time at the moment, but i'd be happy to help out :)
Okay. I'll see what I can come up with and then send it around for some feedback.
Has any progress been made on this? I cannot get UUID's working when using SSR. I always end up with a conflicting id:
Using UUIDv4:
I'll be working on the RFC today.
@jquense I submitted the first draft of an RFC for this. Would love to get your feedback. https://github.com/reactjs/rfcs/pull/32
I did it with Redux Store: could be a middleware
or just a function in store state
uid.js
function generateID() {
let counter = 0;
return function(prefix) {
return (prefix || 'uid') + '--' + counter++;
}
}
Store Generator
import generateUID from './uid';
const generateStore = preloadedState => {
const getUID = new generateUID();
return createStore(
appReducers,
preloadedState,
compose(
applyMiddleware(
...middlewares,
// MIDDLEWARE FOR UNIQUE ID HERE
store => next => action => {
if (action.type === actionTypes.GET_UNIQUE_ID) {
return getUID(action.payload);
} else {
return next(action);
}
}
)
)
);
}
// Client.js
const store = generateStore();
...
// Server.js
function(req,res,next) {
const store = generateStore();
}
...
uidAction.js
// payload: prefix parameter
export function getUID(payload) {
return {
type: actionTypes.GET_UNIQUE_ID,
payload
}
}
Somewhere in Component with connect
<div id={props.getUID('hello-world')}>Hello World</div>
By doing this, Client Store
and Server Store
in every request will have same counter
outcome when is called. Using Redux connect
we can access to this action everywhere.
See theKashey comment below.
It's a bit strange to read such long conversation, when @milesj described the "right" solution years ago:
I then pass this UID along to all the children via contexts, which allows them to stay consistent: https://github.com/titon/toolkit/blob/3.0/src/components/accordion/Header.js#L62 I would then end up with IDs like
titon-s7h1nks-accordion-header-1
andtiton-s7h1nks-accordion-section-1
.
prefix=youPrefix+youId
to your child and reset counter. _So repeat the .1, with a "longer" prefix_.https://github.com/thearnica/react-uid is implementing all there features, while maria-uid
is bound to the render order of the components, which depends on code-splitting, the order data got loaded, a moon phase and so on.
@theKashey that's because everyone wanted to note their idea to remind the community and themselves, just like you and me so the topic is long. This unique UID with SSR friendly is just a simple trick but worth for noting down.
I've tried to store id counter in the local scope of the component but it seems won't work too:
let idCounter = 0
export const TextField = ({ id }) => {
return <input id={id || `text-field-${++idCounter}`} />
}
Just to follow up on this, we are testing a potential solution to this internally but not ready to draft an RFC yet.
Im wrote hook for generate ID with SSR supports. Gist with example — https://gist.github.com/yarastqt/a35261d77d723d14f6d1945dd8130b94
@yarastqt There are issues your solution does not cover (it never resets the SSR counter for example), see react-uid instead.
@Merri it's not true, i increase counter only on client side
Most helpful comment
Just to follow up on this, we are testing a potential solution to this internally but not ready to draft an RFC yet.