Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
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/*"]
}
}
}
1 change: 1 addition & 0 deletions client/src/components/home/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function HomeComponent() {
<h1 className={inter.className}>
React Hooks Playground
</h1>
<p>Best viewed with React Profiler</p>
</div>

{navlinks.map((item, index) => (
Expand Down
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 (WARNING!) ❌</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)
Loading