Downshift: Support RefObjects for sub-components

Created on 13 May 2019  路  6Comments  路  Source: downshift-js/downshift

  • downshift version: 3.2.10
  • node version: 10.9.0
  • npm (or yarn) version: 6.2.0

Relevant code or config

const Select = () => {
  const menuRef = useRef(null);

  return (
    <Downshift>
      {({ getMenuProps, getInputProps }) => (
        <div>
          <input {...getInputProps()} />
          <div {...getMenuProps({ ref: menuRef })}>...options</div>
        </div>
      )}
    </Downshift>
  );
};

What you did:

I passed an object-style ref to getMenuProps

What happened:

I got an error:

downshift.esm.js:236 Uncaught TypeError: fn.apply is not a function
    at downshift.esm.js:236
    at Array.forEach (<anonymous>)
    at downshift.esm.js:234
    ...

Reproduction repository:

Problem description: Downshift tries to compose external refs, but it assumes all refs are functions.

Suggested solution: Augment ref composition to account for object-style refs. You can wrap Downshift's function ref in another function which calls it, then also sets externalRef.current = element (if external ref is an object) or calls external ref (if it's a function).

I'll create a PR soonish, if time allows. Otherwise this issue should help remind me that I need to.

enhancement

Most helpful comment

@a-type did you ever figure this out? I'm facing a similar issue with access the DOM node via ref.

Update:

For you internet travelers passing by you can do the following to get the DOM node:

const Select = () => {
    const menuRef = useRef(null);

    return (
        <Downshift>
            {({ getMenuProps, getInputProps }) => (
                <div>
                    <input {...getInputProps()} />
                    <div
                        {...getMenuProps({
                            ref: e => {
                                menuRef.current = e;
                            }
                        })}>
                        ...options
                    </div>
                </div>
            )}
        </Downshift>
    );
};

Supply a callback to getMenuProps and you'll have access to the menu. This is poorly documented.

All 6 comments

Can you specify what are you trying to achieve?

If you just want the ref, then pass a refKey to getMenuProps, and get it from the result using destructuring. Like this:

    const { innerRef, ...accessibilityMenuProps } = getMenuProps(
      { refKey: 'innerRef' },
      { suppressRefError: true },
    )

I'm trying to compose a ref through multiple different sources, including a hook. I'm developing a hook to use Popper.js.

Here's a full example I mocked up of the hook interacting with Downshift. It's not quite functional yet, but getting there:

const SelectDemo = ({ options = ['Apple', 'Orange', 'Bannana', 'Kiwi'] }) => {
  const [value, setValue] = React.useState(null);
  const anchorRef = React.useRef(null);
  const popperProps = usePopper<HTMLDivElement, HTMLInputElement>({
    anchorRef,
    placement: 'bottom',
    fullWidth: true,
    overflowPadding: 15,
    flip: true,
  });

  return (
    <Downshift onChange={setValue}>
      {({
        getInputProps,
        getItemProps,
        getLabelProps,
        getMenuProps,
        isOpen,
        inputValue,
        highlightedIndex,
        selectedItem,
      }) => (
        <div>
          <label {...getLabelProps()}>Enter a fruit</label>
          <input {...getInputProps({ ref: anchorRef })} />
          {isOpen && (
            <ul {...getMenuProps(popperProps)}>
              {options
                .filter(item => !inputValue || item.includes(inputValue))
                .map((item, index) => (
                  <li
                    {...getItemProps({
                      key: item,
                      index,
                      item,
                      style: {
                        backgroundColor:
                          highlightedIndex === index ? 'lightgray' : 'white',
                        fontWeight: selectedItem === item ? 'bold' : 'normal',
                      },
                    })}
                  >
                    {item}
                  </li>
                ))}
            </ul>
          )}
          <div>Value: {value}</div>
        </div>
      )}
    </Downshift>
  );
};

(I sorta forgot, I was using getInputProps, but the same principle applies here).

The basic idea is just to be able to establish a ref for my component and provide it to any libraries which need to use it.

I could theoretically use the ref created by Downshift, but that would require me to restructure and establish a new component inside the render prop in order to use a hook. I'm not very excited about changing component boundaries for the sake of technical quirks.

I had thought just passing a ref in would be fine, since Downshift seems to support providing a ref, but of course then I discovered this was only for function refs. If you already support the pattern, I don't think it would be so difficult to support all ref types.

const composeRefs = (ourRef, theirRef) => {
  if (typeof theirRef === 'function') {
    return (el) => {
      ourRef(el);
      theirRef(el);
    };
  } else {
    return (el) => {
      ourRef(el);
      theirRef.current = el;
    }
  }
};

I'd be happy to drop this function into the places refs are used via PR.

@a-type did you ever figure this out? I'm facing a similar issue with access the DOM node via ref.

Update:

For you internet travelers passing by you can do the following to get the DOM node:

const Select = () => {
    const menuRef = useRef(null);

    return (
        <Downshift>
            {({ getMenuProps, getInputProps }) => (
                <div>
                    <input {...getInputProps()} />
                    <div
                        {...getMenuProps({
                            ref: e => {
                                menuRef.current = e;
                            }
                        })}>
                        ...options
                    </div>
                </div>
            )}
        </Downshift>
    );
};

Supply a callback to getMenuProps and you'll have access to the menu. This is poorly documented.

@atomicpages thanks for your comment.

But there is an issue with TypeScript in this approach to set ref in a callback: React.RefObject doesn't allow to mutate its 'current' property.

Do you have any idea on how to deal with it without casting ref to type 'any'?

@victormovchan TS allows you to mutate current if you type it with | null:

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L932

@a-type
you're right, thanks

Was this page helpful?
0 / 5 - 0 ratings