Skip to content

Commit

Permalink
Revise Lesson 2
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinb-khan committed Jul 3, 2024
1 parent be104ed commit 4577431
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 29 deletions.
4 changes: 2 additions & 2 deletions src/react-render-perf/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import highlightRerenders from "./images/highlight-rerenders.png"

# React Render Perf

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

## Preliminaries

Expand Down
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-01/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default function Page() {
<Content components={{code}} />
</div>
<div className={styles.column}>
<h3>
⚠️ HMR may not work, please reload after making changes ⚠️
</h3>
<Tabs tabs={{exercise: Exercise, solution: Solution}} />
</div>
</div>
Expand Down
79 changes: 52 additions & 27 deletions src/react-render-perf/lesson-02/content.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
React Context is useful for sharing data with its descendent components. When
changes are made to the context, this causes the context and all of its
descendents to be re-rendered. If the number of descendents is large, this can
result in poor rendering performance.
React Context is useful for sharing data with its descendent components without
having to perform prop-drilling (passing props down through several layers of
components). When changes are made to the context, this causes the context and all
of its descendents to be re-rendered. If the number of descendents is large, this
can result in poor rendering performance.

In order to avoid re-rendering all of the descendents we'd like only those
components using the context to re-render. Ideally, they should only re-render
Expand All @@ -12,27 +13,27 @@ re-render both child components of the context even though only one of them
needs to be updated.

```tsx filename="index.tsx"
import {createContext, useContext, useState} from "react";
import * as React from "react";

type FooBar = {
foo: number;
bar: number;
};

const FooBarContext = createContext<FooBar>({foo: 0, bar: 0});
const FooBarContext = React.createContext<FooBar>({foo: 0, bar: 0});

const Foo = () => {
const {foo} = useContext(FooBarContext);
const {foo} = React.useContext(FooBarContext);
return <h1>foo = {foo}</h1>;
};

const Bar = () => {
const {bar} = useContext(FooBarContext);
const {bar} = React.useContext(FooBarContext);
return <h1>bar = {bar}</h1>;
};

const Parent = () => {
const [value, setValue] = useState<FooBar>({foo: 0, bar: 0});
const [value, setValue] = React.useState<FooBar>({foo: 0, bar: 0});
const incrementFoo = () => setValue(fb => {...fb, foo: fb.foo + 1});
const incrementBar = () => setValue(fb => {...fb, bar: fb.bar + 1});
<>
Expand All @@ -56,31 +57,37 @@ data they care about. The parent component will emit the appropriate event when
increment the values for <code>foo</code> and <code>bar</code>.

```tsx
import {createContext, useContext, useState, useMemo} from "react";
import * as React from "react";
import EventEmitter from "eventemitter3";

const FooBarContext = createContext<EventEmitter | null>(null);
const FooBarContext = React.createContext<EventEmitter | null>(null);

const Foo = () => {
const emitter = useContext(FooBarContext);
const [foo, setFoo] = useState<number>(0);
emitter?.on("foo", setFoo);
const emitter = React.useContext(FooBarContext);
const [foo, setFoo] = React.useState<number>(0);
React.useEffect(() => {
emitter?.on("foo", setFoo);
return () => emitter?.off("foo", setFoo);
}, []);

return <h1>foo = {foo}</h1>;
};

const Bar = () => {
const emitter = useContext(FooBarContext);
const [bar, setBar] = useState<number>(0);
emitter?.on("bar", setBar);
const emitter = React.useContext(FooBarContext);
const [bar, setBar] = React.useState<number>(0);
React.useEffect(() => {
emitter?.on("bar", setBar);
return () => emitter?.off("bar", setBar);
});

return <h1>bar = {bar}</h1>;
};

const Parent = () => {
const [foo, setFoo] = useState<number>(0);
const [bar, setBar] = useState<number>(0);
const emitter = useMemo(() => new EventEmitter(), []);
const [foo, setFoo] = React.useState<number>(0);
const [bar, setBar] = React.useState<number>(0);
const emitter = React.useMemo(() => new EventEmitter(), []);

const incrementFoo = () => {
emitter.emit("foo", foo + 1);
Expand Down Expand Up @@ -109,26 +116,44 @@ need to memoize <code>Foo</code> and <code>Bar</code> themselves. Thankfully the
props so this is trivial to do.

```tsx
import {createContext, useContext, useState, memo} from "react";
import * as React from "react";
import arePropsEqual from "react-fast-compare";

const Foo = memo(() => {
const emitter = useContext(FooBarContext);
const [foo, setFoo] = useState<number>(0);
const Foo = React.memo(() => {
const emitter = React.useContext(FooBarContext);
const [foo, setFoo] = React.useState<number>(0);
emitter?.on("foo", setFoo);

return <h1>foo = {foo}</h1>;
}, arePropsEqual);

const Bar = memo(() => {
const emitter = useContext(FooBarContext);
const [bar, setBar] = useState<number>(0);
const Bar = React.memo(() => {
const emitter = React.useContext(FooBarContext);
const [bar, setBar] = React.useState<number>(0);
emitter?.on("bar", setBar);

return <h1>bar = {bar}</h1>;
}, arePropsEqual);
```

## Notes

- The example above shows the <code>Parent</code> component creating the event emitter. This is
only necessary if <code>Parent</code> was being used in multiple places and we didn't want its
descendents sharing the same event emitter. If there's only a single instance of
<code>Parent</code> the event emitter could be created outside of the component and could be
used to initialize the context. This would result in the context value always being
defined and we wouldn't need to use optional chaining when accessing properties on
the <code>emitter</code> we get from the context.
- Why not just use <code>redux</code> for prop-drilling? Unfortunately, it has the same issues
as the naive use of context shown in the original code above. That being said, the context +
event emitter pattern isn't great either because it's a pattern as opposed to a library. In
the long term I think we should evaluate possible replaces for <code>redux</code> that don't
have this preformance issue like <code>jotai</code>, <code>zustand</code>, or <code>recoil.js</code>.
- <code>React.useMemo(callback)</code> and <code>React.memo(Component, arePropsEqual?)</code> are
both use to memoize things. The former is used for memoizing computations _within_ a functional
component while the latter is used for memoizing functional components themselves.

## Exercise

1. Use the profiler in React dev tools to measure the render performance of the
Expand Down
11 changes: 11 additions & 0 deletions src/react-render-perf/lesson-02/exercise/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ export default function Exercise2() {
return (
<div>
<h1>Exercise 2: Prevent Context From Rendering</h1>
<p>
Below is a colour picker. Clicking on it will select the colour
under the mouse cursor. Profile to see where the performance
issues are and then resolve them using the technique described
in this lesson.
</p>
<p>
NOTE: There are multiple components in this example that could
be memoized. Part of this exercise is figuring out which ones
make sense to memoize.
</p>
<ColorPicker />
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-02/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default function Page() {
<Content components={{code}} />
</div>
<div className={styles.column}>
<h3>
⚠️ HMR may not work, please reload after making changes ⚠️
</h3>
<Tabs tabs={{exercise: Exercise, solution: Solution}} />
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-03/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default function Page() {
<Content components={{code}} />
</div>
<div className={styles.column}>
<h3>
⚠️ HMR may not work, please reload after making changes ⚠️
</h3>
<Tabs tabs={{exercise: Exercise, solution: Solution}} />
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/react-render-perf/lesson-04/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default function Page() {
<Content components={{code}} />
</div>
<div className={styles.column}>
<h3>
⚠️ HMR may not work, please reload after making changes ⚠️
</h3>
<Tabs tabs={{exercise: Exercise, solution: Solution}} />
</div>
</div>
Expand Down

0 comments on commit 4577431

Please sign in to comment.