From 148347b97b99dfb9a2d62f0e91515d33e70a57a2 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Mon, 26 Feb 2024 11:06:06 -0500 Subject: [PATCH] write README.md for lesson 2, tweak solution code to align better with README.md --- src/react-render-perf/lesson-02/README.md | 141 +++++++++++++++++- .../solution/color-picker-context.tsx | 9 +- .../lesson-02/solution/index copy.tsx | 10 -- .../lesson-02/solution/index.tsx | 10 +- 4 files changed, 151 insertions(+), 19 deletions(-) delete mode 100644 src/react-render-perf/lesson-02/solution/index copy.tsx diff --git a/src/react-render-perf/lesson-02/README.md b/src/react-render-perf/lesson-02/README.md index 71d64f7..f4df55d 100644 --- a/src/react-render-perf/lesson-02/README.md +++ b/src/react-render-perf/lesson-02/README.md @@ -1,5 +1,142 @@ [Home](../README.md) -# 02 - Prevent Context From Rerendering +# 02 - Prevent Context From Re-rendering -TODO +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. + +In order to avoid re-rendering all of the descendents we'd like only those +components using the context to re-render. Ideally, the should only re-render +if the data that they're making use of changes. + +In the following example, everytime we update the `value` in the context, we +re-render both child components of the context even though only one of them +needs to be update. + +```tsx +import {createContext, useContext, useState} from "react"; + +type FooBar = { + foo: number; + bar: number; +}; + +const FooBarContext = createContext({foo: 0, bar: 0}); + +const Foo = () => { + const {foo} = useContext(FooBarContext); + return

foo = {foo}

; +}; + +const Bar = () => { + const {bar} = useContext(FooBarContext); + return

bar = {bar}

; +}; + +const Parent = () => { + const [value, setValue] = useState({foo: 0, bar: 0}); + const incrementFoo = () => setValue(fb => {...fb, foo: fb.foo + 1}); + const incrementBar = () => setValue(fb => {...fb, foo: fb.foo + 1}); + <> + + + + + + + +}; +``` + +To avoid this problem we can replace the plain old JavaScript object being +used for the context's `value` with a event emitter instance. The instance is +only created once and is never replaced so the `value` never changes which +means that the context will never trigger a re-render. + +The child components will register event listeners for changes to only the +data they care about. The parent component will emit the appropriate event when +increment the values for `foo` and `bar`. + +```tsx +import {createContext, useContext, useState, useMemo} from "react"; +import EventEmitter from "eventemitter3"; + +const FooBarContext = createContext(null); + +const Foo = () => { + const emitter = useContext(FooBarContext); + const [foo, setFoo] = useState(0); + emitter?.on("foo", setFoo); + + return

foo = {foo}

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

bar = {bar}

; +}; + +const Parent = () => { + const [foo, setFoo] = useState(0); + const [bar, setBar] = useState(0); + const emitter = useMemo(() => new EventEmitter(), []); + + const incrementFoo = () => { + emitter.emit("foo", foo + 1); + setFoo(foo + 1); + }; + const incrementBar = () => { + emitter.emit("bar", bar + 1); + setBar(bar + 1); + } + + <> + + + + + + + +}; +``` + +This is necessary but not sufficient to prevent both `Foo` and `Bar` from +re-rendering when either `foo` or `bar` is incremented. That's because +`Parent` is re-rendering when its state changes. To avoid this issue we also +need to memoize `Foo` and `Bar` themselves. Thankfully they don't take any +props so this is trivial to do. + +```tsx +import {createContext, useContext, useState, memo} from "react"; + +const Foo = memo(() => { + const emitter = useContext(FooBarContext); + const [foo, setFoo] = useState(0); + emitter?.on("foo", setFoo); + + return

foo = {foo}

; +}); + +const Bar = memo(() => { + const emitter = useContext(FooBarContext); + const [bar, setBar] = useState(0); + emitter?.on("bar", setBar); + + return

bar = {bar}

; +}); +``` + +## Exercise + +1. Use the profiler in React dev tools to measure the render performance of the +code in the "exercise" folder. +2. Update the code in the "exercise" folder to use an `EventEmitter` to prevent +the `value` in the context provider from changing. +3. Memoize only the necessary components to prevent unecessary re-renders. +4. Use the profiler in React dev tools to measure the render performance again. diff --git a/src/react-render-perf/lesson-02/solution/color-picker-context.tsx b/src/react-render-perf/lesson-02/solution/color-picker-context.tsx index 64f6290..e72e366 100644 --- a/src/react-render-perf/lesson-02/solution/color-picker-context.tsx +++ b/src/react-render-perf/lesson-02/solution/color-picker-context.tsx @@ -1,10 +1,7 @@ import {createContext} from "react"; -import { - colorPickerEventEmitter, - ColorPickerEventEmitter, -} from "./color-picker-event-emitter"; +import {ColorPickerEventEmitter} from "./color-picker-event-emitter"; -export const ColorPickerContext = createContext( - colorPickerEventEmitter, +export const ColorPickerContext = createContext( + null, ); diff --git a/src/react-render-perf/lesson-02/solution/index copy.tsx b/src/react-render-perf/lesson-02/solution/index copy.tsx deleted file mode 100644 index e96eaf8..0000000 --- a/src/react-render-perf/lesson-02/solution/index copy.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import {ColorPicker} from "./color-picker"; - -export default function Exercise2() { - return ( -
-

Solution 2: Prevent Context From Rendering

- -
- ); -} diff --git a/src/react-render-perf/lesson-02/solution/index.tsx b/src/react-render-perf/lesson-02/solution/index.tsx index 11835ed..62a39b7 100644 --- a/src/react-render-perf/lesson-02/solution/index.tsx +++ b/src/react-render-perf/lesson-02/solution/index.tsx @@ -1,10 +1,18 @@ +import {useMemo} from "react"; + import {ColorPicker} from "./color-picker"; +import {ColorPickerEventEmitter} from "./color-picker-event-emitter"; +import {ColorPickerContext} from "./color-picker-context"; export default function Solution2() { + const emitter = useMemo(() => new ColorPickerEventEmitter(), []); + return (

Solution 2: Prevent Context From Rendering

- + + +
); }