Skip to content

Commit

Permalink
address old issues in cheatsheet (#275)
Browse files Browse the repository at this point in the history
* batch1

* make age optional in defaultprops example

* talk about prop-types

* add useimperativehandle

Co-authored-by: swyx <[email protected]>
  • Loading branch information
swyxio and swyx authored Aug 21, 2020
1 parent 928de8d commit bb18d84
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 65 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2405,6 +2405,7 @@ It is worth mentioning some resources to help you get started:
- Marius Schultz: https://blog.mariusschulz.com/series/typescript-evolution with an [Egghead.io course](https://egghead.io/courses/advanced-static-types-in-typescript)
- Basarat's Deep Dive: https://basarat.gitbook.io/typescript/
- Rares Matei: [Egghead.io course](https://egghead.io/courses/practical-advanced-typescript)'s advanced TypeScript course on Egghead.io is great for newer typescript features and practical type logic applications (e.g. recursively making all properties of a type `readonly`)
- Go through [Remo Jansen's TypeScript ladder](http://www.techladder.io/?tech=typescript)
- Shu Uesugi: [TypeScript for Beginner Programmers](https://ts.chibicode.com/)
<!--END-SECTION:learn-ts-->
Expand Down
18 changes: 18 additions & 0 deletions docs/advanced/patterns_by_usecase.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,24 @@ type NumbersChildren = number[];
type TwoNumbersChildren = [number, number];
```

<details>
<summary>
Don't forget that you can also use `prop-types` if TS fails you.
</summary>

```ts
Parent.propTypes = {
children: PropTypes.shape({
props: PropTypes.shape({
// could share `propTypes` to the child
value: PropTypes.string.isRequired,
}),
}).isRequired,
};
```

</details>

### What You CANNOT Do

The thing you cannot do is **specify which components** the children are, e.g. If you want to express the fact that "React Router `<Routes>` can only have `<Route>` as children, nothing else is allowed" in TypeScript.
Expand Down
4 changes: 2 additions & 2 deletions docs/basic/getting-started/default-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const Greet = ({ age = 21 }: GreetProps) => {
// class components
// ////////////////
type GreetProps = {
age: number;
age?: number;
};

class Greet extends React.Component<GreetProps> {
Expand Down Expand Up @@ -125,7 +125,7 @@ export class MyComponent extends React.Component<IMyComponentProps> {

The problem with this approach is it causes complex issues with the type inference working with `JSX.LibraryManagedAttributes`. Basically it causes the compiler to think that when creating a JSX expression with that component, that all of its props are optional.

[See commentary by @ferdaber here](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/issues/57).
[See commentary by @ferdaber here](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/issues/57) and [here](https://github.com/typescript-cheatsheets/react/issues/61).

</details>

Expand Down
140 changes: 78 additions & 62 deletions docs/basic/getting-started/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Hooks

Hooks are [supported in `@types/react` from v16.8 up](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a05cc538a42243c632f054e42eab483ebf1560ab/types/react/index.d.ts#L800-L1031).

**useState**
## useState

Type inference works very well most of the time:

Expand All @@ -24,37 +24,53 @@ const [user, setUser] = React.useState<IUser | null>(null);
setUser(newUser);
```

**useRef**
## useReducer

When using `useRef`, you have two options when creating a ref container that does not have an initial value:
You can use [Discriminated Unions](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) for reducer actions. Don't forget to define the return type of reducer, otherwise TypeScript will infer it.

```ts
const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement | null>(null);
```tsx
type AppState = {};
type Action =
| { type: "SET_ONE"; payload: string } // typescript union types allow for leading |'s to have nicer layout
| { type: "SET_TWO"; payload: number };

export function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case "SET_ONE":
return {
...state,
one: action.payload, // `payload` is string
};
case "SET_TWO":
return {
...state,
two: action.payload, // `payload` is number
};
default:
return state;
}
}
```

The first option will make `ref1.current` read-only, and is intended to be passed in to built-in `ref` attributes that React will manage (because React handles setting the `current` value for you).
[View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/C4TwDgpgBAgmYGVgENjQLxQN4F8CwAUKJLAMbACWA9gHZTqFRQA+2UxEAXFAEQICiAFQD6AeQBy-HgG4oYZCAA2VZABNuAZ2AAnCjQDmUfASass7cF14CRggOqiZchcrXcaAVwC2AIwjajaUJCCAAPMCptYCgAMw8acmo6bQhVD1J-AAotVCs4RBQ0ABooZETabhhymgBKSvgkXOxGKA0AdwpgUgALKEyyyloAOg4a5pMmKFJkDWg+ITFJHk4WyagU4A9tOixVtaghw5zivbXaKwGkofklFVUoAHoHqAADG9dVF6gKDVadPX0p0Ce2ms2sC3sjhWEzWGy2OyBTEOQ2OECKiPYbSo3Euw3ed0ezzeLjuXx+UE8vn8QJwQRhUFUEBiyA8imA0P26wgm22f1ydKYxhwQA)

<details>
<summary>What is the <code>!</code> at the end of <code>null!</code>?</summary>

`null!` is a non-null assertion operator (the `!`). It asserts that any expression before it is not `null` or `undefined`, so if you have `useRef<HTMLElement>(null!)` it means that you're instantiating the ref with a current value of `null` but lying to TypeScript that it's not `null`.
<summary><b>Usage with `Reducer` from `redux`</b></summary>

```ts
function MyComponent() {
const ref1 = useRef<HTMLElement>(null!);
useEffect(() => {
doSomethingWith(ref1.current); // TypeScript won't require null-check e.g. ref1 && ref1.current
});
return <div ref={ref1}> etc </div>;
}
In case you use the [redux](https://github.com/reduxjs/redux) library to write reducer function, It provides a convenient helper of the format `Reducer<State, Action>` which takes care of the return type for you.

So the above reducer example becomes:

```tsx
import { Reducer } from 'redux';

export function reducer: Reducer<AppState, Action>() {}
```

</details>

The second option will make `ref2.current` mutable, and is intended for "instance variables" that you manage yourself.

**useEffect**
## useEffect

When using `useEffect`, take care not to return anything other than a function or `undefined`, otherwise both TypeScript and React will yell at you. This can be subtle when using arrow functions:

Expand All @@ -73,7 +89,35 @@ function DelayedEffect(props: { timerMs: number }) {
}
```

**useRef**
## useRef

When using `useRef`, you have two options when creating a ref container that does not have an initial value:

```ts
const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement | null>(null);
```

The first option will make `ref1.current` read-only, and is intended to be passed in to built-in `ref` attributes that React will manage (because React handles setting the `current` value for you).

<details>
<summary>What is the <code>!</code> at the end of <code>null!</code>?</summary>

`null!` is a non-null assertion operator (the `!`). It asserts that any expression before it is not `null` or `undefined`, so if you have `useRef<HTMLElement>(null!)` it means that you're instantiating the ref with a current value of `null` but lying to TypeScript that it's not `null`.

```ts
function MyComponent() {
const ref1 = useRef<HTMLElement>(null!);
useEffect(() => {
doSomethingWith(ref1.current); // TypeScript won't require null-check e.g. ref1 && ref1.current
});
return <div ref={ref1}> etc </div>;
}
```

</details>

The second option will make `ref2.current` mutable, and is intended for "instance variables" that you manage yourself.

```tsx
function TextInputWithFocusButton() {
Expand Down Expand Up @@ -101,53 +145,25 @@ function TextInputWithFocusButton() {

example from [Stefan Baumgartner](https://fettblog.eu/typescript-react/hooks/#useref)

**useReducer**
## useImperativeHandle

You can use [Discriminated Unions](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) for reducer actions. Don't forget to define the return type of reducer, otherwise TypeScript will infer it.
_we dont have much here, but this is from [a discussion in our issues](https://github.com/typescript-cheatsheets/react/issues/106)_

```tsx
type AppState = {};
type Action =
| { type: "SET_ONE"; payload: string } // typescript union types allow for leading |'s to have nicer layout
| { type: "SET_TWO"; payload: number };

export function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case "SET_ONE":
return {
...state,
one: action.payload, // `payload` is string
};
case "SET_TWO":
return {
...state,
two: action.payload, // `payload` is number
};
default:
return state;
}
type ListProps<ItemType> = {
items: ItemType[];
innerRef?: React.Ref<{ scrollToItem(item: ItemType): void }>;
};

function List<ItemType>(props: ListProps<ItemType>) {
useImperativeHandle(props.innerRef, () => ({
scrollToItem() {},
}));
return null;
}
```

[View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/C4TwDgpgBAgmYGVgENjQLxQN4F8CwAUKJLAMbACWA9gHZTqFRQA+2UxEAXFAEQICiAFQD6AeQBy-HgG4oYZCAA2VZABNuAZ2AAnCjQDmUfASass7cF14CRggOqiZchcrXcaAVwC2AIwjajaUJCCAAPMCptYCgAMw8acmo6bQhVD1J-AAotVCs4RBQ0ABooZETabhhymgBKSvgkXOxGKA0AdwpgUgALKEyyyloAOg4a5pMmKFJkDWg+ITFJHk4WyagU4A9tOixVtaghw5zivbXaKwGkofklFVUoAHoHqAADG9dVF6gKDVadPX0p0Ce2ms2sC3sjhWEzWGy2OyBTEOQ2OECKiPYbSo3Euw3ed0ezzeLjuXx+UE8vn8QJwQRhUFUEBiyA8imA0P26wgm22f1ydKYxhwQA)

<details>

<summary><b>Usage with `Reducer` from `redux`</b></summary>

In case you use the [redux](https://github.com/reduxjs/redux) library to write reducer function, It provides a convenient helper of the format `Reducer<State, Action>` which takes care of the return type for you.

So the above reducer example becomes:

```tsx
import { Reducer } from 'redux';

export function reducer: Reducer<AppState, Action>() {}
```

</details>

**Custom Hooks**
## Custom Hooks

If you are returning an array in your Custom Hook, you will want to avoid type inference as TypeScript will infer a union type (when you actually want different types in each position of the array). Instead, use [TS 3.4 const assertions](https://devblogs.microsoft.com/typescript/announcing-typescript-3-4/#const-assertions):

Expand Down
2 changes: 1 addition & 1 deletion docs/hoc/full-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ export function inject<TProps, TInjectedKeys extends keyof TProps>(

### Using `forwardRef`

For "true" reusability you should also consider exposing a ref for your HOC. You can use `React.forwardRef<Ref, Props>` as documented in [the basic cheatsheet](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/README.md#forwardrefcreateref), but we are interested in more real world examples. [Here is a nice example in practice](https://gist.github.com/OliverJAsh/d2f462b03b3e6c24f5588ca7915d010e) from @OliverJAsh.
For "true" reusability you should also consider exposing a ref for your HOC. You can use `React.forwardRef<Ref, Props>` as documented in [the basic cheatsheet](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/README.md#forwardrefcreateref), but we are interested in more real world examples. [Here is a nice example in practice](https://gist.github.com/OliverJAsh/d2f462b03b3e6c24f5588ca7915d010e) from @OliverJAsh (note - it still has some rough edges, we need help to test this out/document this).

0 comments on commit bb18d84

Please sign in to comment.