Skip to content

Commit 1bd64f9

Browse files
authored
Merge pull request #89 from sw-yx/addHOC
add HOC cheatsheet
2 parents 89f1506 + 3114f3a commit 1bd64f9

File tree

1 file changed

+310
-0
lines changed

1 file changed

+310
-0
lines changed

HOC.md

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# HOC Cheatsheet
2+
3+
**This HOC Cheatsheet** compiles all available knowledge for writing Higher Order Components with React and TypeScript.
4+
5+
- We will map closely to [the official docs on HOCs](https://reactjs.org/docs/higher-order-components.html) initially
6+
- While hooks exist, many libraries and codebases still have a need to type HOCs.
7+
- Render props may be considered in future
8+
- The goal is to write HOCs that offer type safety while not getting in the way.
9+
10+
---
11+
12+
### HOC Cheatsheet Table of Contents
13+
14+
<details>
15+
16+
<summary><b>Expand Table of Contents</b></summary>
17+
18+
- [Section 0: Prerequisites](#section-0-prerequisites)
19+
</details>
20+
21+
# Section 1: React HOC docs in TypeScript
22+
23+
In this first section we refer closely to [the React docs on HOCs](https://reactjs.org/docs/higher-order-components.html) and offer direct TypeScript parallels.
24+
25+
## Docs Example: [Use HOCs For Cross-Cutting Concerns](https://reactjs.org/docs/higher-order-components.html#use-hocs-for-cross-cutting-concerns)
26+
27+
<details>
28+
29+
<summary>
30+
<b>Misc variables referenced in the example below</b>
31+
</summary>
32+
33+
```tsx
34+
/** dummy child components that take anything */
35+
const Comment = (_: any) => null;
36+
const TextBlock = Comment;
37+
38+
/** dummy Data */
39+
type CommentType = { text: string; id: number };
40+
const comments: CommentType[] = [
41+
{
42+
text: 'comment1',
43+
id: 1
44+
},
45+
{
46+
text: 'comment2',
47+
id: 2
48+
}
49+
];
50+
const blog = 'blogpost';
51+
52+
/** mock data source */
53+
const DataSource = {
54+
addChangeListener(e: Function) {
55+
// do something
56+
},
57+
removeChangeListener(e: Function) {
58+
// do something
59+
},
60+
getComments() {
61+
return comments;
62+
},
63+
getBlogPost(id: number) {
64+
return blog;
65+
}
66+
};
67+
/** type aliases just to deduplicate */
68+
type DataType = typeof DataSource;
69+
// type TODO_ANY = any;
70+
71+
/** utility types we use */
72+
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
73+
// type Optionalize<T extends K, K> = Omit<T, keyof K>;
74+
75+
/** Rewritten Components from the React docs that just uses injected data prop */
76+
function CommentList({ data }: WithDataProps<typeof comments>) {
77+
return (
78+
<div>
79+
{data.map((comment: CommentType) => (
80+
<Comment comment={comment} key={comment.id} />
81+
))}
82+
</div>
83+
);
84+
}
85+
interface BlogPostProps extends WithDataProps<string> {
86+
id: number;
87+
// children: ReactNode;
88+
}
89+
function BlogPost({ data, id }: BlogPostProps) {
90+
return (
91+
<div key={id}>
92+
<TextBlock text={data} />;
93+
</div>
94+
);
95+
}
96+
```
97+
98+
</details>
99+
100+
Example HOC from React Docs translated to TypeScript
101+
102+
```tsx
103+
// these are the props to be injected by the HOC
104+
interface WithDataProps<T> {
105+
data: T; // data is generic
106+
}
107+
// T is the type of data
108+
// P is the props of the wrapped component that is inferred
109+
// C is the actual interface of the wrapped component (used to grab defaultProps from it)
110+
export function withSubscription<T, P extends WithDataProps<T>, C>(
111+
// this type allows us to infer P, but grab the type of WrappedComponent separately without it interfering with the inference of P
112+
WrappedComponent: JSXElementConstructor<P> & C,
113+
// selectData is a functor for T
114+
// props is Readonly because it's readonly inside of the class
115+
selectData: (
116+
dataSource: typeof DataSource,
117+
props: Readonly<JSX.LibraryManagedAttributes<C, Omit<P, 'data'>>>
118+
) => T
119+
) {
120+
// the magic is here: JSX.LibraryManagedAttributes will take the type of WrapedComponent and resolve its default props
121+
// against the props of WithData, which is just the original P type with 'data' removed from its requirements
122+
type Props = JSX.LibraryManagedAttributes<C, Omit<P, 'data'>>;
123+
type State = {
124+
data: T;
125+
};
126+
return class WithData extends Component<Props, State> {
127+
constructor(props: Props) {
128+
super(props);
129+
this.handleChange = this.handleChange.bind(this);
130+
this.state = {
131+
data: selectData(DataSource, props)
132+
};
133+
}
134+
135+
componentDidMount = () => DataSource.addChangeListener(this.handleChange);
136+
137+
componentWillUnmount = () =>
138+
DataSource.removeChangeListener(this.handleChange);
139+
140+
handleChange = () =>
141+
this.setState({
142+
data: selectData(DataSource, this.props)
143+
});
144+
145+
render() {
146+
// the typing for spreading this.props is... very complex. best way right now is to just type it as any
147+
// data will still be typechecked
148+
return <WrappedComponent data={this.state.data} {...this.props as any} />;
149+
}
150+
};
151+
// return WithData;
152+
}
153+
154+
/** HOC usage with Components */
155+
export const CommentListWithSubscription = withSubscription(
156+
CommentList,
157+
(DataSource: DataType) => DataSource.getComments()
158+
);
159+
160+
export const BlogPostWithSubscription = withSubscription(
161+
BlogPost,
162+
(DataSource: DataType, props: Omit<BlogPostProps, 'data'>) =>
163+
DataSource.getBlogPost(props.id)
164+
);
165+
```
166+
167+
## Docs Example: [Don’t Mutate the Original Component. Use Composition.](https://reactjs.org/docs/higher-order-components.html#dont-mutate-the-original-component-use-composition)
168+
169+
This is pretty straightforward - make sure to assert the passed props as `T` [due to the TS 3.2 bug](https://github.com/Microsoft/TypeScript/issues/28938#issuecomment-450636046).
170+
171+
```tsx
172+
function logProps<T>(WrappedComponent: React.ComponentType<T>) {
173+
return class extends React.Component {
174+
componentWillReceiveProps(
175+
nextProps: React.ComponentProps<typeof WrappedComponent>
176+
) {
177+
console.log('Current props: ', this.props);
178+
console.log('Next props: ', nextProps);
179+
}
180+
render() {
181+
// Wraps the input component in a container, without mutating it. Good!
182+
return <WrappedComponent {...this.props as T} />;
183+
}
184+
};
185+
}
186+
```
187+
188+
## Docs Example: [Pass Unrelated Props Through to the Wrapped Component](https://reactjs.org/docs/higher-order-components.html#convention-pass-unrelated-props-through-to-the-wrapped-component)
189+
190+
No TypeScript specific advice needed here.
191+
192+
## Docs Example: [Maximizing Composability](https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability)
193+
194+
HOCs can take the form of Functions that return Higher Order Components that return Components.
195+
196+
`connect` from `react-redux` has a number of overloads you can take inspiration [from in the source](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/v5/index.d.ts#L77).
197+
198+
Here we build our own mini `connect` to understand HOCs:
199+
200+
<details>
201+
202+
<summary>
203+
<b>Misc variables referenced in the example below</b>
204+
</summary>
205+
206+
```tsx
207+
/** utility types we use */
208+
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
209+
210+
/** dummy Data */
211+
type CommentType = { text: string; id: number };
212+
const comments: CommentType[] = [
213+
{
214+
text: 'comment1',
215+
id: 1
216+
},
217+
{
218+
text: 'comment2',
219+
id: 2
220+
}
221+
];
222+
/** dummy child components that take anything */
223+
const Comment = (_: any) => null;
224+
/** Rewritten Components from the React docs that just uses injected data prop */
225+
function CommentList({ data }: WithSubscriptionProps<typeof comments>) {
226+
return (
227+
<div>
228+
{data.map((comment: CommentType) => (
229+
<Comment comment={comment} key={comment.id} />
230+
))}
231+
</div>
232+
);
233+
}
234+
```
235+
236+
</details>
237+
238+
```tsx
239+
const commentSelector = (_: any, ownProps: any) => ({
240+
id: ownProps.id
241+
});
242+
const commentActions = () => ({
243+
addComment: (str: string) => comments.push({ text: str, id: comments.length })
244+
});
245+
246+
const ConnectedComment = connect(
247+
commentSelector,
248+
commentActions
249+
)(CommentList);
250+
251+
// these are the props to be injected by the HOC
252+
interface WithSubscriptionProps<T> {
253+
data: T;
254+
}
255+
function connect(mapStateToProps: Function, mapDispatchToProps: Function) {
256+
return function<T, P extends WithSubscriptionProps<T>, C>(
257+
WrappedComponent: React.ComponentType<T>
258+
) {
259+
type Props = JSX.LibraryManagedAttributes<C, Omit<P, 'data'>>;
260+
// Creating the inner component. The calculated Props type here is the where the magic happens.
261+
return class ComponentWithTheme extends React.Component<Props> {
262+
public render() {
263+
// Fetch the props you want inject. This could be done with context instead.
264+
const mappedStateProps = mapStateToProps(this.state, this.props);
265+
const mappedDispatchProps = mapDispatchToProps(this.state, this.props);
266+
// this.props comes afterwards so the can override the default ones.
267+
return (
268+
<WrappedComponent
269+
{...this.props}
270+
{...mappedStateProps}
271+
{...mappedDispatchProps}
272+
/>
273+
);
274+
}
275+
};
276+
};
277+
}
278+
```
279+
280+
## Docs Example: [Wrap the Display Name for Easy Debugging](https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging)
281+
282+
This is pretty straightforward as well.
283+
284+
```tsx
285+
interface WithSubscriptionProps {
286+
data: any;
287+
}
288+
289+
function withSubscription<
290+
T extends WithSubscriptionProps = WithSubscriptionProps
291+
>(WrappedComponent: React.ComponentType<T>) {
292+
class WithSubscription extends React.Component {
293+
/* ... */
294+
public static displayName = `WithSubscription(${getDisplayName(
295+
WrappedComponent
296+
)})`;
297+
}
298+
return WithSubscription;
299+
}
300+
301+
function getDisplayName<T>(WrappedComponent: React.ComponentType<T>) {
302+
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
303+
}
304+
```
305+
306+
## Unwritten: [Caveats section](https://reactjs.org/docs/higher-order-components.html#caveats)
307+
308+
- Don’t Use HOCs Inside the render Method
309+
- Static Methods Must Be Copied Over
310+
- Refs Aren’t Passed Through

0 commit comments

Comments
 (0)