Skip to content

Commit 1aa0ae1

Browse files
authored
Merge pull request #27 from weaponsforge/dev
v1.0.5
2 parents c4cb781 + e92accc commit 1aa0ae1

File tree

14 files changed

+448
-2
lines changed

14 files changed

+448
-2
lines changed

README.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ The following dependecies are used for this project. Feel free to experiment usi
1313
- node v18.14.2
1414
- npm v9.5.0
1515
- > **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".
16+
4. React Developer Tools (optional) [[link]](https://react.dev/learn/react-developer-tools)
17+
- The React Developer Tools is a web browser extension for debugging React apps.
18+
- 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.
19+
- Install for [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
20+
- Install for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)
21+
- Install for [Edge](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil)
1622

1723
### Core Libraries and Frameworks
1824

@@ -31,10 +37,18 @@ The following dependecies are used for this project. Feel free to experiment usi
3137

3238
### Manual Installation and Usage
3339

40+
> 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.
41+
3442
1. Navigate to the **/client** directory from the commandline.
35-
2. Create a `.env` file from the `/client/.env.example` file. Copy it's content when working on localhost.
43+
2. Create a `.env` file from the `/client/.env.example` file. Copy its content when working on localhost.
3644
3. Run: `npm run install`
3745
4. Run: `npm run dev`
46+
5. Open the localhost website on `http://localhost:3000`
47+
48+
### Using the React Profiler
49+
50+
1. Open the React Profiler in the web browser's developer console.
51+
2. Run the demos and observe the components re-rendering. The Profiler highlights rendered components.
3852

3953
### Localhost Development Using Docker
4054

client/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ yarn-error.log*
3232
.vercel
3333

3434
.env
35+
36+
*.zip
37+
*.rar

client/jsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"@/*": ["./src/*"],
55
"@/hooks/*": ["./src/lib/hooks/*"],
66
"@/public/*": ["public/*"],
7-
"@/store/*": ["./src/lib/store/*"]
7+
"@/store/*": ["./src/lib/store/*"],
8+
"@/data/*": ["./src/lib/data/*"]
89
}
910
}
1011
}

client/src/components/home/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function HomeComponent() {
1616
<h1 className={inter.className}>
1717
React Hooks Playground
1818
</h1>
19+
<p>Best viewed with React Profiler</p>
1920
</div>
2021

2122
{navlinks.map((item, index) => (

client/src/components/home/items.json

+4
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@
1818
{
1919
"name": "useReducer",
2020
"link": "/usereducer"
21+
},
22+
{
23+
"name": "memo",
24+
"link": "/memo"
2125
}
2226
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useEffect, useState } from 'react'
2+
3+
import characters from '@/data/characters.json'
4+
import styles from '../../tablesdemo/TablesDemo.module.css'
5+
6+
function FullTable () {
7+
const [data, setData] = useState(characters)
8+
const [headers, setHeaders] = useState([])
9+
10+
useEffect(() => {
11+
if (headers.length === 0) {
12+
setHeaders(Object.keys(data[0]).map((key, id) => ({
13+
id,
14+
name: key
15+
})))
16+
}
17+
}, [headers, data])
18+
19+
const handleCellUpdate = (rowId, field, newValue) => {
20+
if (data[rowId][field] === parseFloat(newValue)) return
21+
22+
setData(prev =>
23+
prev.map(row =>
24+
row.id === rowId ? { ...row, [field]: parseFloat(newValue) } : row
25+
)
26+
)
27+
}
28+
29+
const handleKeyDown = (e, rowIndex, colIndex) => {
30+
// Move cursor to next row
31+
const { keyCode } = e
32+
if (keyCode !== 13) return
33+
34+
const nextIndex = (rowIndex === data.length - 1)
35+
? 0 : rowIndex + 1
36+
37+
const nextId = `cell-${nextIndex}-${colIndex}`
38+
const next = document.getElementById(nextId)
39+
next?.focus()
40+
}
41+
42+
return (
43+
<div className={styles.container}>
44+
<div className={styles.subDescription}>
45+
<h3>Full Table re-rendering (WARNING!) ❌</h3>
46+
<ul>
47+
<li>On edit, this table renders the object array data using map(), rendering the full table.</li>
48+
</ul>
49+
</div>
50+
51+
<form autoComplete='off'>
52+
<table>
53+
<thead>
54+
<tr>
55+
{headers?.map(column => (
56+
<th key={column.id}>
57+
{column.name}
58+
</th>
59+
))}
60+
</tr>
61+
</thead>
62+
<tbody>
63+
{data.map((player, rowIndex) => (
64+
<tr key={player.id}>
65+
{headers?.map((field, colIndex) => (
66+
<td key={field.id}>
67+
{(['id', 'name'].includes(field))
68+
? player[field]
69+
: <input
70+
id={`cell-${rowIndex}-${colIndex}`}
71+
type="text"
72+
defaultValue={player[field.name]}
73+
onFocus={(e) => e.target.select()}
74+
onBlur={(e) => {
75+
const { value } = e.target
76+
handleCellUpdate(rowIndex, field.name, value)
77+
}}
78+
onKeyDown={(e) => handleKeyDown(e, rowIndex, colIndex)}
79+
/>
80+
}
81+
</td>
82+
))}
83+
</tr>
84+
))}
85+
</tbody>
86+
</table>
87+
</form>
88+
</div>
89+
)
90+
}
91+
92+
export default FullTable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useEffect, useState, useCallback } from 'react'
2+
3+
import TableRow from '../tablerow'
4+
5+
import characters from '@/data/characters.json'
6+
import styles from '../../tablesdemo/TablesDemo.module.css'
7+
8+
const MemoizedTable = () => {
9+
const [players, setData] = useState(characters)
10+
const [headers, setHeaders] = useState([])
11+
12+
useEffect(() => {
13+
if (headers.length === 0) {
14+
setHeaders(Object.keys(players[0]).map((key, id) => ({
15+
id,
16+
name: key
17+
})))
18+
}
19+
}, [headers, players])
20+
21+
// Wrap anonymous functions in useCallback() to prevent re-renders on child components.
22+
// Sometimes, local state may need to be included in its dependency array
23+
const handleCellUpdate = useCallback((rowId, field, newValue) => {
24+
setData((prevData) => {
25+
const tempData = [...prevData]
26+
const updatedValue = parseFloat(newValue)
27+
28+
// Update only the affected field in an object element
29+
if (tempData[rowId][field] !== updatedValue) {
30+
tempData[rowId] = {
31+
...tempData[rowId], [field]: updatedValue
32+
}
33+
}
34+
35+
return tempData
36+
})
37+
}, [])
38+
39+
return (
40+
<div className={styles.container}>
41+
<div className={styles.subDescription}>
42+
<h3 style={{ color: 'green' }}>Optimized Table row re-rendering ✔️</h3>
43+
<ul>
44+
<li>This table renders the object array data using map().</li>
45+
<li>On edit, it renders only an &quot;updated&quot; table row using a memoized TableRow component.</li>
46+
</ul>
47+
</div>
48+
49+
<form autoComplete='off'>
50+
<table>
51+
<thead>
52+
<tr>
53+
{headers?.map(column => (
54+
<th key={column.id}>
55+
{column.name}
56+
</th>
57+
))}
58+
</tr>
59+
</thead>
60+
<tbody>
61+
{players?.map((player, rowIndex) => (
62+
<TableRow
63+
key={player.id}
64+
rowIndex={rowIndex}
65+
nextIndex={(rowIndex === players.length - 1)
66+
? 0 : rowIndex + 1
67+
}
68+
headers={headers}
69+
player={player}
70+
onEdit={handleCellUpdate}
71+
/>
72+
))}
73+
</tbody>
74+
</table>
75+
</form>
76+
</div>
77+
)
78+
}
79+
80+
export default MemoizedTable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { memo } from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
/**
5+
* Notes:
6+
*
7+
* This table row component re-renders only if its props changes.
8+
* props.onEdit, an anonymous function, while looking constant also re-renders
9+
* so be sure to wrap it in a useCallback hook in it's parent component.
10+
*
11+
* Try:
12+
* Observe this component's re-renders on the React Profile with and without the memo() hook.
13+
*/
14+
function TableRow ({
15+
nextIndex,
16+
rowIndex,
17+
headers,
18+
player,
19+
onEdit,
20+
key,
21+
idPrefix = 'm'
22+
}) {
23+
console.log(`--Re-rendering for update: ${player.name}`)
24+
25+
const handlePlayerEdit = (e, rowIndex, field) => {
26+
const { value } = e.target
27+
if (player[field] === parseFloat(value)) return
28+
29+
onEdit(rowIndex, field, value)
30+
}
31+
32+
const handleKeyDown = (e, fieldIndex) => {
33+
// Move cursor to next row
34+
const { keyCode } = e
35+
if (keyCode !== 13) return
36+
37+
const nextId = `${idPrefix}-cell-${nextIndex}-${fieldIndex}`
38+
const next = document.getElementById(nextId)
39+
next?.focus()
40+
}
41+
42+
return (
43+
<tr key={key}>
44+
{headers?.map((field, fieldIndex) => (
45+
<td key={player.id}>
46+
{(['id', 'name'].includes(field.name))
47+
? player[field.name]
48+
: <input
49+
id={`${idPrefix}-cell-${rowIndex}-${fieldIndex}`}
50+
type='text'
51+
defaultValue={player[field.name]}
52+
onBlur={(e) => handlePlayerEdit(e, rowIndex, field.name)}
53+
onFocus={(e) => e.target.select()}
54+
onKeyDown={(e) => handleKeyDown(e, fieldIndex)}
55+
/>
56+
}
57+
</td>
58+
))}
59+
</tr>
60+
)
61+
}
62+
63+
TableRow.propTypes = {
64+
nextIndex: PropTypes.number,
65+
rowIndex: PropTypes.number,
66+
headers: PropTypes.object,
67+
player: PropTypes.arrayOf(PropTypes.object),
68+
onEdit: PropTypes.func,
69+
key: PropTypes.number,
70+
idPrefix: PropTypes.string
71+
}
72+
73+
export default memo(TableRow)

0 commit comments

Comments
 (0)