facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.86k stars 46.51k forks source link

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

Closed jquense closed 6 years ago

jquense commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

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 commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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

gaearon commented 6 years ago

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.

jquense commented 6 years ago

@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 commented 6 years ago

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

jquense commented 6 years ago

that's fair! I'll see if i can get some time to put a proper RFC together

n8mellis commented 6 years ago

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

jquense commented 6 years ago

@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 :)

n8mellis commented 6 years ago

Okay. I'll see what I can come up with and then send it around for some feedback.

stevemk14ebr commented 6 years ago

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: capture

n8mellis commented 6 years ago

I'll be working on the RFC today.

n8mellis commented 6 years ago

@jquense I submitted the first draft of an RFC for this. Would love to get your feedback. https://github.com/reactjs/rfcs/pull/32

up209d commented 6 years ago

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.

Merri commented 6 years ago

See theKashey comment below.

theKashey commented 5 years ago

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 and titon-s7h1nks-accordion-section-1.

  1. Uid should not be unique(ie uuid), but might have a unique prefix, you may send from server to client. So store "prefix" and "counter" in a context.
  2. Uid should be nested, to stay consistent. Just provide a prefix=youPrefix+youId to your child and reset counter. So repeat the .1, with a "longer" prefix.
  3. Job is done.

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.

up209d commented 5 years ago

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

shpindler commented 4 years ago

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 commented 4 years ago

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

yarastqt commented 4 years ago

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

Merri commented 4 years ago

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

yarastqt commented 4 years ago

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

nemoDreamer commented 3 years ago

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 commented 2 years ago

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

eps1lon commented 2 years ago

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 https://github.com/reactjs/rfcs/pull/32

gaearon commented 2 years ago

useId() is out in React 18.

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