Skip to content

Commit 95d6c0a

Browse files
committed
Add polymorphic-react-component
1 parent fde23a6 commit 95d6c0a

32 files changed

+12939
-0
lines changed

polymorphic-react-component/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
HOST=0.0.0.0
2+
PORT=4848
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*

polymorphic-react-component/01.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const Text = <C extends React.ElementType>({
2+
as,
3+
children,
4+
}: {
5+
as?: C;
6+
children: React.ReactNode;
7+
}) => {
8+
const Component = as || "span";
9+
10+
return <Component>{children}</Component>;
11+
};

polymorphic-react-component/02.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type TextProps<C extends React.ElementType> = {
2+
as?: C;
3+
children: React.ReactNode;
4+
} & React.ComponentPropsWithoutRef<C>;
5+
6+
export const Text = <C extends React.ElementType>({
7+
as,
8+
children,
9+
...restProps
10+
}: TextProps<C>) => {
11+
const Component = as || "span";
12+
13+
return <Component {...restProps}>{children}</Component>;
14+
};

polymorphic-react-component/03.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type TextProps<C extends React.ElementType> = {
2+
as?: C;
3+
children: React.ReactNode;
4+
} & React.ComponentPropsWithoutRef<C>;
5+
6+
// Note the Generic defaulted to a "span". If no "as" prop is passed e.g., <Text>Hi</Text>, TS will treat the rendered element as a span and provide typings accordingly
7+
export const Text = <C extends React.ElementType = "span">({
8+
as,
9+
children,
10+
...restProps
11+
}: TextProps<C>) => {
12+
const Component = as || "span";
13+
14+
return <Component {...restProps}>{children}</Component>;
15+
};

polymorphic-react-component/04.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
type Rainbow =
2+
| "red"
3+
| "orange"
4+
| "yellow"
5+
| "green"
6+
| "blue"
7+
| "indigo"
8+
| "violet";
9+
10+
type TextProps<C extends React.ElementType> = {
11+
as?: C;
12+
color?: Rainbow | "black";
13+
};
14+
15+
type Props<C extends React.ElementType> = React.PropsWithChildren<
16+
TextProps<C>
17+
> &
18+
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
19+
20+
export const Text = <C extends React.ElementType = "span">({
21+
as,
22+
color,
23+
children,
24+
...restProps
25+
}: Props<C>) => {
26+
const Component = as || "span";
27+
28+
const style = color ? { style: { color } } : {};
29+
30+
// Note restProps being passed down as well
31+
return (
32+
<Component {...restProps} {...style}>
33+
{children}
34+
</Component>
35+
);
36+
};

polymorphic-react-component/05.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
type Rainbow =
2+
| "red"
3+
| "orange"
4+
| "yellow"
5+
| "green"
6+
| "blue"
7+
| "indigo"
8+
| "violet";
9+
10+
type AsProp<C extends React.ElementType> = {
11+
as?: C;
12+
};
13+
14+
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
15+
16+
type PolymorphicComponentProp<
17+
C extends React.ElementType,
18+
Props = {}
19+
> = React.PropsWithChildren<Props & AsProp<C>> &
20+
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
21+
22+
type TextProps = { color?: Rainbow | "black" };
23+
24+
export const Text = <C extends React.ElementType = "span">({
25+
as,
26+
color,
27+
children,
28+
}: PolymorphicComponentProp<C, TextProps>) => {
29+
const Component = as || "span";
30+
31+
const style = color ? { style: { color } } : {};
32+
33+
return <Component {...style}>{children}</Component>;
34+
};

polymorphic-react-component/06.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from "react";
2+
3+
type Rainbow =
4+
| "red"
5+
| "orange"
6+
| "yellow"
7+
| "green"
8+
| "blue"
9+
| "indigo"
10+
| "violet";
11+
12+
type PolymorphicRef<C extends React.ElementType> =
13+
React.ComponentPropsWithRef<C>["ref"];
14+
15+
type AsProp<C extends React.ElementType> = {
16+
as?: C;
17+
};
18+
19+
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
20+
21+
type PolymorphicComponentProp<
22+
C extends React.ElementType,
23+
Props = {}
24+
> = React.PropsWithChildren<Props & AsProp<C>> &
25+
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
26+
27+
type PolymorphicComponentPropWithRef<
28+
C extends React.ElementType,
29+
Props = {}
30+
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
31+
32+
type TextProps<C extends React.ElementType> = PolymorphicComponentPropWithRef<
33+
C,
34+
{ color?: Rainbow | "black" }
35+
>;
36+
37+
type TextComponent = <C extends React.ElementType = "span">(
38+
props: TextProps<C>
39+
) => React.ReactElement | null;
40+
41+
export const Text: TextComponent = React.forwardRef(
42+
<C extends React.ElementType = "span">(
43+
{ as, color, children }: TextProps<C>,
44+
ref?: PolymorphicRef<C>
45+
) => {
46+
const Component = as || "span";
47+
48+
const style = color ? { style: { color } } : {};
49+
50+
return (
51+
<Component {...style} ref={ref}>
52+
{children}
53+
</Component>
54+
);
55+
}
56+
);

polymorphic-react-component/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
![https://i.imgur.com/oNNDQDC.png](https://i.imgur.com/oNNDQDC.png)
2+
3+
<br />
4+
5+
> **Polymorphic**: occurring in several different forms, in particular with reference to species or genetic variation...
6+
7+
<br />
8+
<br />
9+
10+
# How to read the code
11+
12+
Read the implementation snippets from `01` to `06`
13+
14+
[01: A basic polymorphic component implementation](/01.tsx)
15+
16+
[02: Handling relevant component props](/02.tsx):
17+
18+
[03: Providing a default generic type](/03.tsx):
19+
20+
[04: Handling unique component props](/04.tsx)
21+
22+
[05: Building a reusable polymorphic type utility](/05.tsx)
23+
24+
[06: Handling polymorphic components with Ref](/06.tsx)
25+
26+
<br />
27+
<br />
28+
29+
# Relevant Links
30+
31+
- [Live workshop details](https://devcher.com/class/build-strongly-typed-polymorphic-components-with-react-and-typescript-UWwzxjSxrh)
32+
33+
- Watch me teach this [course on Udemy](https://www.udemy.com/course/build-polymorphic-components-with-react-and-typescript/?referralCode=DF6B523A0C852F2044DC)
34+
35+
- [Presentation](https://excalidraw.com/#json=3mAFa-9SfI53dtQ6q5ykA,frb3BKbw-Zivudv-kixuVQ) on Excalidraw (open in incognito to not override your current drawings )
36+
37+
- [use cases (PDF)](/use-cases.pdf)
38+
39+
<br />
40+
<br />
41+
42+
## Read more
43+
44+
- Polymorphic components in the wild: [Chakra UI](https://chakra-ui.com/docs/components/layout/box#as-prop) and [Material UI](https://mui.com/guides/composition/#component-prop)
45+
- JSX: [Choosing type at runtime](https://reactjs.org/docs/jsx-in-depth.html#choosing-the-type-at-runtime)
46+
- Typescript [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)
47+
- Typescript Generics and arrow functions:
48+
- [What's the syntax for generics in TS?](https://stackoverflow.com/questions/32308370/what-is-the-syntax-for-typescript-arrow-functions-with-generics?)
49+
- [TS Github issue](https://github.com/Microsoft/TypeScript/issues/4922)
50+
- [Why color appears as HTML attribute on a div?](https://stackoverflow.com/questions/67142430/why-color-appears-as-html-attribute-on-a-div)
51+
- React Ref: [Forwarding Refs](https://reactjs.org/docs/forwarding-refs.html)
52+
53+
<br />
54+
<br />
55+
56+
## Download the accompanying ebook and receive my 5-day typescript secrets newsletter
57+
58+
[Download ebook here](https://www.ohansemmanuel.com/books/how-to-build-strongly-typed-polymorphic-react-components).
59+
60+
<br />
61+
62+
![https://i.imgur.com/U62L81u.png](https://i.imgur.com/U62L81u.png)
63+
64+
You'll automatically receive my 5-day newsletter to get you thinking and writing Typescript like a pro 🕺
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# 1. Build your own Polymorphic component
2+
3+
This exercise will test your understanding of the core concepts in the class.
4+
5+
You will build a `Border` polymorphic React component.
6+
7+
Here is the basic usage of the component:
8+
9+
```jsx
10+
export const UserComponent = () => {
11+
return (
12+
<Border as="blockquote" color="blue" width={4} variant="solid">
13+
You only truly learn by practicing
14+
</Border>
15+
);
16+
};
17+
```
18+
19+
The Border component renders a styled border around whatever the children node is.
20+
21+
<br />
22+
23+
![https://i.imgur.com/qBqR3la.png](https://i.imgur.com/qBqR3la.png)
24+
25+
<br />
26+
27+
## Requirements:
28+
29+
1. The component must accept a polymorphic `as` prop
30+
2. The component must render the passed in `as` element type
31+
3. The component should support its own props e.g., `color`, `width` and `variant`. Where `color` is any valid `color` string, and `width` a number that defines the width of the border and `variant` the style of the border. See the valid CSS border styles on [W3schools](https://www.w3schools.com/css/css_border.asp)
32+
4. Using typescript, the component should display a type error during development if an invalid element type is passed
33+
5. Using typescript, the component should display a type error if wrong attributes are passed for valid element types
34+
6. Using typescript, the component should display a type error if wrong refs are passed to the component
35+
36+
# 2. Extend the polymorphic utility type to take in a user defined name e.g., 'as' or 'component' or 'what-a-user-wants'
37+
38+
The reusable utility built in the class assumes the user defined polymorphic prop is always `as`. Create your own version of a reusable utility that lets a user use any string they want as the polymorphic prop e.g., `as`, `component`, or `whatever`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "polymorphic",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"@testing-library/jest-dom": "^5.16.2",
7+
"@testing-library/react": "^12.1.3",
8+
"@testing-library/user-event": "^13.5.0",
9+
"@types/jest": "^27.4.1",
10+
"@types/node": "^16.11.26",
11+
"@types/react": "^17.0.39",
12+
"@types/react-dom": "^17.0.11",
13+
"react": "^18.1.0",
14+
"react-dom": "^18.1.0",
15+
"react-scripts": "5.0.0",
16+
"typescript": "^4.6.2",
17+
"web-vitals": "^2.1.4"
18+
},
19+
"scripts": {
20+
"start": "react-scripts start",
21+
"build": "react-scripts build",
22+
"test": "react-scripts test",
23+
"eject": "react-scripts eject"
24+
},
25+
"eslintConfig": {
26+
"extends": [
27+
"react-app",
28+
"react-app/jest"
29+
]
30+
},
31+
"browserslist": {
32+
"production": [
33+
">0.2%",
34+
"not dead",
35+
"not op_mini all"
36+
],
37+
"development": [
38+
"last 1 chrome version",
39+
"last 1 firefox version",
40+
"last 1 safari version"
41+
]
42+
}
43+
}

0 commit comments

Comments
 (0)