Skip to content

Commit

Permalink
feat: test using memo
Browse files Browse the repository at this point in the history
  • Loading branch information
weaponsforge committed Jan 8, 2024
1 parent d0a0cad commit f14c8bb
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 1 deletion.
3 changes: 3 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ yarn-error.log*
.vercel

.env

*.zip
*.rar
3 changes: 2 additions & 1 deletion client/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"@/*": ["./src/*"],
"@/hooks/*": ["./src/lib/hooks/*"],
"@/public/*": ["public/*"],
"@/store/*": ["./src/lib/store/*"]
"@/store/*": ["./src/lib/store/*"],
"@/data/*": ["./src/lib/data/*"]
}
}
}
4 changes: 4 additions & 0 deletions client/src/components/home/items.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@
{
"name": "useReducer",
"link": "/usereducer"
},
{
"name": "memo",
"link": "/memo"
}
]
92 changes: 92 additions & 0 deletions client/src/features/memo/components/fulltable/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.container}>
<div className={styles.subDescription}>
<h3>Full Table re-rendering</h3>
<ul>
<li>On edit, this table renders the object array data using map(), rendering the full table.</li>
</ul>
</div>

<form autoComplete='off'>
<table>
<thead>
<tr>
{headers?.map(column => (
<th key={column.id}>
{column.name}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((player, rowIndex) => (
<tr key={player.id}>
{headers?.map((field, colIndex) => (
<td key={field.id}>
{(['id', 'name'].includes(field))
? player[field]
: <input
id={`cell-${rowIndex}-${colIndex}`}
type="text"
defaultValue={player[field.name]}
onFocus={(e) => e.target.select()}
onBlur={(e) => {
const { value } = e.target
handleCellUpdate(rowIndex, field.name, value)
}}
onKeyDown={(e) => handleKeyDown(e, rowIndex, colIndex)}
/>
}
</td>
))}
</tr>
))}
</tbody>
</table>
</form>
</div>
)
}

export default FullTable
80 changes: 80 additions & 0 deletions client/src/features/memo/components/memoizedtable/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.container}>
<div className={styles.subDescription}>
<h3 style={{ color: 'green' }}>Optimized Table row re-rendering</h3>
<ul>
<li>This table renders the object array data using map().</li>
<li>On edit, it renders only an &quot;updated&quot; table row using a memoized TableRow component.</li>
</ul>
</div>

<form autoComplete='off'>
<table>
<thead>
<tr>
{headers?.map(column => (
<th key={column.id}>
{column.name}
</th>
))}
</tr>
</thead>
<tbody>
{players?.map((player, rowIndex) => (
<TableRow
key={player.id}
rowIndex={rowIndex}
nextIndex={(rowIndex === players.length - 1)
? 0 : rowIndex + 1
}
headers={headers}
player={player}
onEdit={handleCellUpdate}
/>
))}
</tbody>
</table>
</form>
</div>
)
}

export default MemoizedTable
73 changes: 73 additions & 0 deletions client/src/features/memo/components/tablerow/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<tr key={key}>
{headers?.map((field, fieldIndex) => (
<td key={player.id}>
{(['id', 'name'].includes(field.name))
? player[field.name]
: <input
id={`${idPrefix}-cell-${rowIndex}-${fieldIndex}`}
type='text'
defaultValue={player[field.name]}
onBlur={(e) => handlePlayerEdit(e, rowIndex, field.name)}
onFocus={(e) => e.target.select()}
onKeyDown={(e) => handleKeyDown(e, fieldIndex)}
/>
}
</td>
))}
</tr>
)
}

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)
83 changes: 83 additions & 0 deletions client/src/features/memo/components/unoptimizedtable/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.container}>
<div className={styles.subDescription}>
<h3 style={{ color: 'red' }}>Table re-rendering all rows (WARNING!)</h3>
<ul>
<li>This table renders the object array data using map().</li>
<li>It&lsquo;s using a memoized TableRow component but</li>
<li>it&apos;s handleCellUpdate() method, an anonymous function is not memoized using useCallback().</li>
<li>On edit, it renders all table rows.</li>
</ul>
</div>

<form autoComplete='off'>
<table>
<thead>
<tr>
{headers?.map(column => (
<th key={column.id}>
{column.name}
</th>
))}
</tr>
</thead>
<tbody>
{players?.map((player, rowIndex) => (
<TableRow
key={player.id}
rowIndex={rowIndex}
nextIndex={(rowIndex === players.length - 1)
? 0 : rowIndex + 1
}
headers={headers}
player={player}
onEdit={handleCellUpdate}
idPrefix='u'
/>
))}
</tbody>
</table>
</form>
</div>
)
}

export default UnoptimizedTable
5 changes: 5 additions & 0 deletions client/src/features/memo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import TablesDemo from './tablesdemo'

export {
TablesDemo
}
Loading

0 comments on commit f14c8bb

Please sign in to comment.