From f14c8bb68ea846eb0fca0868756441c20197d523 Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Mon, 8 Jan 2024 10:43:39 +0800 Subject: [PATCH 1/2] feat: test using memo --- client/.gitignore | 3 + client/jsconfig.json | 3 +- client/src/components/home/items.json | 4 + .../memo/components/fulltable/index.js | 92 +++++++++++++++++++ .../memo/components/memoizedtable/index.js | 80 ++++++++++++++++ .../memo/components/tablerow/index.js | 73 +++++++++++++++ .../memo/components/unoptimizedtable/index.js | 83 +++++++++++++++++ client/src/features/memo/index.js | 5 + .../memo/tablesdemo/TablesDemo.module.css | 46 ++++++++++ client/src/features/memo/tablesdemo/index.js | 25 +++++ client/src/lib/data/characters.json | 12 +++ client/src/pages/memo/index.js | 7 ++ 12 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 client/src/features/memo/components/fulltable/index.js create mode 100644 client/src/features/memo/components/memoizedtable/index.js create mode 100644 client/src/features/memo/components/tablerow/index.js create mode 100644 client/src/features/memo/components/unoptimizedtable/index.js create mode 100644 client/src/features/memo/index.js create mode 100644 client/src/features/memo/tablesdemo/TablesDemo.module.css create mode 100644 client/src/features/memo/tablesdemo/index.js create mode 100644 client/src/lib/data/characters.json create mode 100644 client/src/pages/memo/index.js diff --git a/client/.gitignore b/client/.gitignore index 12bc6b5..9003584 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -32,3 +32,6 @@ yarn-error.log* .vercel .env + +*.zip +*.rar diff --git a/client/jsconfig.json b/client/jsconfig.json index f93b09f..341cc20 100644 --- a/client/jsconfig.json +++ b/client/jsconfig.json @@ -4,7 +4,8 @@ "@/*": ["./src/*"], "@/hooks/*": ["./src/lib/hooks/*"], "@/public/*": ["public/*"], - "@/store/*": ["./src/lib/store/*"] + "@/store/*": ["./src/lib/store/*"], + "@/data/*": ["./src/lib/data/*"] } } } diff --git a/client/src/components/home/items.json b/client/src/components/home/items.json index a18c185..f14ce3e 100644 --- a/client/src/components/home/items.json +++ b/client/src/components/home/items.json @@ -18,5 +18,9 @@ { "name": "useReducer", "link": "/usereducer" + }, + { + "name": "memo", + "link": "/memo" } ] \ No newline at end of file diff --git a/client/src/features/memo/components/fulltable/index.js b/client/src/features/memo/components/fulltable/index.js new file mode 100644 index 0000000..8cb07e6 --- /dev/null +++ b/client/src/features/memo/components/fulltable/index.js @@ -0,0 +1,92 @@ +import { useEffect, useState } from 'react' + +import characters from '@/data/characters.json' +import styles from '../../tablesdemo/TablesDemo.module.css' + +function FullTable () { + const [data, setData] = useState(characters) + const [headers, setHeaders] = useState([]) + + useEffect(() => { + if (headers.length === 0) { + setHeaders(Object.keys(data[0]).map((key, id) => ({ + id, + name: key + }))) + } + }, [headers, data]) + + const handleCellUpdate = (rowId, field, newValue) => { + if (data[rowId][field] === parseFloat(newValue)) return + + setData(prev => + prev.map(row => + row.id === rowId ? { ...row, [field]: parseFloat(newValue) } : row + ) + ) + } + + const handleKeyDown = (e, rowIndex, colIndex) => { + // Move cursor to next row + const { keyCode } = e + if (keyCode !== 13) return + + const nextIndex = (rowIndex === data.length - 1) + ? 0 : rowIndex + 1 + + const nextId = `cell-${nextIndex}-${colIndex}` + const next = document.getElementById(nextId) + next?.focus() + } + + return ( +
+
+

Full Table re-rendering

+
    +
  • On edit, this table renders the object array data using map(), rendering the full table.
  • +
+
+ +
+ + + + {headers?.map(column => ( + + ))} + + + + {data.map((player, rowIndex) => ( + + {headers?.map((field, colIndex) => ( + + ))} + + ))} + +
+ {column.name} +
+ {(['id', 'name'].includes(field)) + ? player[field] + : e.target.select()} + onBlur={(e) => { + const { value } = e.target + handleCellUpdate(rowIndex, field.name, value) + }} + onKeyDown={(e) => handleKeyDown(e, rowIndex, colIndex)} + /> + } +
+
+
+ ) +} + +export default FullTable diff --git a/client/src/features/memo/components/memoizedtable/index.js b/client/src/features/memo/components/memoizedtable/index.js new file mode 100644 index 0000000..3a0b1b3 --- /dev/null +++ b/client/src/features/memo/components/memoizedtable/index.js @@ -0,0 +1,80 @@ +import { useEffect, useState, useCallback } from 'react' + +import TableRow from '../tablerow' + +import characters from '@/data/characters.json' +import styles from '../../tablesdemo/TablesDemo.module.css' + +const MemoizedTable = () => { + const [players, setData] = useState(characters) + const [headers, setHeaders] = useState([]) + + useEffect(() => { + if (headers.length === 0) { + setHeaders(Object.keys(players[0]).map((key, id) => ({ + id, + name: key + }))) + } + }, [headers, players]) + + // Wrap anonymous functions in useCallback() to prevent re-renders on child components. + // Sometimes, local state may need to be included in its dependency array + const handleCellUpdate = useCallback((rowId, field, newValue) => { + setData((prevData) => { + const tempData = [...prevData] + const updatedValue = parseFloat(newValue) + + // Update only the affected field in an object element + if (tempData[rowId][field] !== updatedValue) { + tempData[rowId] = { + ...tempData[rowId], [field]: updatedValue + } + } + + return tempData + }) + }, []) + + return ( +
+
+

Optimized Table row re-rendering

+
    +
  • This table renders the object array data using map().
  • +
  • On edit, it renders only an "updated" table row using a memoized TableRow component.
  • +
+
+ +
+ + + + {headers?.map(column => ( + + ))} + + + + {players?.map((player, rowIndex) => ( + + ))} + +
+ {column.name} +
+
+
+ ) +} + +export default MemoizedTable diff --git a/client/src/features/memo/components/tablerow/index.js b/client/src/features/memo/components/tablerow/index.js new file mode 100644 index 0000000..daa8063 --- /dev/null +++ b/client/src/features/memo/components/tablerow/index.js @@ -0,0 +1,73 @@ +import { memo } from 'react' +import PropTypes from 'prop-types' + +/** + * Notes: + * + * This table row component re-renders only if its props changes. + * props.onEdit, an anonymous function, while looking constant also re-renders + * so be sure to wrap it in a useCallback hook in it's parent component. + * + * Try: + * Observe this component's re-renders on the React Profile with and without the memo() hook. + */ +function TableRow ({ + nextIndex, + rowIndex, + headers, + player, + onEdit, + key, + idPrefix = 'm' +}) { + console.log(`--Re-rendering for update: ${player.name}`) + + const handlePlayerEdit = (e, rowIndex, field) => { + const { value } = e.target + if (player[field] === parseFloat(value)) return + + onEdit(rowIndex, field, value) + } + + const handleKeyDown = (e, fieldIndex) => { + // Move cursor to next row + const { keyCode } = e + if (keyCode !== 13) return + + const nextId = `${idPrefix}-cell-${nextIndex}-${fieldIndex}` + const next = document.getElementById(nextId) + next?.focus() + } + + return ( + + {headers?.map((field, fieldIndex) => ( + + {(['id', 'name'].includes(field.name)) + ? player[field.name] + : handlePlayerEdit(e, rowIndex, field.name)} + onFocus={(e) => e.target.select()} + onKeyDown={(e) => handleKeyDown(e, fieldIndex)} + /> + } + + ))} + + ) +} + +TableRow.propTypes = { + nextIndex: PropTypes.number, + rowIndex: PropTypes.number, + headers: PropTypes.object, + player: PropTypes.arrayOf(PropTypes.object), + onEdit: PropTypes.func, + key: PropTypes.number, + idPrefix: PropTypes.string +} + +export default memo(TableRow) diff --git a/client/src/features/memo/components/unoptimizedtable/index.js b/client/src/features/memo/components/unoptimizedtable/index.js new file mode 100644 index 0000000..0fdf3d6 --- /dev/null +++ b/client/src/features/memo/components/unoptimizedtable/index.js @@ -0,0 +1,83 @@ +import { useEffect, useState } from 'react' + +import TableRow from '../tablerow' + +import characters from '@/data/characters.json' +import styles from '../../tablesdemo/TablesDemo.module.css' + +const UnoptimizedTable = () => { + const [players, setData] = useState(characters) + const [headers, setHeaders] = useState([]) + + useEffect(() => { + if (headers.length === 0) { + setHeaders(Object.keys(players[0]).map((key, id) => ({ + id, + name: key + }))) + } + }, [headers, players]) + + // Wrap anonymous functions in useCallback() to prevent re-renders on child components. + // Sometimes, local state may need to be included in its dependency array + const handleCellUpdate = (rowId, field, newValue) => { + setData((prevData) => { + const tempData = [...prevData] + const updatedValue = parseFloat(newValue) + + // Update only the affected field in an object element + if (tempData[rowId][field] !== updatedValue) { + tempData[rowId] = { + ...tempData[rowId], [field]: updatedValue + } + } + + return tempData + }) + } + + return ( +
+
+

Table re-rendering all rows (WARNING!)

+
    +
  • This table renders the object array data using map().
  • +
  • It‘s using a memoized TableRow component but
  • +
  • it's handleCellUpdate() method, an anonymous function is not memoized using useCallback().
  • +
  • On edit, it renders all table rows.
  • +
+
+ +
+ + + + {headers?.map(column => ( + + ))} + + + + {players?.map((player, rowIndex) => ( + + ))} + +
+ {column.name} +
+
+
+ ) +} + +export default UnoptimizedTable diff --git a/client/src/features/memo/index.js b/client/src/features/memo/index.js new file mode 100644 index 0000000..03a94c2 --- /dev/null +++ b/client/src/features/memo/index.js @@ -0,0 +1,5 @@ +import TablesDemo from './tablesdemo' + +export { + TablesDemo +} diff --git a/client/src/features/memo/tablesdemo/TablesDemo.module.css b/client/src/features/memo/tablesdemo/TablesDemo.module.css new file mode 100644 index 0000000..2ff8010 --- /dev/null +++ b/client/src/features/memo/tablesdemo/TablesDemo.module.css @@ -0,0 +1,46 @@ +.demoContainer { + width: 100vw; + display: grid; + place-content: center; +} + +.container table { + border: 1px solid #000; + border-collapse: collapse; + width: 600px; + margin-bottom: 32px; +} + +.container th { + font-weight: bold; + border: 1px solid #000; + text-align: center; +} + +.container td { + width: 150px; + border: 1px solid #000; + text-align: center; +} + +.container input { + text-align: center; +} + +.container input { + border: none; + width: 100%; +} + +.container input:focus { + outline: none; +} + +.description { + padding: 8px; +} + +.subDescription { + padding: 16px; + font-size: 14px; +} \ No newline at end of file diff --git a/client/src/features/memo/tablesdemo/index.js b/client/src/features/memo/tablesdemo/index.js new file mode 100644 index 0000000..485da6c --- /dev/null +++ b/client/src/features/memo/tablesdemo/index.js @@ -0,0 +1,25 @@ +import FullTable from '../components/fulltable' +import MemoizedTable from '../components/memoizedtable' +import UnoptimizedTable from '../components/unoptimizedtable' +import styles from './TablesDemo.module.css' + +function TablesDemo () { + return ( +
+

Tables

+ +
+ These table renders data from an object array.
+ An update to a cell item updates its whole array data source. +
+ + + + + + +
+ ) +} + +export default TablesDemo diff --git a/client/src/lib/data/characters.json b/client/src/lib/data/characters.json new file mode 100644 index 0000000..f97dfa6 --- /dev/null +++ b/client/src/lib/data/characters.json @@ -0,0 +1,12 @@ +[ + { "id": 0, "name": "razor", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 1, "name": "ei", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 2, "name": "childe", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 3, "name": "ningguang", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 4, "name": "kazuha", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 5, "name": "ayaka", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 6, "name": "xinqui", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 7, "name": "nahida", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 8, "name": "kazuha", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 }, + { "id": 9, "name": "bennet", "hp": 0, "atk": 0, "def": 0, "crit_rate": 0, "crit_dmg": 0 } +] \ No newline at end of file diff --git a/client/src/pages/memo/index.js b/client/src/pages/memo/index.js new file mode 100644 index 0000000..02cecfa --- /dev/null +++ b/client/src/pages/memo/index.js @@ -0,0 +1,7 @@ +import { TablesDemo } from '@/features/memo' + +function MemoPage () { + return () +} + +export default MemoPage From 4cfedb89f31ebae350bf01575b57eb3b0038c1fb Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Mon, 8 Jan 2024 11:02:31 +0800 Subject: [PATCH 2/2] chore: update README --- README.md | 16 +++++++++++++++- client/src/components/home/index.js | 1 + .../features/memo/components/fulltable/index.js | 2 +- .../memo/components/memoizedtable/index.js | 2 +- .../memo/components/unoptimizedtable/index.js | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ee9f7c..38696b2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ The following dependecies are used for this project. Feel free to experiment usi - node v18.14.2 - npm v9.5.0 - > **NOTE:** We will use v18.14.2 for the official production client and server builds but feel free to use other NodeJS versions by setting "engine-strict=false" in the .npmrc file when working on localhost development as needed, but please use v18.14.2 when installing new modules. Do not commit the package.json or package-lock.json files should they change when "engine-strict=false". +4. React Developer Tools (optional) [[link]](https://react.dev/learn/react-developer-tools) + - The React Developer Tools is a web browser extension for debugging React apps. + - It's best to view these demos with the React Profiler, one of the tools available in the React Developer Tools for observing the components re-rendering on state updates. + - Install for [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) + - Install for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/) + - Install for [Edge](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil) ### Core Libraries and Frameworks @@ -31,10 +37,18 @@ The following dependecies are used for this project. Feel free to experiment usi ### Manual Installation and Usage +> It's best to view these demos with the React Profiler, one of the tools available in the React Developer Tools for observing the components re-rendering on state updates. + 1. Navigate to the **/client** directory from the commandline. -2. Create a `.env` file from the `/client/.env.example` file. Copy it's content when working on localhost. +2. Create a `.env` file from the `/client/.env.example` file. Copy its content when working on localhost. 3. Run: `npm run install` 4. Run: `npm run dev` +5. Open the localhost website on `http://localhost:3000` + +### Using the React Profiler + +1. Open the React Profiler in the web browser's developer console. +2. Run the demos and observe the components re-rendering. The Profiler highlights rendered components. ### Localhost Development Using Docker diff --git a/client/src/components/home/index.js b/client/src/components/home/index.js index b6c20cc..4c7e0ef 100644 --- a/client/src/components/home/index.js +++ b/client/src/components/home/index.js @@ -16,6 +16,7 @@ export default function HomeComponent() {

React Hooks Playground

+

Best viewed with React Profiler

{navlinks.map((item, index) => ( diff --git a/client/src/features/memo/components/fulltable/index.js b/client/src/features/memo/components/fulltable/index.js index 8cb07e6..36486ba 100644 --- a/client/src/features/memo/components/fulltable/index.js +++ b/client/src/features/memo/components/fulltable/index.js @@ -42,7 +42,7 @@ function FullTable () { return (
-

Full Table re-rendering

+

Full Table re-rendering (WARNING!) ❌

  • On edit, this table renders the object array data using map(), rendering the full table.
diff --git a/client/src/features/memo/components/memoizedtable/index.js b/client/src/features/memo/components/memoizedtable/index.js index 3a0b1b3..c40b927 100644 --- a/client/src/features/memo/components/memoizedtable/index.js +++ b/client/src/features/memo/components/memoizedtable/index.js @@ -39,7 +39,7 @@ const MemoizedTable = () => { return (
-

Optimized Table row re-rendering

+

Optimized Table row re-rendering ✔️

  • This table renders the object array data using map().
  • On edit, it renders only an "updated" table row using a memoized TableRow component.
  • diff --git a/client/src/features/memo/components/unoptimizedtable/index.js b/client/src/features/memo/components/unoptimizedtable/index.js index 0fdf3d6..b20adbb 100644 --- a/client/src/features/memo/components/unoptimizedtable/index.js +++ b/client/src/features/memo/components/unoptimizedtable/index.js @@ -39,7 +39,7 @@ const UnoptimizedTable = () => { return (
    -

    Table re-rendering all rows (WARNING!)

    +

    Table re-rendering all rows (WARNING!) ❌

    • This table renders the object array data using map().
    • It‘s using a memoized TableRow component but