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

Generating unique ID's and SSR (for a11y and more) #5867

Closed
jquense opened this issue Jan 16, 2016 · 37 comments
Closed

Generating unique ID's and SSR (for a11y and more) #5867

jquense opened this issue Jan 16, 2016 · 37 comments

Comments

@jquense
Copy link
Contributor

jquense commented Jan 16, 2016

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!

@milesj
Copy link
Contributor

milesj commented Jan 16, 2016

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.

@jquense
Copy link
Contributor Author

jquense commented Jan 16, 2016

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.

@rileyjshaw
Copy link
Contributor

Pulling from #4000, which is a bit long... I believe the major criteria were:

  • unique
  • consistent between client & server
  • consistent over time
  • baked in (or in .addons)
  • encapsulated: not dependent on knowledge of / access to the full tree
  • less cumbersome than some of the presently demonstrated solutions
  • shouldn't require users to manually manage & pass in their own ids

From #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 :)

@milesj
Copy link
Contributor

milesj commented Jan 17, 2016

@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);
}

@jquense
Copy link
Contributor Author

jquense commented Jan 17, 2016

@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.

@milesj
Copy link
Contributor

milesj commented Jan 17, 2016

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.

@jquense
Copy link
Contributor Author

jquense commented Jan 17, 2016

@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.

@jimfb
Copy link
Contributor

jimfb commented Jan 17, 2016

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.

@brigand
Copy link
Contributor

brigand commented Jan 17, 2016

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.

@cannona
Copy link
Contributor

cannona commented Feb 1, 2016

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.

@jquense
Copy link
Contributor Author

jquense commented Apr 11, 2016

@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.

@bjornstar
Copy link

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.

@n8mellis
Copy link

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.

@PaulKiddle
Copy link

@n8mellis Unfortunately findDOMNode will throw if called during or before the first client-side render.

@gaearon
Copy link
Collaborator

gaearon commented Jan 2, 2018

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 #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 #4000 too.

So I think we can close this.

@gaearon gaearon closed this as completed Jan 2, 2018
@jquense
Copy link
Contributor Author

jquense commented Jan 2, 2018

@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.

@gaearon
Copy link
Collaborator

gaearon commented Jan 2, 2018

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.)

Joozty added a commit to oacore/design that referenced this issue Aug 17, 2020
This never really worker for SSR applications because different ID is generated on server side than on client side. There are some ways to make it work properly but it looks too complicated. Let the user pass it down.

See more:
facebook/react#5867
Joozty added a commit to oacore/design that referenced this issue Aug 19, 2020
This never really worker for SSR applications because different ID is generated on server side than on client side. There are some ways to make it work properly but it looks too complicated. Let the user pass it down.

See more:
facebook/react#5867
@shpindler
Copy link

shpindler commented Aug 21, 2020

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}`} />
}

@gaearon
Copy link
Collaborator

gaearon commented Aug 21, 2020

Just to follow up on this, we are testing a potential solution to this internally but not ready to draft an RFC yet.

@yarastqt
Copy link

Im wrote hook for generate ID with SSR supports. Gist with example — https://gist.github.com/yarastqt/a35261d77d723d14f6d1945dd8130b94

@Merri
Copy link

Merri commented Sep 14, 2020

@yarastqt There are issues your solution does not cover (it never resets the SSR counter for example), see react-uid instead.

@yarastqt
Copy link

@Merri it's not true, i increase counter only on client side

@nemoDreamer
Copy link

Tangential to this thread, if anyone is interested, I wrote a lil' context/hook that returns an RNG seeded by the value you pass into its provider, and it's served me well in keeping random values between server and client:

import React, { useContext } from "react";
import seedrandom from "seedrandom";

const Random = React.createContext("seed");

const rngCache: {
  [key: string]: ReturnType<seedrandom>;
} = {};

export const useRandom = (): [ReturnType<seedrandom>, string] => {
  const seed = useContext(Random);

  let rng = rngCache[seed];
  if (!rng) {
    rng = rngCache[seed] = seedrandom(seed);
  }

  return [rng, seed];
};

export default Random;

In your parent component:

<Random.Provider value="your seed">
  {/* ... */}
</Random.Provider>

Then, in your nested components:

const [rng] = useRandom();

// `rng.quick()`
// etc...

Hope it's useful to someone! 😄

@phegman
Copy link

phegman commented Feb 28, 2022

I am using @reach/auto-id and it seems to be working well out of the box for me!

@eps1lon
Copy link
Collaborator

eps1lon commented Mar 5, 2022

There are plans currently to ship useId in React 18 which should resolve this request. If there's anything missing please continue the discussion in reactjs/rfcs#32

@gaearon
Copy link
Collaborator

gaearon commented Mar 29, 2022

useId() is out in React 18.

https://reactjs.org/blog/2022/03/29/react-v18.html#useid

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

No branches or pull requests