Skip to content

Instantly share code, notes, and snippets.

@ifiokjr
Forked from jaredpalmer/forwardRefWithAs.tsx
Created June 13, 2020 03:11
Show Gist options
  • Save ifiokjr/70771308bc515bb8ac0ccf0691fde75a to your computer and use it in GitHub Desktop.
Save ifiokjr/70771308bc515bb8ac0ccf0691fde75a to your computer and use it in GitHub Desktop.
forwardRefWithAs
import * as React from 'react';
/**
* React.Ref uses the readonly type `React.RefObject` instead of
* `React.MutableRefObject`, We pretty much always assume ref objects are
* mutable (at least when we create them), so this type is a workaround so some
* of the weird mechanics of using refs with TS.
*/
export type AssignableRef<ValueType> =
| {
bivarianceHack(instance: ValueType | null): void;
}['bivarianceHack']
| React.MutableRefObject<ValueType | null>
| null;
////////////////////////////////////////////////////////////////////////////////
// The following types help us deal with the `as` prop.
// I kind of hacked around until I got this to work using some other projects,
// as a rough guide, but it does seem to work so, err, that's cool? Yay TS! 🙃
// P = additional props
// T = type of component to render
export type As<BaseProps = any> = React.ElementType<BaseProps>;
export type PropsWithAs<
ComponentType extends As,
ComponentProps
> = ComponentProps &
Omit<
React.ComponentPropsWithRef<ComponentType>,
'as' | keyof ComponentProps
> & {
as?: ComponentType;
};
export type PropsFromAs<
ComponentType extends As,
ComponentProps
> = (PropsWithAs<ComponentType, ComponentProps> & { as: ComponentType }) &
PropsWithAs<ComponentType, ComponentProps>;
export type ComponentWithForwardedRef<
ElementType extends React.ElementType,
ComponentProps
> = React.ForwardRefExoticComponent<
ComponentProps &
React.HTMLProps<React.ElementType<ElementType>> &
React.ComponentPropsWithRef<ElementType>
>;
export interface ComponentWithAs<ComponentType extends As, ComponentProps> {
// These types are a bit of a hack, but cover us in cases where the `as` prop
// is not a JSX string type. Makes the compiler happy so 🤷‍♂️
<TT extends As>(
props: PropsWithAs<TT, ComponentProps>
): React.ReactElement | null;
(
props: PropsWithAs<ComponentType, ComponentProps>
): React.ReactElement | null;
displayName?: string;
propTypes?: React.WeakValidationMap<
PropsWithAs<ComponentType, ComponentProps>
>;
contextTypes?: React.ValidationMap<any>;
defaultProps?: Partial<PropsWithAs<ComponentType, ComponentProps>>;
}
/**
* This is a hack for sure. The thing is, getting a component to intelligently
* infer props based on a component or JSX string passed into an `as` prop is
* kind of a huge pain. Getting it to work and satisfy the constraints of
* `forwardRef` seems dang near impossible. To avoid needing to do this awkward
* type song-and-dance every time we want to forward a ref into a component
* that accepts an `as` prop, we abstract all of that mess to this function for
* the time time being.
*
* TODO: Eventually we should probably just try to get the type defs above
* working across the board, but ain't nobody got time for that mess!
*
* @param Comp
*/
export function forwardRefWithAs<Props, ComponentType extends As>(
comp: (
props: PropsFromAs<ComponentType, Props>,
ref: React.RefObject<any>
) => React.ReactElement | null
) {
return (React.forwardRef(comp as any) as unknown) as ComponentWithAs<
ComponentType,
Props
>;
}
/*
Test components to make sure our dynamic As prop components work as intended
type PopupProps = {
lol: string;
children?: React.ReactNode | ((value?: number) => JSX.Element);
};
export const Popup = forwardRefWithAs<PopupProps, 'input'>(
({ as: Comp = 'input', lol, className, children, ...props }, ref) => {
return (
<Comp ref={ref} {...props}>
{typeof children === 'function' ? children(56) : children}
</Comp>
);
}
);
export const TryMe1: React.FC = () => {
return <Popup as="input" lol="lol" name="me" />;
};
export const TryMe2: React.FC = () => {
let ref = React.useRef(null);
return <Popup ref={ref} as="div" lol="lol" />;
};
export const TryMe4: React.FC = () => {
return <Popup as={Whoa} lol="lol" test="123" name="boop" />;
};
export const Whoa: React.FC<{
help?: boolean;
lol: string;
name: string;
test: string;
}> = props => {
return <input {...props} />;
};
*/
// export const TryMe3: React.FC = () => {
// return <Popup as={Cool} lol="lol" name="me" test="123" />;
// };
// let Cool = styled(Whoa)`
// padding: 10px;
// `
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment