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

[Question] How to properly define namespaced react components? #165

Closed
bryceosterhaus opened this issue Nov 4, 2019 · 12 comments
Closed
Assignees
Labels
good first issue Good for newcomers

Comments

@bryceosterhaus
Copy link
Contributor

I typically use namespacing in my react components to group similar components. I was wondering what the best approach to defining a type here would be. I generally do something like the example below, however using as ... isn't ideal for me. Any other ideas how to properly type a react component that also has namespaced components?

interface IProps extends React.HTMLAttributes<HTMLDivElement> {
	someProp?: any;
}

type TMyComponent = React.ForwardRefExoticComponent<IProps> & {
	ChildComponent: typeof ChildComponent;
};

import ChildComponent from './ChildComponent';

const MyComponent = React.forwardRef(
	({someProp, ...otherProps}: IProps, ref) => {
		return <div {...otherProps} />;
	}
) as TMyComponent;

MyComponent.ChildComponent = ChildComponent;

export default MyComponent;
@swyxio
Copy link
Collaborator

swyxio commented Nov 4, 2019

seems fine. i rarely use forwardRef :)

@bryceosterhaus
Copy link
Contributor Author

Unfortunately we have to use lots of forwardRefs since its for a component library. Reason this came up is because this sort of type declaration doesnt work well with react-docgen. We need to also send a fix on their end, but just wanted to make sure I wasn't missing anything on the TS side.

Thanks for your help!

@swyxio
Copy link
Collaborator

swyxio commented Nov 4, 2019

cc @ferdaber this seems in your wheelhouse

@ferdaber
Copy link
Collaborator

ferdaber commented Nov 5, 2019

It's a little bit annoying but what you can do is use Object.assign. It works for both default and non default exports, but kind of messes with the components' display names (you may have to assign them manually instead of relying on Function.prototype.name.

import ChildComponent from './ChildComponent';
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
	someProp?: any;
}

const _MyComponent = React.forwardRef(
	({someProp, ...otherProps}: IProps, ref: React.Ref<HTMLDivElement>) => {
		return <div {...otherProps} ref={ref} />;
	}
)
_MyComponent.displayName = 'MyComponent'

type TMyComponent = React.ForwardRefExoticComponent<IProps> & {
	ChildComponent: typeof ChildComponent;
};
// this will have type TMyComponent, or more specifically
//  `typeof _MyComponent & { ChildComponent: typeof ChildComponent }`
const MyComponent = Object.assign(_MyComponent, { ChildComponent })
export default MyComponent;

@bryceosterhaus
Copy link
Contributor Author

@ferdaber thanks for the feedback! I'll give this a try and see how it looks.

@ferdaber
Copy link
Collaborator

ferdaber commented Nov 5, 2019

You can also create a helper function to do all of this:

import ChildComponent from './ChildComponent'
function createNamespacedComponent<T extends React.JSXElementConstructor<any>, U>(getComponent: () => T, namespaceMembers: U): T & U {
  const NamespaceComponent = getComponent();
  return Object.assign(NamespaceComponent, namespaceMembers);
}

const MyComponent = createNamespaceComponent(
  () => {
    const MyComponent = React.forwardRef(
	({someProp, ...otherProps}: IProps, ref: React.Ref<HTMLDivElement>) => {
		return <div {...otherProps} ref={ref} />;
	}
    )
    MyComponent.displayName = 'MyComponent'
    return MyComponent
  },
  { ChildComponent }
)

@bryceosterhaus
Copy link
Contributor Author

bryceosterhaus commented Nov 5, 2019

Using object.assign turned out working well. It also looks pretty clean and the TS inference seems to work better than my previous implementation. Here is the general idea of what it looks like now

//...
const Input = React.forwardRef<HTMLInputElement, IProps>((props, ref) => (
  <input {...props} ref={ref} />
));

export default Object.assign(Input, {
  Group,
  GroupInsetItem,
  GroupItem,
  GroupText
});

@swyxio
Copy link
Collaborator

swyxio commented Nov 5, 2019

damn that looks pretty sweet! can you PR this in somewhere that fits pls @bryceosterhaus

@swyxio swyxio reopened this Nov 5, 2019
@bryceosterhaus
Copy link
Contributor Author

#166, let me know if you need me to add anything else.

@swyxio
Copy link
Collaborator

swyxio commented Nov 5, 2019

resolved! this went great i think!

@swyxio swyxio closed this as completed Nov 5, 2019
@christopher-francisco
Copy link

Say I wanted to type this, I'm trying the following with no luck:

export type NamespacedComponent = React.FC<any> & {
  [name: string]: React.FC<any>;
};

const namespaced: NamespacedComponent = Object.assign(ButtonGroup, {
  Item
});

It won't compile saying

  Type 'FC<Props> & { Item: FC<Props>; }' is not assignable to type '{ [name: string]: FC<any>; }'.
    Index signature is missing in type 'FunctionComponent<Props> & { Item: FC<Props>; }'.

What am I missing

@swyxio
Copy link
Collaborator

swyxio commented Mar 11, 2021

  1. @christopher-francisco it would make it easier to help you if you provided a ts playground for us to repro

  2. dont use React.FC :)

  3. try the advice in https://react-typescript-cheatsheet.netlify.app/docs/advanced/misc_concerns/#namespaced-components

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

No branches or pull requests

4 participants