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

Add useReactPath hook for reflection and stable IDs #15435

Closed
mike-marcacci opened this issue Apr 17, 2019 · 19 comments
Closed

Add useReactPath hook for reflection and stable IDs #15435

mike-marcacci opened this issue Apr 17, 2019 · 19 comments

Comments

@mike-marcacci
Copy link

Do you want to request a feature or report a bug?
Feature: this is a proposal that would solve #1137 and other similar cases by providing a reflection hook that retrieves the path in the react tree to a component.

What is the current behavior?
There is currently no mechanism for finding a component's path in the react component tree without using unstable, internal APIs.

What is the expected behavior?
There are a wide range of use-cases that require stable, unique namespaces that are stable across app instances.

My particular use case is rendering SVGs that include <defs> blocks. The challenge is that these definitions are all globally scoped. It's fairly easy to get around this by prefixing these definitions with an id that is stable and unique to the component instance:

import { useMemo, ReactElement } from "react";

function gen(): string {
  return Math.random()
    .toString(36)
    .substr(2, 5);
}

function useUniqueId(): string {
  return useMemo(gen, []);
}

function SomeComponent(): ReactElement {
  const id = useUniqueId();
  return (
    <svg>
      <defs>
        <circle id={`${id}-myCircle`} cx="0" cy="0" r="5" />
      </defs>

      <use x="5" y="5" xlink:href={`#${id}-myCircle`} fill="red" />
    </svg>
  )
}

While this works well, the generated ID is different each time a component is instantiated. This means that it's not possible to safely rehydrate server-rendered SVGs, as the HTML will have the wrong prefix.

Instead, I would like a hook that provides the react path to the current component. For example:

function SomeComponent(): ReactElement {
  const path = useReactPath();
  return <pre>{JSON.stringify(path, null, 2)}</pre>
}

// somewhere else:

<div>
  <div>Foo</div>
  <div>{[
    <div key="bar">Bar</div>,
    <div key="baz">
      Baz
      <SomeComponent />
    </div>
  ]}</div>
</div>

We would expect the path to be:

[
  1,     // after the <div>Foo</div> block
  0,     // the interpolation block
  "baz", // the array specifies keys
  1      // after the Baz text block
]

This array could then be used much like an XPath for testing automation purposes, or hashed into a unique ID or prefix, for purposes like mine.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
This has never been possible.

@nmain
Copy link

nmain commented Apr 18, 2019

The specific use case you've outlined here could be solved with a different useUniqueId implementation:

let currentId = 0;

export function useUniqueId() {
	const ref = useRef(0);
	if (ref.current === 0) {
		ref.current = ++currentId;
	}
	return "bonk-bonk-" + ref.current;
}

This will return the ids bonk-bonk-1, bonk-bonk-2, ... for each different component instance. While the numbers are not easily predictable, they should be deterministic. If the singleton currentId is a problem for processing multiple renderings in a single javascript context, you could have the ids be unique in the scope of a provider, or reset currentId for each request, or similar.

@mike-marcacci
Copy link
Author

Hi @nmain - thanks so much for the speedy and thoughtful reply! I actually use a similar pattern elsewhere, with currentId persisted in a provider and passed down via context (although I hadn't seen the useRef trick to checking initial mount, which is pretty cool).

My concern here is that the deterministic execution order is (as far as I know) just an effect of current implementation details in react-server, and is likely to change with the introduction of "concurrent mode". If this is incorrect, I'm more than happy to close this issue, as it would mean that my use case is already solved.

@leeseean
Copy link

you are dreaming

@nmain
Copy link

nmain commented Apr 19, 2019

@mike-marcacci That's a good point. The stability of useUniqueId, or any similarly written hook, can't be relied on in the long term.

@jfinity
Copy link

jfinity commented Jun 11, 2019

(shameless plug) @mike-marcacci , I don't have any react-sever experience in particular, but I just published a (poorly documented) package that might help with your problem -- or inspire a different solution -- https://www.npmjs.com/package/react-folder

Looking at your prior example, you could write something like:

import { createSystem } from "react-folder";

const [Folder, mkDir, useCWDRef] = createSystem({ separator: `","` });

const Div = mkDir(props => <div {...props} />);

render

<div>
  <div>Foo</div>
  <Div folder="1">{<Folder name="0">[
    <div key="bar">Bar</div>,
    <Div key="baz" folder="baz">
      Baz
      <SomeComponent folder="1" />
    </Div>
  ]</Folder>}</Div>
</div>

and observe conditions like the following

const SomeComponent = mkDir(() => {
  const cwd = useCWDRef();
  const path = `["${cwd()}"]`;
  if (path !== `["","1","0","baz","1",""]`) {
    throw "this should never happen -- " + path;
  }
  return <pre>{JSON.stringify(JSON.parse(path).slice(1, -1), null, 2)}</pre>;
});

I'll admit that it's very manual and it would be nice to have a canonical approach supported internally in React (esp. for compatibility between libraries), but I think there can also be benefits to being able to declare your own path hierarchies for different purposes -- perhaps: test automation ids, accessibility/analytics tagging, or external state management.

@stale
Copy link

stale bot commented Jan 10, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@mike-marcacci
Copy link
Author

This kind of repeatable, stable ID continues to be impossible without a solution that is aware of a components position in the tree. As concurrent moves towards stability, this is increasingly important, as it breaks the hacks that are currently in use.

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@jquense
Copy link
Contributor

jquense commented Jan 10, 2020

this is being handled in #17322 I believe @mike-marcacci

@mike-marcacci
Copy link
Author

@jquense very interesting; that does indeed address the same use-case, albeit in a way that requires re-rendering on mismatch. I see these as competing approaches that address the same problem, so if that route is chosen, this can be closed. However, I believe this may be a superior approach.

@mike-marcacci
Copy link
Author

If there is an interest in this approach and I can get a nod from someone on the react team that it's a direction that would be considered, I can go ahead and implement it.

@stale
Copy link

stale bot commented Apr 9, 2020

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Apr 9, 2020
@gaearon
Copy link
Collaborator

gaearon commented Apr 10, 2020

@mike-marcacci Can you describe in more detail why you consider this a better approach?

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Apr 10, 2020
@mike-marcacci
Copy link
Author

Hi @gaearon,

I haven't read through the most recent (merged) implementation of useOpaqueIdentifier so I may be arguing against a strawman here.

Basically, my primary motivation is similar: to allow creation of an identifier that is stable across serialization and hydration.

My implementation strategy is the result of answering this question: "What is naturally unique to a component instance that is already stable across serialization and hydration?" The answer to this is that the "path" in the virtual dom tree (taking "key" into consideration) is not only unique across serialization/hydration but is also the mechanism used internally by react to identify an "instance" of a component.

Either exposing an element's position directly as I've proposed, or using it as the basis for generating an opaque identifier, solves this problem without requiring us to keep track of any additional state. This is a pretty important feature in my eyes.

@mike-marcacci
Copy link
Author

OK, just skimmed through #17322 and I see that this state is being persisted via attributes on the serialized HTML. This is probably a pretty reasonable way to persist state here, although prefixing IDs with s_ and c_ based on the counter used (server/client) seems a bit less than ideal. There are probably performance benefits from using a counter rather than serializing a tree path anyway.

Either way it sounds like the merged approach solves my actual use-case, so I'll close this issue. Also, by creating a new hook primitive that's designed to be used as a "totally opaque identifier" it would be perfectly possible to switch out the underlying mechanism if there was ever a desire.

@gaearon
Copy link
Collaborator

gaearon commented Apr 10, 2020

With the strategy you're proposing, I think the length of IDs would keep growing the deeper we get in the tree. Is that a concern?

@gaearon
Copy link
Collaborator

gaearon commented Apr 10, 2020

(To be honest I don't have the background on the design in #17322 other than that the keypath was probably considered. After all, React did emit keypath in early versions as an attribute, although mostly as an implementation detail. But I'd also be curious to understand in more depth how these approaches compare.)

@mike-marcacci
Copy link
Author

@gaearon - ya, identifier length was something I considered too, and so this proposal was focused on exposing a primitive that could be used for generating such an identifier among other things. In my own experiments for this (and similar problems elsewhere) I relied on generating a stable hash, which is surprisingly performant especially given the fact that the value is subsequently memoized. However, it does lead to considerably longer identifiers (128 bits, for example) than a counter, which could be an issue if huge numbers of identifiers were used on the same page.

@gaearon
Copy link
Collaborator

gaearon commented Apr 10, 2020

Makes sense. I think another concern with using the keypath as a generic identifier is that the keypath will change over time. Either you'll only get the initial one (but then it might clash for a client-only newly mounted component that happened to mount in the same place at some later point). Or we'll have to issue state updates for every component that uses a keypath whenever any of its parents is reordered. That doesn't sound very promising.

@beaniemonk
Copy link

@mike-marcacci this was super interesting! I also had a need for stable unique IDs per component instance, and after a lot of experimentation and iteration, also landed on a very similar "path" approach (SSR wasn't a concern in our specific case), which I ran through imurmurhash and then memo'd. Pretty cool to see it discussed here.

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

No branches or pull requests

7 participants