├── .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 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 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 | 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 | --------------------------------------------------------------------------------