Skip to content

Commit

Permalink
FEI-5465.3: Create README.md for lesson 01
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinbarabash committed Feb 22, 2024
1 parent 6bada65 commit cecc902
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 24 deletions.
11 changes: 11 additions & 0 deletions src/react-render-perf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# React Render Perf

The purpose of this workshop is to learn about common issues that can result
react renders taking longer than expected.

## Lessons

1. Memoizing expensive to render components
2. Prevent React.Context from re-render the whole tree
3. Avoid using React.Context at all
4. Minimizing re-renders by splitting up large components
21 changes: 0 additions & 21 deletions src/react-render-perf/index.tsx

This file was deleted.

129 changes: 129 additions & 0 deletions src/react-render-perf/lesson-01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 01 - Memoizing Expensive Components

Memoization can be used to avoid unnecessary renders. This is most useful when
the component itself is expensive to render (e.g. the `MathJax` component in
webapp) or it renders a lot of descedent components.

Memoization works by saving the rendered output of the component based on the
props that are being passed in. Often times props will appear to have changed
when their actual values haven't. In JavaScript, two objects with the same
properties are considered different objects. Similarly, two functions with the
same implementation are considered differen objects.

In order for memoization to have the desired benefit, we want don't want the
component to rerender if there are only superficial changes to props.

`React.memo(Component, arePropsEqual?)` by default does a shallow comparison of
props and as such isn't able to determine when a prop that's on object or function
is the same or not. We can pass a custom `arePropsEqual` function to override
that behavior. To keep things simple we use a third-party library called
`react-fast-compare` which provides a function that does a deep comparison of
objects.

```ts
import arePropsEqual from "react-fast-compare";

type Props = {
user: {name: string, id: string},
onClick: () => void,
}

const ChildComponent = (props: Props) => {
// ...
}

export default React.memo(ChildComponent, arePropsEqual);
```

There is a bit of a gotcha here when it comes to props that are functions.
`react-fast-compare` cannot check if two functions are the same. Imagine the
following scenario:

```ts
import ChildComponent from "./child-component";

type Props = {
user: {name: string, id: string},
};

const ParentComponent = (props: Props) => {
const result = useQuery(QUERY);

const handleClick = () => {
if (result.data) {
// do something with the data
}
};

return <ChildComponent user={user} onClick={handleClick}>
}
```

Each time `ParentComponent` renders, a new copy of `handleClick` will be created
even if the `result` from `useQuery` isn't ready yet. We only want this function
to treated as a new function when `result` changes. React provides a hook called
`useCallback` which does exactly that by memoizing the function.

```ts
import ChildComponent from "./my-component";

type Props = {
user: {name: string, id: string},
};

const ParentComponent = (props: Props) => {
const result = useQuery(QUERY);

const handleClick = React.useCallback(() => {
if (result.data) {
// do something with the data
}
}, [result]);

return <ChildComponent user={user} onClick={handleClick}>
}
```

If the `ParentComponent` is a class-based component, there is no need to memoize
function props that are pre-bound methods. This is because the method never changes
for the component instance. If the prop is an inline function though, it should be
convered to a pre-bound method.

```ts
import ChildComponent from "./my-component";

type Props = {
user: {name: string, id: string},
};
type State = {
result?: Result<typeof QUERY>,
}

class ParentComponent extends React.Component<Props, State> {
componentDidMount() {
fetch(QUERY).then((result) => {
this.setState({result});
});
}

handleClick = () => {
if (this.result?.data) {
// do something with the data
}
}

render() {
return <ChildComponent user={user} onClick={handleClick}>
}
}
```

**WARNING:**
Memoization is not free. It requires memory so you should be picky when deciding
what to memoize.

| Good Candidates | Bad Candidates |
| ------------------------------------------ | ---------------------------------------- |
| lots of descendants | few descendants |
| expensive to render | inexpensive to render |
| actual values of props change infrequently | actual values of props change frequently |
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-01/exercise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Exercise1() {
return <h1>Exercise 1: Memoizing Expensive Components</h1>;
}
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-02/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 02 - Prevent Context From Rerendering

TODO
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-02/exercise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Exercise2() {
return <h1>Exercise 2: Prevent Context From Rendering</h1>;
}
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-03/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 03 - Avoid Using Context

TODO
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-03/exercise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Exercise3() {
return <h1>Exercise 3: Avoid Using Context</h1>;
}
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-04/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 04 - Splitting Large Components

TODO
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-04/exercise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Exercise4() {
return <h1>Exercise 4: Splitting up large components</h1>;
}
29 changes: 29 additions & 0 deletions src/react-render-perf/routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {BrowserRouter, Route} from "react-router-dom";

import TableOfContents from "./table-of-contents";
import Lesson1Exercise from "./lesson-01/exercise";
import Lesson2Exercise from "./lesson-02/exercise";
import Lesson3Exercise from "./lesson-03/exercise";
import Lesson4Exercise from "./lesson-04/exercise";

export default function Routes() {
return (
<BrowserRouter>
<Route path="/react-render-perf" exact={true}>
<TableOfContents />
</Route>
<Route path="/react-render-perf/01" exact={true}>
<Lesson1Exercise />
</Route>
<Route path="/react-render-perf/02" exact={true}>
<Lesson2Exercise />
</Route>
<Route path="/react-render-perf/03" exact={true}>
<Lesson3Exercise />
</Route>
<Route path="/react-render-perf/04" exact={true}>
<Lesson4Exercise />
</Route>
</BrowserRouter>
);
}
32 changes: 32 additions & 0 deletions src/react-render-perf/table-of-contents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Link} from "react-router-dom";

export default function ReactRenderPerf() {
return (
<>
<Link to="/">Home</Link>
<h1>React Render Perf</h1>
<ul>
<li>
<Link to="/react-render-perf/01">
01 - Memoizing Expensive Components
</Link>
</li>
<li>
<Link to="/react-render-perf/02">
02 - Prevent Context From Rendering
</Link>
</li>
<li>
<Link to="/react-render-perf/03">
03 - Avoid Using Context
</Link>
</li>
<li>
<Link to="/react-render-perf/04">
04 - Splitting Up Large Components
</Link>
</li>
</ul>
</>
);
}
6 changes: 3 additions & 3 deletions src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {BrowserRouter, Route} from "react-router-dom";

import ReactRenderPerf from "./react-render-perf";
import ReactRenderPerfRoutes from "./react-render-perf/routes";
import Homepage from "./homepage";

export default function Routes() {
Expand All @@ -9,8 +9,8 @@ export default function Routes() {
<Route path="/" exact={true}>
<Homepage />
</Route>
<Route path="/react-render-perf" exact={true}>
<ReactRenderPerf />
<Route path="/react-render-perf" exact={false}>
<ReactRenderPerfRoutes />
</Route>
</BrowserRouter>
);
Expand Down

0 comments on commit cecc902

Please sign in to comment.