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/.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/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/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..36486ba
--- /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 (WARNING!) ❌
+
+ On edit, this table renders the object array data using map(), rendering the full table.
+
+
+
+
+
+ )
+}
+
+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..c40b927
--- /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.
+
+
+
+
+
+ )
+}
+
+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..b20adbb
--- /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.
+
+
+
+
+
+ )
+}
+
+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