Skip to content

Commit

Permalink
FEI-5465.4: Lesson 2 - Prevent Context From Rerendering
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinbarabash committed Feb 23, 2024
1 parent a7143c3 commit 853e0a3
Show file tree
Hide file tree
Showing 19 changed files with 371 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"eventemitter3": "^5.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.2",
Expand Down
24 changes: 24 additions & 0 deletions src/react-render-perf/lesson-02/exercise/child.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {fib} from "../../shared/fib";
import {Circle} from "../../shared/shapes";

type Props = {
circle: Circle;
onClick: () => void;
};

export function ChildComponent({circle, onClick}: Props) {
return (
<div>
<h1>Child Component</h1>
<p>fib(36) = {fib(36)}</p>
<button onClick={onClick}>Click me!</button>
<svg>
<circle
r={circle.radius}
cx={circle.center.x}
cy={circle.center.y}
/>
</svg>
</div>
);
}
10 changes: 10 additions & 0 deletions src/react-render-perf/lesson-02/exercise/color-picker-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {createContext} from "react";

import {Color} from "../../shared/color";

type Context = {
color?: Color;
setColor: (color?: Color) => void;
};

export const ColorPickerContext = createContext<Context | null>(null);
24 changes: 24 additions & 0 deletions src/react-render-perf/lesson-02/exercise/color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {useState} from "react";

import {Grid} from "./grid";
import {ColorPickerContext} from "./color-picker-context";
import {Color, toCssColor} from "../../shared/color";

export function ColorPicker() {
const [color, setColor] = useState<Color | undefined>(undefined);

return (
<div>
<ColorPickerContext.Provider value={{color, setColor}}>
<Grid
topLeft={{red: 0, green: 0, blue: 256}}
topRight={{red: 256, green: 0, blue: 256}}
bottomLeft={{red: 0, green: 256, blue: 256}}
bottomRight={{red: 256, green: 256, blue: 256}}
steps={64} /* the larger the number the worse performance */
/>
</ColorPickerContext.Provider>
current color = {color ? toCssColor(color) : "none"}
</div>
);
}
22 changes: 22 additions & 0 deletions src/react-render-perf/lesson-02/exercise/gradient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {ReactElement} from "react";

import {Swatch} from "./swatch";
import {Color, lerpColor} from "../../shared/color";

type Props = {
start: Color;
stop: Color;
steps: number;
};

export function Gradient({start, stop, steps}: Props) {
const swatches: Array<ReactElement<typeof Swatch>> = [];
for (let i = 0; i < steps; i++) {
const color = lerpColor(start, stop, i / steps);
swatches.push(<Swatch color={color} size={512 / steps} />);
}

return (
<div style={{display: "flex", flexDirection: "row"}}>{swatches}</div>
);
}
33 changes: 33 additions & 0 deletions src/react-render-perf/lesson-02/exercise/grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {ReactElement} from "react";

import {Gradient} from "./gradient";
import {Color, lerpColor} from "../../shared/color";

type Props = {
topLeft: Color;
topRight: Color;
bottomLeft: Color;
bottomRight: Color;
steps: number;
};

export function Grid({
topLeft,
topRight,
bottomLeft,
bottomRight,
steps,
}: Props) {
const gradients: Array<ReactElement<typeof Gradient>> = [];
for (let i = 0; i < steps; i++) {
const start = lerpColor(topLeft, bottomLeft, i / steps);
const stop = lerpColor(topRight, bottomRight, i / steps);
gradients.push(<Gradient start={start} stop={stop} steps={steps} />);
}

return (
<div style={{display: "flex", flexDirection: "column"}}>
{gradients}
</div>
);
}
9 changes: 8 additions & 1 deletion src/react-render-perf/lesson-02/exercise/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {ColorPicker} from "./color-picker";

export default function Exercise2() {
return <h1>Exercise 2: Prevent Context From Rendering</h1>;
return (
<div>
<h1>Solution 2: Prevent Context From Rendering</h1>
<ColorPicker />
</div>
);
}
32 changes: 32 additions & 0 deletions src/react-render-perf/lesson-02/exercise/swatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {useContext} from "react";

import {Color, toCssColor} from "../../shared/color";
import {ColorPickerContext} from "./color-picker-context";

type Props = {
color: Color;
size: number;
};

export function Swatch({color, size}: Props) {
const {setColor, color: selectedColor} = useContext(ColorPickerContext)!;

const isSelected =
selectedColor &&
selectedColor.red === color.red &&
selectedColor.green === color.green &&
selectedColor.blue === color.blue;

return (
<div
style={{
width: size,
height: size,
background: toCssColor(color),
boxSizing: "border-box",
border: isSelected ? "1px solid black" : "none",
}}
onClick={() => setColor(color)}
/>
);
}
24 changes: 24 additions & 0 deletions src/react-render-perf/lesson-02/solution/child.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {fib} from "../../shared/fib";
import {Circle} from "../../shared/shapes";

type Props = {
circle: Circle;
onClick: () => void;
};

export function ChildComponent({circle, onClick}: Props) {
return (
<div>
<h1>Child Component</h1>
<p>fib(36) = {fib(36)}</p>
<button onClick={onClick}>Click me!</button>
<svg>
<circle
r={circle.radius}
cx={circle.center.x}
cy={circle.center.y}
/>
</svg>
</div>
);
}
10 changes: 10 additions & 0 deletions src/react-render-perf/lesson-02/solution/color-picker-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {createContext} from "react";

import {
colorPickerEventEmitter,
ColorPickerEventEmitter,
} from "./color-picker-event-emitter";

export const ColorPickerContext = createContext<ColorPickerEventEmitter>(
colorPickerEventEmitter,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {EventEmitter} from "eventemitter3";

export class ColorPickerEventEmitter extends EventEmitter {
selectedColor: string | undefined;

public onColorChange(color: string, callback: (state: boolean) => void) {
this.on(color, callback);
}

public onSelectedColorChange(callback: (selectedColor: string) => void) {
this.on("any", callback);
}

public selectColor(color: string) {
if (this.selectedColor && this.selectedColor !== color) {
this.emit(this.selectedColor, false);
}
this.emit(color, true);
this.emit("any", color);
this.selectedColor = color;
}
}

export const colorPickerEventEmitter = new ColorPickerEventEmitter();
23 changes: 23 additions & 0 deletions src/react-render-perf/lesson-02/solution/color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {useContext, useState} from "react";

import Grid from "./grid";
import {ColorPickerContext} from "./color-picker-context";

export function ColorPicker() {
const [color, setColor] = useState<string | undefined>(undefined);
const colorPickerEventEmitter = useContext(ColorPickerContext)!;
colorPickerEventEmitter.onSelectedColorChange(setColor);

return (
<div>
<Grid
topLeft={{red: 0, green: 0, blue: 256}}
topRight={{red: 256, green: 0, blue: 256}}
bottomLeft={{red: 0, green: 256, blue: 256}}
bottomRight={{red: 256, green: 256, blue: 256}}
steps={64} /* the larger the number the worse performance */
/>
current color = {color ? color : "none"}
</div>
);
}
24 changes: 24 additions & 0 deletions src/react-render-perf/lesson-02/solution/gradient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {ReactElement} from "react";

import {Swatch} from "./swatch";
import {Color, lerpColor, toCssColor} from "../../shared/color";

type Props = {
start: Color;
stop: Color;
steps: number;
};

export function Gradient({start, stop, steps}: Props) {
const swatches: Array<ReactElement<typeof Swatch>> = [];
for (let i = 0; i < steps; i++) {
const color = lerpColor(start, stop, i / steps);
swatches.push(
<Swatch key={toCssColor(color)} color={color} size={512 / steps} />,
);
}

return (
<div style={{display: "flex", flexDirection: "row"}}>{swatches}</div>
);
}
37 changes: 37 additions & 0 deletions src/react-render-perf/lesson-02/solution/grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {ReactElement, memo} from "react";
import arePropsEqual from "react-fast-compare";

import {Gradient} from "./gradient";
import {Color, lerpColor, toCssColor} from "../../shared/color";

type Props = {
topLeft: Color;
topRight: Color;
bottomLeft: Color;
bottomRight: Color;
steps: number;
};

export default memo(function Grid({
topLeft,
topRight,
bottomLeft,
bottomRight,
steps,
}: Props) {
const gradients: Array<ReactElement<typeof Gradient>> = [];
for (let i = 0; i < steps; i++) {
const start = lerpColor(topLeft, bottomLeft, i / steps);
const stop = lerpColor(topRight, bottomRight, i / steps);
const key = `${toCssColor(start)}-${toCssColor(stop)}`;
gradients.push(
<Gradient key={key} start={start} stop={stop} steps={steps} />,
);
}

return (
<div style={{display: "flex", flexDirection: "column"}}>
{gradients}
</div>
);
}, arePropsEqual);
10 changes: 10 additions & 0 deletions src/react-render-perf/lesson-02/solution/index copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {ColorPicker} from "./color-picker";

export default function Exercise2() {
return (
<div>
<h1>Solution 2: Prevent Context From Rendering</h1>
<ColorPicker />
</div>
);
}
9 changes: 8 additions & 1 deletion src/react-render-perf/lesson-02/solution/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {ColorPicker} from "./color-picker";

export default function Solution2() {
return <h1>Solution 2: Prevent Context From Rendering</h1>;
return (
<div>
<h1>Solution 2: Prevent Context From Rendering</h1>
<ColorPicker />
</div>
);
}
31 changes: 31 additions & 0 deletions src/react-render-perf/lesson-02/solution/swatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {useContext, useState} from "react";

import {Color, toCssColor} from "../../shared/color";
import {ColorPickerContext} from "./color-picker-context";

type Props = {
color: Color;
size: number;
};

export function Swatch({color, size}: Props) {
const colorPickerEventEmitter = useContext(ColorPickerContext)!;
const [isSelected, setIsSelected] = useState(false);

colorPickerEventEmitter.onColorChange(toCssColor(color), setIsSelected);

return (
<div
style={{
width: size,
height: size,
background: toCssColor(color),
boxSizing: "border-box",
border: isSelected ? "1px solid black" : "none",
}}
onClick={() =>
colorPickerEventEmitter.selectColor(toCssColor(color))
}
/>
);
}
21 changes: 21 additions & 0 deletions src/react-render-perf/shared/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type Color = {
red: number;
green: number;
blue: number;
};

export function toCssColor(color: Color) {
return `rgb(${color.red}, ${color.green}, ${color.blue})`;
}

export function lerpColor(start: Color, end: Color, t: number): Color {
return {
red: lerp(start.red, end.red, t),
green: lerp(start.green, end.green, t),
blue: lerp(start.blue, end.blue, t),
};
}

function lerp(start: number, end: number, t: number) {
return start + (end - start) * t;
}
Loading

0 comments on commit 853e0a3

Please sign in to comment.