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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useUniqueId hook and fix several needlessly incrementing ids #2079

Merged
merged 4 commits into from Sep 5, 2019

Conversation

BPScott
Copy link
Member

@BPScott BPScott commented Sep 5, 2019

WHY are these changes introduced?

We want components to have stable ids between rerenders, otherwise their ids will constantly increment on every render which leads to additional wasteful dom reconciliation.

Currently that's not easily possible with functional components.

WHAT is this pull request doing?

Adds a useUniqueId hook that returns an ever incrementing ID when called multiple times but remains constant through rerenders. See the hook tests for examples of how this works in practice.

This hook returns a stable value across multiple rerenders, and can optionally be overridden with a fixed value for components that allow the user to provide their own id.

Use this hook in several functional components that currently have unstable ids that change between rerenders.

The idFactory for this hook is populated in the AppProvider, with eye to eventually allowing consumers to override the idFactory so it can be reset between server renders (making this public and configurable is a follow up step). See #1761 for info where we should go onwards with that. Eventually having this be a standalone library in quilt would be good but I've not got the time to split this out at the moment so I'm doing something just within Polaris for now

What comes next?

This converts all the currently functional components that use ids to use this hook. Class based components which call their generateId function within a render instead of calling it when initializing a private instance variable also exhibit this behaviour. We could either fix those now, or as part of migrating those components to use hooks - I'm leaning towards the latter.

We will want to make the generator factory we pass into the UniqueIdFactory in the AppProvider that gets passed into the by the consumer so that it can be reset on each server render to complete #1761

How to 馃帺

The following playground contains a bunch of components that trigger rerenderings of a component on a once per second basis thanks to setInterval incrementing a counter. See that only thing that updates within each component is the count content and that any id values remain constant across multiple renderings of the same component.

You can compare this by running this playground on master (but commenting out the Test component stuff) and seeing that various id values within the components also update.

Copy-paste this code in playground/Playground.tsx:
import React, {useState, useRef, useEffect} from 'react';
import {
  Page,
  RadioButton,
  ChoiceList,
  FormLayout,
  OptionList,
  Select,
} from '../src';
import {useUniqueId} from '../src/utilities/unique-id';

function noop() {}

export function Playground() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount((count) => count + 1);
  }, 1000);

  return (
    <Page title="Playground">
      <Test renderCount={count} />
      <hr />
      <Test renderCount={count} />
      <hr />
      <Test renderCount={count} />
      <hr />
      <hr />

      <ChoiceList
        title={count.toString()}
        choices={[
          {label: 'Hidden', value: 'hidden'},
          {label: 'Optional', value: 'optional'},
          {label: 'Required', value: 'required'},
        ]}
        selected={[]}
      />

      <hr />

      <FormLayout.Group helpText="help">Group {count}</FormLayout.Group>

      <hr />

      <hr />

      <OptionList
        title={count.toString()}
        options={[
          {label: 'Hidden', value: 'hidden'},
          {label: 'Optional', value: 'optional'},
          {label: 'Required', value: 'required'},
        ]}
        selected={[]}
        onChange={noop}
      />

      <hr />

      <RadioButton label={count} value="hidden" />

      <hr />

      <Select
        label={count.toString()}
        options={[
          {label: 'Hidden', value: 'hidden'},
          {label: 'Optional', value: 'optional'},
          {label: 'Required', value: 'required'},
        ]}
      />
    </Page>
  );
}

function Test({renderCount}) {
  // const id = useUniqueId();
  const id = useUniqueId();
  const idPrefix = useUniqueId('prefix-');
  const idPrefix2 = useUniqueId('prefix2-');

  return (
    <div>
      {renderCount} :: {id} | {idPrefix} | {idPrefix2}
    </div>
  );
}

function useInterval(callback: () => void, delay: number) {
  const savedCallback = useRef<typeof callback>();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

This hook returns a stable value across multiple rerenders, and can
optionally be overridden with a fixed value

Use this hook in several functional components that currently have
unstable ids that change between rerenders.

Populate the idFactory of this hook in the AppProvider, with eye to
eventually allowing consumers to override the idFactory so it can be
reset between server renders (making this public and configurable is a
follow up step)
@BPScott BPScott changed the title Add useUniqueId hook Add useUniqueId hook and fix several needlessly incrementing ids Sep 5, 2019
UNRELEASED.md Outdated Show resolved Hide resolved
Copy link
Contributor

@dleroux dleroux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor suggestions for consistency but overall this is great! Thank you for doing this Ben 馃帀

src/utilities/unique-id/unique-id-factory.ts Outdated Show resolved Hide resolved
src/utilities/unique-id/tests/hooks.test.tsx Outdated Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Outdated Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Outdated Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Outdated Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Outdated Show resolved Hide resolved
src/utilities/unique-id/hooks.ts Outdated Show resolved Hide resolved
src/utilities/unique-id/tests/hooks.test.tsx Show resolved Hide resolved
Copy link
Contributor

@amrocha amrocha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with Tim about tests being hard to read.

I also am a bit wary of overusing hooks, but I think this use case is fine. It's a simple hook that lets developers focus on the functionality they want to use instead of React internals, and prevents easy mistakes from happening.

- Use PascalCase for default global prefix
- Fix typos
- Rejig tests to create elements a little more like what we would write
  in real components
Copy link
Contributor

@amrocha amrocha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertions are still not easy to read, but they're just tests so it's fine

@BPScott BPScott merged commit cd2cb60 into master Sep 5, 2019
@BPScott BPScott deleted the use-unique-id-hook branch September 5, 2019 22:18
chloerice pushed a commit that referenced this pull request Sep 6, 2019
* Add useUniqueId hook

This hook returns a stable value across multiple rerenders, and can
optionally be overridden with a fixed value

Use this hook in several functional components that currently have
unstable ids that change between rerenders.

Populate the idFactory of this hook in the AppProvider, with eye to
eventually allowing consumers to override the idFactory so it can be
reset between server renders (making this public and configurable is a
follow up step)
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

Successfully merging this pull request may close these issues.

None yet

4 participants