recompose icon indicating copy to clipboard operation
recompose copied to clipboard

Support ref-forwarding

Open andrewgreenh opened this issue 7 years ago • 6 comments

Hello together! Since the newest React-Version forwarding of refs is supported. Should this be used in this library? This would make the recompose HOCs truly transparent. This would be a breaking change of the API, because some users might be using the lifecycle HOC to add public methods to their instances. This would no longer work, when all recompose HOCs forward refs.

andrewgreenh avatar Apr 05 '18 15:04 andrewgreenh

Just use React.forwardRef when it is needed. Wrap any enhanced component whenever needed as like as in React doc https://reactjs.org/docs/forwarding-refs.html What the reason to provide ref forwarding for every enhancer? refs are rarely used thing. There are other enhancers libraries , custom enhancers - all that break composability - some enhancers forwardRefs, some are not. So React.forwardRef api looks the best.

istarkov avatar Apr 05 '18 15:04 istarkov

Hey, everyone! So what is exactly the best way to make forwardRef() work with recompose'd components? F.ex :

const Comp = React.forwardRef(({focused, ...rest}, ref) => <input className={classnames(styles.comp, focused && styles.focused)} ref={ref} />);

const EnhancedComp = compose(
  withState('focused', 'setFocused', false),
  mapProps(({ setFocused, ...rest }) => ({
    onBlur: () => setFocused(false),
    onFocus: () => setFocused(true),
    ...rest
  }))
)(Comp);

// Won't work like this
<EnhancedComp {...props} ref={someRef} /> 
// ...because ref is not a prop and won't be passed along with props through wrappers.

// This also won't work
React.forwardRef((props, ref) => <EnhancedComp {...props} ref={ref}/>)
// ...because ref captures top-most wrapper instead of <input>

Rom325 avatar May 16 '18 13:05 Rom325

Indeed, reading documentation helps! I've found out a way to do this.

const Comp = ({focused, forwardedRef, ...rest}) => <input className={classnames(styles.comp, focused && styles.focused)} ref={forwardedRef} />);

const EnhancedComp = compose(
  withState('focused', 'setFocused', false),
  mapProps(({ setFocused, ...rest }) => ({
    onBlur: () => setFocused(false),
    onFocus: () => setFocused(true),
    ...rest
  }))
)(Comp);

// This works! Because forwardedRef is now treated like a regular prop.
const EnhancedWithRef = React.forwardRef(({...props}, ref)) => <EnhancedComp {...props} forwardedRef={ref} />);

Rom325 avatar May 18 '18 12:05 Rom325

Hi. Just faced same issue, that is not possible to workaround. I think issue is in branch HOC.

When using together with another HOC, that adds ref, I'm not sure how I can workaround it, except adding toClass above branch.

For example, DragLayer from react-dnd adds ref to its wrapped component: https://github.com/react-dnd/react-dnd/blob/master/packages/react-dnd/src/DragLayer.tsx#L105

This gives refs warning:

compose(
  DragLayer,
  branch,
);

This does not gives refs warning:

compose(
  DragLayer,
  toClass,
  branch,
);

ipanasenko avatar Jun 18 '18 12:06 ipanasenko

This is relatively easy to add support in your own code base, but I think it'd be nice to have as a helper here as well. Here's what I'm doing right now:

const enhance: HOC<*, EnhancedProps> = compose(
  forwardRefs(),
  mapProps(/* ... */),
  restoreRefs(),
);
/* @flow */
/* eslint-disable react/no-multi-comp */

import React, {
  type ComponentType,
} from 'react';

import {
  getDisplayName,
  compose as baseCompose,
  type HOC,
} from 'recompose';

import hoistStatics from 'hoist-non-react-statics';

export const compose: $Compose = ((...funcs: *) => (
  baseCompose(
    forwardRefs(),
    ...funcs,
    restoreRefs(),
  )
): any);

export const forwardRefs = <Base, Enhanced>({
  propName = 'forwardedRef',
}: {
  propName: string,
} = {}): HOC<Base, Enhanced> => ((
  Component: ComponentType<Base>,
): ComponentType<Enhanced> => {
  const forwarder = (props: *, ref: *) => (
    <Component { ...{ [propName]: ref, ...props } } />
  );

  const Container = (React: any).forwardRef(forwarder);
  const displayName = getDisplayName(Component);

  forwarder.displayName = displayName;
  Container.displayName = displayName;

  hoistStatics(Container, Component);

  return Container;
});

export const restoreRefs = <Base, Enhanced>({
  propName = 'forwardedRef',
}: {
  propName: string,
} = {}): HOC<Base, Enhanced> => ((
  Component: ComponentType<Base>,
): ComponentType<Enhanced> => {
  const Container = ({ [propName]: ref, ...props }: *) => (
    <Component ref={ ref } { ...props } />
  );

  Container.displayName = getDisplayName(Component);

  return (Container: any);
});

wbyoung avatar Aug 13 '18 06:08 wbyoung

After much searching, I ended up creating a withForwardingRef

const withForwardingRef = <Props extends {[_: string]: any}>(BaseComponent: React.ReactType<Props>) =>
    React.forwardRef((props, ref) => <BaseComponent {...props} forwardedRef={ref} />);

export default withForwardingRef;

usage:

const Comp = ({forwardedRef}) => (
 <button ref={forwardedRef} />
)
const MyBeautifulComponent = withForwardingRef<Props>(Comp);  // Now Comp has a prop called forwardedRef

usage of usage:

<MyBeautifulComponent ref={someRef} />

oreporan avatar Nov 22 '18 19:11 oreporan