├── .prettierrc.json
├── .prettierignore
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── .idea
├── .gitignore
├── codeStyles
│ └── codeStyleConfig.xml
├── misc.xml
├── vcs.xml
├── jsLinters
│ └── eslint.xml
├── prettier.xml
├── modules.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── my-app.iml
├── src
├── components
│ ├── batch-state-update
│ │ ├── utils.js
│ │ ├── BatchStateUpdate.jsx
│ │ └── cases
│ │ │ ├── AsyncDataUpdate.jsx
│ │ │ ├── BrowserDataUpdate.jsx
│ │ │ └── UnstableDataUpdate.jsx
│ ├── props-update
│ │ ├── cases
│ │ │ ├── ControlledInput.jsx
│ │ │ ├── InputWithChildren.jsx
│ │ │ ├── TwoInputs.jsx
│ │ │ ├── MemoTwoInputs.jsx
│ │ │ ├── BrokenMemo2Inputs.jsx
│ │ │ └── FixMemo2Inputs.jsx
│ │ └── PropsUpdate.jsx
│ ├── context-update
│ │ ├── ContextUpdate.jsx
│ │ └── cases
│ │ │ ├── UpdateCtx.jsx
│ │ │ ├── UpdateCtxChildren.jsx
│ │ │ └── UpdateCtxBits.jsx
│ └── presentation
│ │ └── Presentation.jsx
├── static
│ └── media
│ │ ├── memo_intro.png
│ │ ├── rail_fail.jpeg
│ │ ├── memo_pocket.jpeg
│ │ └── useCallback_intro.png
├── shared
│ ├── index.jsx
│ ├── ParentPaper.jsx
│ └── MyBox.jsx
├── utils.js
├── index.js
├── index.css
├── hooks
│ ├── useRenderCount.jsx
│ └── useRenderCounter.js
├── pages.jsx
├── App.css
└── App.js
├── .gitignore
├── package.json
└── README.md
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/src/components/batch-state-update/utils.js:
--------------------------------------------------------------------------------
1 | export const getRandomNumber = (num = 0) =>
2 | Math.ceil(Math.random() * Math.pow(10, num));
3 |
--------------------------------------------------------------------------------
/src/static/media/memo_intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/src/static/media/memo_intro.png
--------------------------------------------------------------------------------
/src/static/media/rail_fail.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/src/static/media/rail_fail.jpeg
--------------------------------------------------------------------------------
/src/shared/index.jsx:
--------------------------------------------------------------------------------
1 | import MyBox from './MyBox'
2 | import ParentPaper from "./ParentPaper";
3 |
4 | export {
5 | MyBox,
6 | ParentPaper,
7 | }
8 |
--------------------------------------------------------------------------------
/src/static/media/memo_pocket.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/src/static/media/memo_pocket.jpeg
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export const a11yProps = (index) => ({
2 | id: `simple-tab-${index}`,
3 | 'aria-controls': `simple-tabpanel-${index}`,
4 | });
5 |
6 |
--------------------------------------------------------------------------------
/src/static/media/useCallback_intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/greyGroot/how-to-fail-react-render-optimization/HEAD/src/static/media/useCallback_intro.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/useRenderCount.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useRenderCount = (deps) => {
4 | const [renderCount, setRenderCount] = useState(0);
5 |
6 | const clearCounter = () => setRenderCount(0);
7 |
8 | useEffect(() => {
9 | setRenderCount((renderCount) => renderCount + 1);
10 | }, deps);
11 |
12 | return { renderCount, clearCounter };
13 | };
14 |
15 | export default useRenderCount;
16 |
--------------------------------------------------------------------------------
/src/hooks/useRenderCounter.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | const useRenderCounter = () => {
4 | const renderCount = useRef(0)
5 |
6 | useEffect(() => {
7 | if (renderCount) renderCount.current++;
8 | })
9 |
10 | const clearRenderCount = () => {
11 | if (renderCount) renderCount.current = 0;
12 | }
13 |
14 | return { renderCount, clearRenderCount}
15 | }
16 |
17 | export default useRenderCounter;
18 |
--------------------------------------------------------------------------------
/.idea/my-app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/shared/ParentPaper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paper, Typography } from "@material-ui/core"
3 | import { styled } from '@material-ui/core/styles';
4 |
5 | const StyledPaper = styled(Paper)({
6 | border: '4px solid red',
7 | padding: '8px',
8 | '& .MuiTypography-root': {
9 | fontWeight: 'bold',
10 | }
11 | })
12 |
13 | const ParentPaper = (props) => (
14 |
15 |
16 | Parent component
17 |
18 | {props.children}
19 |
20 | )
21 |
22 | export default ParentPaper;
23 |
--------------------------------------------------------------------------------
/src/pages.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import BatchStateUpdate from "./components/batch-state-update/BatchStateUpdate";
4 | import PropsUpdate from "./components/props-update/PropsUpdate";
5 | import ContextUpdate from "./components/context-update/ContextUpdate";
6 | import Presentation from "./components/presentation/Presentation"
7 |
8 | export const pages = [
9 | {
10 | name: "Presentation",
11 | component: Presentation,
12 | },
13 | {
14 | name: "Props update",
15 | component: PropsUpdate,
16 | },
17 | {
18 | name: "Batch state update",
19 | component: BatchStateUpdate,
20 | },
21 | {
22 | name: "Context update",
23 | component: ContextUpdate,
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/shared/MyBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Typography } from "@material-ui/core";
3 | import { styled } from '@material-ui/core/styles';
4 |
5 | const StyledBox = styled(Box)({
6 | border: props => `2px solid ${props.color || 'black'}`,
7 | borderRadius: '10px',
8 | '& .MuiTypography-root': {
9 | fontWeight: 'bold',
10 | }
11 | })
12 |
13 | const MyBox = (props) => {
14 | const { title, ...rest} = props;
15 |
16 | return (
17 |
18 | {props.title && (
19 |
20 | {props.title}
21 |
22 | )}
23 | {props.children}
24 |
25 | )
26 | };
27 |
28 | export default MyBox;
29 |
--------------------------------------------------------------------------------
/src/components/props-update/cases/ControlledInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Typography } from '@material-ui/core';
3 |
4 | import { MyBox, ParentPaper} from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const ChildComponent = () => {
8 | const {renderCount} = useRenderCounter()
9 |
10 | return (
11 |
12 |
13 | Render count: {renderCount?.current}
14 |
15 |
16 | );
17 | }
18 |
19 | const ControlledInput = () => {
20 | const [value, setValue] = useState('');
21 |
22 | return (
23 |
24 | Controlled Input
25 |
26 | Value: {value}
27 |
28 |
29 |
30 | setValue(e.currentTarget.value)}
33 | value={value}
34 | />
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | export default ControlledInput;
45 |
--------------------------------------------------------------------------------
/src/components/props-update/cases/InputWithChildren.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Typography, Input } from "@material-ui/core";
3 |
4 | import { MyBox, ParentPaper} from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const ChildComponent = () => {
8 | const {renderCount} = useRenderCounter()
9 |
10 | return (
11 |
12 |
13 | Render count: {renderCount?.current}
14 |
15 |
16 | );
17 | }
18 |
19 | const ParentComponent = (props) => {
20 | const [value, setValue] = useState('');
21 |
22 | return (
23 |
24 | Input with children
25 |
26 |
27 | Value: {value}
28 |
29 |
30 | setValue(e.currentTarget.value)}/>
31 |
32 |
33 | {props.children}
34 |
35 |
36 | );
37 | }
38 |
39 | const InputWithChildren = () => (
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | export default InputWithChildren;
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hot-to-fail-react-render-optimization",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.3",
7 | "@material-ui/icons": "^4.11.2",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "lint-staged": "^10.5.4",
12 | "react": "^17.0.1",
13 | "react-dom": "^17.0.1",
14 | "react-router-dom": "^5.2.0",
15 | "react-scripts": "4.0.2",
16 | "web-vitals": "^1.0.1"
17 | },
18 | "lint-staged": {
19 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
20 | "prettier --write"
21 | ]
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "husky": {
48 | "hooks": {
49 | "pre-commit": "lint-staged"
50 | }
51 | },
52 | "devDependencies": {
53 | "eslint": "^7.21.0",
54 | "prettier": "^2.2.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react";
2 | import {AppBar, Tabs, Tab, Box} from '@material-ui/core'
3 |
4 | import "./App.css";
5 | import { a11yProps } from './utils'
6 | import { pages } from "./pages";
7 |
8 |
9 | function TabPanel(props) {
10 | const { children, value, index, ...other } = props;
11 |
12 | return (
13 |
20 | {value === index && (
21 |
22 | {children}
23 |
24 | )}
25 |
26 | );
27 | }
28 |
29 | const App = () => {
30 | const [value, setValue] = useState(0);
31 |
32 | const handleChange = (event, newValue) => {
33 | setValue(newValue);
34 | };
35 |
36 | return (
37 | <>
38 |
39 |
40 | {pages.map((element, id) => (
41 |
42 | ))}
43 |
44 |
45 |
46 | {pages.map((element, id) => (
47 |
48 | {element.component}
49 |
50 | ))}
51 |
52 | >
53 | );
54 | }
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/src/components/props-update/cases/TwoInputs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Typography } from '@material-ui/core';
3 |
4 | import { MyBox, ParentPaper } from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const ChildA = ({valueA}) => {
8 | const {renderCount} = useRenderCounter()
9 |
10 | return (
11 |
12 |
13 | Value: {valueA}
14 | Render count: {renderCount?.current}
15 |
16 |
17 | );
18 | }
19 |
20 | const ChildB = ({valueB}) => {
21 | const {renderCount} = useRenderCounter()
22 |
23 | return (
24 |
25 |
26 | Value B: {valueB}
27 | Render count: {renderCount?.current}
28 |
29 |
30 | );
31 | }
32 |
33 | const TwoInputs = () => {
34 | const [valueA, setValueA] = useState('');
35 | const [valueB, setValueB] = useState('');
36 |
37 | return (
38 |
39 | Two Inputs
40 |
41 |
42 | Input A:
43 | setValueA(e.currentTarget.value)}/>
44 |
45 |
46 | Input B:
47 | setValueB(e.currentTarget.value)}/>
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default TwoInputs;
58 |
--------------------------------------------------------------------------------
/src/components/props-update/cases/MemoTwoInputs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Typography } from '@material-ui/core';
3 |
4 | import { MyBox, ParentPaper } from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const ChildA = React.memo(({valueA}) => {
8 |
9 | const {renderCount} = useRenderCounter()
10 |
11 | return (
12 |
13 |
14 | Value A: {valueA}
15 | Render count: {renderCount?.current}
16 |
17 |
18 | );
19 | })
20 |
21 | const ChildB = React.memo(({valueB}) => {
22 |
23 | const {renderCount} = useRenderCounter()
24 |
25 | return (
26 |
27 |
28 | Value B: {valueB}
29 | Render count: {renderCount?.current}
30 |
31 |
32 | );
33 | })
34 |
35 | const MemoTwoInputs = () => {
36 | const [valueA, setValueA] = useState('');
37 | const [valueB, setValueB] = useState('');
38 |
39 | return (
40 |
41 | Memo Two Inputs
42 |
43 |
44 | Input A:
45 | setValueA(e.currentTarget.value)}/>
46 |
47 |
48 | Input B:
49 | setValueB(e.currentTarget.value)}/>
50 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | export default MemoTwoInputs;
60 |
--------------------------------------------------------------------------------
/src/components/props-update/cases/BrokenMemo2Inputs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Typography } from '@material-ui/core';
3 |
4 | import { MyBox, ParentPaper } from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const Child = React.memo(({obj, clearValue}) => {
8 | const { renderCount } = useRenderCounter()
9 |
10 | return (
11 |
12 |
13 | {obj.name}: {obj.value}
14 | Render count: {renderCount?.current}
15 |
16 |
17 |
18 |
19 | );
20 | })
21 |
22 | const BrokenMemo2Inputs = () => {
23 | const [valueA, setValueA] = useState('');
24 | const [valueB, setValueB] = useState('');
25 |
26 | return (
27 |
28 | Broken Memo Two Inputs
29 |
30 |
31 | Input A:
32 | setValueA(e.currentTarget.value)}
35 | value={valueA}
36 | />
37 |
38 |
39 |
40 |
41 | Input B:
42 | setValueB(e.currentTarget.value)}
45 | value={valueB}
46 | />
47 |
48 |
49 |
50 | setValueA('')}
53 | />
54 | setValueB('')} // {} !== {}
57 | />
58 |
59 |
60 | );
61 | }
62 |
63 | export default BrokenMemo2Inputs;
64 |
--------------------------------------------------------------------------------
/src/components/batch-state-update/BatchStateUpdate.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Grid, Box, Accordion, AccordionSummary, Typography, AccordionDetails} from "@material-ui/core";
3 |
4 | import AsyncDataUpdate from "./cases/AsyncDataUpdate";
5 | import BrowserDataUpdate from "./cases/BrowserDataUpdate";
6 | import UnstableDataUpdate from "./cases/UnstableDataUpdate";
7 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
8 |
9 | const BatchStateUpdate = () => {
10 | return (
11 | <>
12 |
13 |
14 | }
16 | >
17 | How many renders?
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | }
37 | >
38 | Lets try one more time
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | >
50 | );
51 | };
52 |
53 | export default BatchStateUpdate;
54 |
--------------------------------------------------------------------------------
/src/components/props-update/cases/FixMemo2Inputs.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useCallback, useRef} from 'react';
2 | import { Box, Typography } from '@material-ui/core';
3 |
4 | import { MyBox, ParentPaper } from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const Child = React.memo(({name, value, clearValue}) => {
8 | const { renderCount } = useRenderCounter()
9 |
10 | return (
11 |
12 |
13 | {name}: {value}
14 | Render count: {renderCount?.current}
15 |
16 |
17 |
18 |
19 | );
20 | })
21 |
22 | const FixMemo2Inputs = () => {
23 | const [valueA, setValueA] = useState('');
24 | const [valueB, setValueB] = useState('');
25 |
26 | const clearValueA = useCallback(() => setValueA(''), [])
27 | const clearValueB = useRef(() => setValueB(''));// { current: щось лежить }
28 |
29 | return (
30 |
31 | Fix Memo Two Inputs
32 |
33 |
34 | Input A:
35 | setValueA(e.currentTarget.value)}
38 | value={valueA}
39 | />
40 |
41 |
42 |
43 |
44 | Input B:
45 | setValueB(e.currentTarget.value)}
48 | value={valueB}
49 | />
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default FixMemo2Inputs;
65 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/batch-state-update/cases/AsyncDataUpdate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import {
3 | Typography,
4 | List,
5 | ListItem,
6 | Button,
7 | Box,
8 | Paper,
9 | } from "@material-ui/core";
10 |
11 | import { getRandomNumber } from "../utils";
12 | import useRenderCount from "../../../hooks/useRenderCount";
13 |
14 | const AsyncDataUpdate = () => {
15 | const [firstNum, setFirstNum] = useState(null);
16 | const [secondNum, setSecondNum] = useState(null);
17 | const [thirdNum, setThirdNum] = useState(null);
18 | const [updateState, setUpdateState] = useState(false);
19 |
20 | const { renderCount, clearCounter } = useRenderCount([
21 | firstNum,
22 | secondNum,
23 | thirdNum,
24 | ]);
25 |
26 | useEffect(() => {
27 | if (updateState) {
28 | setTimeout(() => {
29 | setFirstNum(getRandomNumber(1));
30 | setSecondNum(getRandomNumber(2));
31 | setThirdNum(getRandomNumber(3));
32 |
33 | setUpdateState(false);
34 | }, 500);
35 | }
36 | }, [updateState]);
37 |
38 | const handleClick = () => {
39 | setUpdateState(true);
40 | };
41 |
42 | const handleClear = () => {
43 | setFirstNum(null);
44 | setSecondNum(null);
45 | setThirdNum(null);
46 | clearCounter();
47 | };
48 |
49 | return (
50 |
51 |
52 | Async Data Update
53 |
54 |
55 |
56 | First Number: {firstNum}
57 | Second Number: {secondNum}
58 | Third Number: {thirdNum}
59 |
60 |
61 |
62 |
63 | Component was rendered {renderCount} times
64 |
67 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default AsyncDataUpdate;
77 |
--------------------------------------------------------------------------------
/src/components/batch-state-update/cases/BrowserDataUpdate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from "react";
2 | import {
3 | Typography,
4 | List,
5 | ListItem,
6 | Button,
7 | Box,
8 | Paper,
9 | } from "@material-ui/core";
10 |
11 | import { getRandomNumber } from "../utils";
12 | import useRenderCount from "../../../hooks/useRenderCount";
13 |
14 | const BrowserDataUpdate = () => {
15 | const [firstNum, setFirstNum] = useState(null);
16 | const [secondNum, setSecondNum] = useState(null);
17 | const [thirdNum, setThirdNum] = useState(null);
18 |
19 | const { renderCount, clearCounter } = useRenderCount([
20 | firstNum,
21 | secondNum,
22 | thirdNum,
23 | ]);
24 |
25 | const keydownHandler = useCallback((event) => {
26 | event.preventDefault();
27 |
28 | if (event.key === "Enter") {
29 | setFirstNum(getRandomNumber(1));
30 | setSecondNum(getRandomNumber(2));
31 | setThirdNum(getRandomNumber(3));
32 | }
33 | }, []);
34 |
35 | useEffect(() => {
36 | document.addEventListener("keydown", keydownHandler, false);
37 |
38 | return () => {
39 | document.removeEventListener("keydown", keydownHandler, false);
40 | };
41 | }, [keydownHandler]);
42 |
43 | const handleClear = () => {
44 | setFirstNum(null);
45 | setSecondNum(null);
46 | setThirdNum(null);
47 | clearCounter();
48 | };
49 |
50 | return (
51 |
52 |
53 | Browser Data Update
54 |
55 |
56 |
57 | First Number: {firstNum}
58 | Second Number: {secondNum}
59 | Third Number: {thirdNum}
60 |
61 |
62 |
63 |
64 | Component was rendered {renderCount} times
65 |
68 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default BrowserDataUpdate;
78 |
--------------------------------------------------------------------------------
/src/components/context-update/ContextUpdate.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Box, Grid, Accordion, AccordionSummary, AccordionDetails, Typography} from '@material-ui/core'
3 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
4 |
5 | import UpdateCtx from './cases/UpdateCtx'
6 | import UpdateCtxChildren from "./cases/UpdateCtxChildren";
7 | import UpdateCtxBits from "./cases/UpdateCtxBits";
8 |
9 | const ContextUpdate = () => (
10 | <>
11 |
12 |
13 | }
15 | >
16 | Update Context
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | }
32 | >
33 | Update Context with a pinch of optimization
34 |
35 |
36 |
37 |
38 | {/**/}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | }
52 | >
53 | Update Context like a Boss
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {/**/}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | >
70 | )
71 |
72 | export default ContextUpdate;
73 |
--------------------------------------------------------------------------------
/src/components/batch-state-update/cases/UnstableDataUpdate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Typography,
5 | List,
6 | ListItem,
7 | Button,
8 | Box,
9 | Paper,
10 | } from "@material-ui/core";
11 |
12 | import { getRandomNumber } from "../utils";
13 | import useRenderCount from "../../../hooks/useRenderCount";
14 |
15 | const UnstableDataUpdate = () => {
16 | const [firstNum, setFirstNum] = useState(null);
17 | const [secondNum, setSecondNum] = useState(null);
18 | const [thirdNum, setThirdNum] = useState(null);
19 | const [updateState, setUpdateState] = useState(false);
20 |
21 | const { renderCount, clearCounter } = useRenderCount([
22 | firstNum,
23 | secondNum,
24 | thirdNum,
25 | ]);
26 |
27 | useEffect(() => {
28 | if (updateState) {
29 | setTimeout(() => {
30 | ReactDOM.unstable_batchedUpdates(() => {
31 | setFirstNum(getRandomNumber(1));
32 | setSecondNum(getRandomNumber(2));
33 | setThirdNum(getRandomNumber(3));
34 |
35 | setUpdateState(false);
36 | })
37 | }, 0);
38 | }
39 | }, [updateState]);
40 |
41 | const handleClick = () => {
42 | setUpdateState(true);
43 | };
44 |
45 | const handleClear = () => {
46 | setFirstNum(null);
47 | setSecondNum(null);
48 | setThirdNum(null);
49 | clearCounter();
50 | };
51 |
52 | return (
53 |
54 |
55 | Unstable Data Update
56 |
57 |
58 |
59 | First Number: {firstNum}
60 | Second Number: {secondNum}
61 | Third Number: {thirdNum}
62 |
63 |
64 |
65 |
66 | Component was rendered {renderCount} times
67 |
70 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default UnstableDataUpdate;
80 |
--------------------------------------------------------------------------------
/src/components/presentation/Presentation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Card, CardMedia, CardContent, Typography } from "@material-ui/core";
3 | import { makeStyles } from "@material-ui/core";
4 |
5 | import MemoPocketImg from '../../static/media/memo_pocket.jpeg'
6 | import RailFailImg from '../../static/media/rail_fail.jpeg'
7 | import ReactMemoImg from '../../static/media/memo_intro.png'
8 | import UseCallbackImg from '../../static/media/useCallback_intro.png'
9 |
10 | const useStyles = makeStyles({
11 | root: {
12 | marginBottom: '20px',
13 | maxHeight: '1000px',
14 | maxWidth: '1000px',
15 | }
16 | })
17 |
18 | const Presentation = () => {
19 | const classes = useStyles();
20 |
21 | return (
22 |
27 |
28 |
29 | How to Fail React Render Optimization?
30 |
31 |
37 |
38 |
39 |
40 | What is Memoization?
41 |
42 |
48 |
49 |
50 |
51 | React.memo
52 |
53 |
59 |
60 |
61 |
62 |
63 | React.useCallback
64 |
65 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default Presentation;
77 |
--------------------------------------------------------------------------------
/src/components/props-update/PropsUpdate.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Box, Grid, Accordion, AccordionSummary, AccordionDetails, Typography} from '@material-ui/core'
3 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
4 |
5 | import ControlledInput from "./cases/ControlledInput";
6 | import InputWithChildren from "./cases/InputWithChildren";
7 | import TwoInputs from "./cases/TwoInputs";
8 | import MemoTwoInputs from "./cases/MemoTwoInputs";
9 | import BrokenMemo2Inputs from "./cases/BrokenMemo2Inputs";
10 | import FixMemo2Inputs from "./cases/FixMemo2Inputs";
11 |
12 | const PropsUpdate = () => (
13 | <>
14 |
15 |
16 | }
18 | >
19 | No need for Memo
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | }
37 | aria-controls="panel2a-content"
38 | id="panel2a-header"
39 | >
40 | I got need for Memo
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | }
58 | >
59 | Just Memo it!
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | >
73 | );
74 |
75 | export default PropsUpdate;
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # How to Fail React Render Optimization?
2 |
3 | Use memo or not use memo? When do unexpected renders happen and how to avoid them? We will go through the main cases in React and discuss how to spot them during code review.
4 |
5 | ## React.context + observedBits
6 |
7 | ### Must read:
8 | * [Investigate use of context + observedBits for performance optimization](https://github.com/reduxjs/react-redux/issues/1018)
9 | * [A Secret parts of React New Context API](https://koba04.medium.com/a-secret-parts-of-react-new-context-api-e9506a4578aa)
10 | * [Expressions and operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators)
11 | * [React Context: a Hidden Power](https://dev.to/alexkhismatulin/react-context-a-hidden-power-3h8j)
12 |
13 | ## Batched State Update in Async function
14 |
15 | ### Must read:
16 | * [React State Batch Update](https://medium.com/swlh/react-state-batch-update-b1b61bd28cd2)
17 | * [Simplifying state management in React apps with batched updates](https://blog.logrocket.com/simplifying-state-management-in-react-apps-with-batched-updates/)
18 |
19 | ## About React Memoization
20 |
21 | ### Must read:
22 | * [A (Mostly) Complete Guide to React Rendering Behavior](https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/)
23 | * [Use React.memo() wisely](https://dmitripavlutin.com/use-react-memo-wisely/#1-reactmemo)
24 | * [Код на React и TypeScript, который работает быстро. Доклад Яндекса](https://habr.com/ru/company/yandex/blog/536682/)
25 | * [Fix the slow render before you fix the re-render](https://kentcdodds.com/blog/fix-the-slow-render-before-you-fix-the-re-render)
26 | * [Reconciliation](https://reactjs.org/docs/reconciliation.html)
27 |
28 | ### Must watch:
29 | * [Я.Субботник по разработке интерфейсов](https://www.youtube.com/watch?v=wTkeS-X_OIU&t=5652s)
30 | * [Подробно о React Reconciliation, или Как React добился 60 fps](https://youtu.be/NPXJnKytER4)
31 | * [ReactJS под капотом](https://youtu.be/A0W2n2azH5s)
32 |
33 | #### Would be nice to read:
34 | * [What the fuck is memoization](https://whatthefuck.is/memoization)
35 | * [React, Inline Functions, and Performance](https://medium.com/componentdidblog/react-inline-functions-and-performance-bdff784f5578)
36 | * [Optimizing React Rendering](https://flexport.engineering/optimizing-react-rendering-part-1-9634469dca02#432e)
37 | * [Avoid Inline Function Definition in the Render Function](https://www.codementor.io/blog/react-optimization-5wiwjnf9hj#7-avoid-inline-function-definition-in-the-render-function)
38 | * [When to useMemo and useCallback](https://kentcdodds.com/blog/usememo-and-usecallback)
39 | * [Your Guide to React.useCallback()](https://dmitripavlutin.com/dont-overuse-react-usecallback/)
40 | * [Optimize Conditional Rendering in React](https://medium.com/technofunnel/https-medium-com-mayank-gupta-6-88-21-performance-optimizations-techniques-for-react-d15fa52c2349#a1c2)
41 | * [When should you NOT use React memo?](https://github.com/facebook/react/issues/14463)
42 |
43 | ## How to start project?
44 |
45 | ### `yarn start`
46 |
47 | Runs the app in the development mode.\
48 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
49 |
50 | The page will reload if you make edits.\
51 | You will also see any lint errors in the console.
52 |
53 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
54 |
--------------------------------------------------------------------------------
/src/components/context-update/cases/UpdateCtx.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import { Grid, Typography } from '@material-ui/core'
3 |
4 | import { MyBox, ParentPaper} from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const ValueCtx = React.createContext({
8 | valueA: '',
9 | valueB: '',
10 | update: (key, value) => {}
11 | })
12 |
13 | const SubChildB = () => {
14 | const { renderCount } = useRenderCounter();
15 | const {valueB, update} = useContext(ValueCtx)
16 |
17 | const [value, setValue] = useState(valueB)
18 |
19 | const handleClick = () => update('valueB', value)
20 |
21 | return (
22 |
23 |
24 | rendered times: {renderCount.current}
25 | value B: {valueB}
26 | setValue(e.currentTarget.value)}/>
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | const SubChildA = () => {
34 | const { renderCount } = useRenderCounter();
35 | const {valueA, update} = useContext(ValueCtx)
36 |
37 | const [value, setValue] = useState(valueA)
38 |
39 | const handleClick = () => update('valueA', value)
40 |
41 | return (
42 |
43 |
44 | rendered times: {renderCount.current}
45 | value A: {valueA}
46 | setValue(e.currentTarget.value)}/>
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | const ChildB = () => {
54 | const { renderCount } = useRenderCounter();
55 |
56 | return (
57 |
58 | rendered times: {renderCount.current}
59 |
60 |
61 | );
62 | }
63 |
64 | const ChildA = () => {
65 | const { renderCount } = useRenderCounter();
66 |
67 | return (
68 |
69 | rendered times: {renderCount.current}
70 |
71 |
72 | );
73 | }
74 |
75 | const Child0 = () => {
76 | const { renderCount } = useRenderCounter();
77 |
78 | return (
79 |
80 | Child 0 rendered times: {renderCount.current}
81 |
82 | );
83 | }
84 |
85 | const Child$ = () => {
86 | const { renderCount } = useRenderCounter();
87 |
88 | return (
89 |
90 |
91 | rendered times: {renderCount.current}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | }
107 |
108 | const UpdateCtx = () => {
109 | const [values, setValues] = useState({
110 | valueA: '',
111 | valueB: '',
112 | })
113 |
114 | const update = (key, value) =>
115 | setValues(values => ({
116 | ...values,
117 | [key]: value
118 | })
119 | )
120 |
121 | const valueCtx = {
122 | ...values,
123 | update,
124 | }
125 |
126 | return (
127 |
128 | Update Ctx
129 |
130 |
131 |
132 |
133 |
134 |
135 | );
136 | }
137 |
138 | export default UpdateCtx
139 |
--------------------------------------------------------------------------------
/src/components/context-update/cases/UpdateCtxChildren.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import { Grid, Typography } from '@material-ui/core'
3 |
4 | import { MyBox, ParentPaper} from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const SubChildB = () => {
8 | const { renderCount } = useRenderCounter();
9 | const {valueB, update} = useContext(ValueCtx)
10 |
11 | const [value, setValue] = useState(valueB)
12 |
13 | const handleClick = () => update('valueB', value)
14 |
15 | return (
16 |
17 |
18 | rendered times: {renderCount.current}
19 | value B: {valueB}
20 | setValue(e.currentTarget.value)}/>
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | const SubChildA = () => {
28 | const { renderCount } = useRenderCounter();
29 | const {valueA, update} = useContext(ValueCtx)
30 |
31 | const [value, setValue] = useState(valueA)
32 |
33 | const handleClick = () => update('valueA', value)
34 |
35 | return (
36 |
37 |
38 | rendered times: {renderCount.current}
39 | value A: {valueA}
40 | setValue(e.currentTarget.value)}/>
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | const ChildB = () => {
48 | const { renderCount } = useRenderCounter();
49 |
50 | return (
51 |
52 | rendered times: {renderCount.current}
53 |
54 |
55 | );
56 | }
57 |
58 | const ChildA = () => {
59 | const { renderCount } = useRenderCounter();
60 |
61 | return (
62 |
63 | rendered times: {renderCount.current}
64 |
65 |
66 | );
67 | }
68 |
69 | const Child0 = () => {
70 | const { renderCount } = useRenderCounter();
71 |
72 | return (
73 |
74 | Child 0 rendered times: {renderCount.current}
75 |
76 | );
77 | }
78 |
79 | const Child$ = () => {
80 | const { renderCount } = useRenderCounter();
81 |
82 | return (
83 |
84 | rendered times: {renderCount.current}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | }
99 |
100 | const initialCtx = {
101 | values: {
102 | valueA: '',
103 | valueB: '',
104 | }
105 | }
106 |
107 | const ValueCtx = React.createContext(initialCtx)
108 |
109 | const CtxProvider = (props) => {
110 | const [values, setValues] = useState({
111 | valueA: '',
112 | valueB: '',
113 | })
114 |
115 | const update = (key, value) =>
116 | setValues(values => ({
117 | ...values,
118 | [key]: value
119 | })
120 | )
121 |
122 | const valueCtx = {
123 | ...values,
124 | update,
125 | }
126 |
127 | return (
128 |
129 | {props.children}
130 |
131 | );
132 | }
133 |
134 | const UpdateCtxChildren = () => (
135 |
136 | Update Ctx Children
137 |
138 |
139 |
140 |
141 |
142 |
143 | )
144 |
145 | export default UpdateCtxChildren
146 |
--------------------------------------------------------------------------------
/src/components/context-update/cases/UpdateCtxBits.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import {Typography, Grid} from '@material-ui/core'
3 |
4 | import { MyBox, ParentPaper} from "../../../shared";
5 | import useRenderCounter from "../../../hooks/useRenderCounter";
6 |
7 | const SubChildB = () => {
8 | const { renderCount } = useRenderCounter();
9 | const {values: {valueB}, updateValue} = useContext(ValueCtx, observedBitsMap.valueB)
10 |
11 | const [value, setValue] = useState(valueB)
12 |
13 | const handleClick = () => updateValue('valueB', value)
14 |
15 | return (
16 |
17 |
18 | Sub Child B rendered times: {renderCount.current}
19 | value B: {valueB}
20 | setValue(e.currentTarget.value)}/>
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | const SubChildA = () => {
28 | const { renderCount } = useRenderCounter();
29 | const {values: {valueA}, updateValue} = useContext(ValueCtx, observedBitsMap.valueA)
30 |
31 | const [value, setValue] = useState(valueA)
32 |
33 | const handleClick = () => updateValue('valueA', value)
34 |
35 | return (
36 |
37 |
38 | Sub Child A rendered times: {renderCount.current}
39 | value A: {valueA}
40 | setValue(e.currentTarget.value)}/>
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | const ChildB = () => {
48 | const { renderCount } = useRenderCounter();
49 |
50 | return (
51 |
52 | rendered times: {renderCount.current}
53 |
54 |
55 | );
56 | }
57 |
58 | const ChildA = () => {
59 | const { renderCount } = useRenderCounter();
60 |
61 | return (
62 |
63 | rendered times: {renderCount.current}
64 |
65 |
66 | );
67 | }
68 |
69 | const Child0 = () => {
70 | const { renderCount } = useRenderCounter();
71 |
72 | return (
73 |
74 | Child 0 rendered times: {renderCount.current}
75 |
76 | );
77 | }
78 |
79 | const Child$ = () => {
80 | const { renderCount } = useRenderCounter();
81 |
82 | return (
83 |
84 | rendered times: {renderCount.current}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | }
99 |
100 | const observedBitsMap = {
101 | valueA: 0b01,
102 | valueB: 0b10,
103 | };
104 |
105 | const calculateChangedBits = (currentValues, nextValues) => {
106 | let result = 0;
107 |
108 | Object.entries(nextValues.values).forEach(([key, value]) => {
109 | if (value !== currentValues.values[key]) {
110 | result = result | observedBitsMap[key];
111 | }
112 | });
113 |
114 | return result;
115 | };
116 |
117 | const initialCtx = {
118 | values: {
119 | valueA: '',
120 | valueB: '',
121 | }
122 | }
123 |
124 | const ValueCtx = React.createContext(initialCtx, calculateChangedBits)
125 |
126 | const CtxProvider = (props) => {
127 | const ctx = useContext(ValueCtx)
128 | const [values, setValues] = useState(ctx.values)
129 |
130 | const updateValue = (key, value) =>
131 | setValues(values => ({
132 | ...values,
133 | [key]: value
134 | })
135 | )
136 |
137 | return (
138 |
139 | {props.children}
140 |
141 | );
142 | }
143 |
144 | const UpdateCtxBits = () => (
145 |
146 | Update Ctx Bits
147 |
148 |
149 |
150 |
151 |
152 |
153 | )
154 |
155 | export default UpdateCtxBits
156 |
--------------------------------------------------------------------------------