mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
Let memoized file rows skip unrelated re-renders
FileRow and FileCard were wrapped in memo, but every handler passed to them was re-created on each render, so a keystroke or a selection re-rendered every row of a large directory. useStableHandler gives the rows fixed handler identities that still run the latest code.
This commit is contained in:
1 parent
540b86c445
commit
e49c627640
3 files changed
+51
-11
No files matched your search
@@ -18,6 +18,7 @@ import Login from '/src/pages/Login.jsx';
|
||||
import SSOCallback from '/src/pages/SSOCallback.jsx';
|
||||
import {useOverflowItems} from '/src/hooks/useOverflowItems.js';
|
||||
import useFocusTrap from '/src/hooks/ai/useFocusTrap.js';
|
||||
import {useStableHandler} from '/src/hooks/useStableHandler.js';
|
||||
window.calls=[]; window.users=[]; window.reads=0;
|
||||
window.onUser=user=>window.users.push(user);
|
||||
window.testApi={
|
||||
@@ -38,9 +39,16 @@ function Focus(){
|
||||
useFocusTrap(box,{active,restoreFocusRef:restore});
|
||||
return <><button id="first" ref={first}>first</button><button id="second" ref={second}>second</button><div ref={box}><button>inside</button></div></>
|
||||
}
|
||||
window.rowRenders=0;window.seen=[];
|
||||
const Row=React.memo(function Row({onPick}){window.rowRenders++;return <button id="row" onClick={onPick}>row</button>});
|
||||
function Stable(){
|
||||
const [count,setCount]=useState(0);window.inc=()=>setCount(n=>n+1);
|
||||
const onPick=useStableHandler(()=>window.seen.push(count));
|
||||
return <><output>{count}</output><Row onPick={onPick}/></>
|
||||
}
|
||||
function App(){
|
||||
const [,setTick]=useState(0);window.bump=()=>setTick(n=>n+1);
|
||||
if(mode==='overflow')return <Overflow/>;if(mode==='focus')return <Focus/>;
|
||||
if(mode==='overflow')return <Overflow/>;if(mode==='stable')return <Stable/>;if(mode==='focus')return <Focus/>;
|
||||
const entry=mode==='sso'?'/login/callback/test?code=code1&state=state1':{pathname:'/login',state:{requires2FA:true,tempToken:'challenge1'}};
|
||||
return <MemoryRouter initialEntries={[entry]}><Routes><Route path="/login" element={<Login/>}/><Route path="/login/callback/:provider" element={<SSOCallback/>}/><Route path="*" element={<div id="done">done</div>}/></Routes></MemoryRouter>
|
||||
}
|
||||
@@ -104,5 +112,13 @@ try {
|
||||
await page.goto(origin+'/hook-check?mode=focus');await page.locator('#first').waitFor();await page.waitForTimeout(100);
|
||||
await page.evaluate(()=>window.deactivate());await page.waitForFunction(()=>document.activeElement.id==='first');
|
||||
console.log('PASS focus restores activation target after ref changes');
|
||||
await page.goto(origin+'/hook-check?mode=stable');await page.locator('#row').waitFor();
|
||||
const mounted=await page.evaluate(()=>window.rowRenders);
|
||||
for(let i=0;i<3;i++)await page.evaluate(()=>window.inc());
|
||||
await page.waitForFunction(()=>document.querySelector('output').textContent==='3');
|
||||
assert.equal(await page.evaluate(()=>window.rowRenders),mounted,'a parent update re-rendered the memoized row');
|
||||
await page.locator('#row').click();
|
||||
assert.deepEqual(await page.evaluate(()=>window.seen),[3]);
|
||||
console.log('PASS a stable handler keeps memoized rows still and still sees the latest state');
|
||||
assert.deepEqual(errors,[]);
|
||||
} finally {await browser?.close();await server.close()}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useCallback, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
// A callback whose identity never changes but that always runs the latest
|
||||
// `fn`. Pass it to memoized children (list rows) so a parent re-render does
|
||||
// not re-render every row just because its handlers were re-created.
|
||||
// Not for use during render: the latest `fn` is installed after commit.
|
||||
export function useStableHandler(fn) {
|
||||
const ref = useRef(fn);
|
||||
useLayoutEffect(() => {
|
||||
ref.current = fn;
|
||||
});
|
||||
return useCallback((...args) => ref.current(...args), []);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTopbarActions } from '@/hooks/useTopbarActions';
|
||||
import { errorReason } from '@/utils/errorMessage';
|
||||
import usePersistentState from '@/hooks/usePersistentState';
|
||||
import { useStableHandler } from '@/hooks/useStableHandler';
|
||||
import { formatTime } from '@/utils/intl';
|
||||
|
||||
// Demo rail shortcuts (Quick access) — one-click jumps to the paths people
|
||||
@@ -865,6 +866,16 @@ function FileManager() {
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, entry });
|
||||
};
|
||||
|
||||
// Stable identities for the memoized rows/cards: a fresh handler on every
|
||||
// render made each keystroke or selection re-render every row.
|
||||
const rowOpen = useStableHandler(handleOpen);
|
||||
const rowToggleSelect = useStableHandler(handleToggleSelect);
|
||||
const rowContext = useStableHandler(openContextMenu);
|
||||
const rowDownload = useStableHandler((entry) => fileApi.download(entry));
|
||||
const rowRename = useStableHandler(openRenameModal);
|
||||
const rowPermissions = useStableHandler(openPermissionsModal);
|
||||
const rowDelete = useStableHandler((entry) => handleDelete(entry));
|
||||
|
||||
const copyPathToClipboard = (path) => copyText(path);
|
||||
|
||||
const downloadSelected = () => {
|
||||
@@ -1309,9 +1320,9 @@ function FileManager() {
|
||||
entry={entry}
|
||||
selected={selectedPaths.has(entry.path)}
|
||||
selectMode={selectMode}
|
||||
onOpen={handleOpen}
|
||||
onToggleSelect={handleToggleSelect}
|
||||
onContext={openContextMenu}
|
||||
onOpen={rowOpen}
|
||||
onToggleSelect={rowToggleSelect}
|
||||
onContext={rowContext}
|
||||
isS3={isS3}
|
||||
/>
|
||||
))}
|
||||
@@ -1348,13 +1359,13 @@ function FileManager() {
|
||||
entry={entry}
|
||||
selected={selectedPaths.has(entry.path)}
|
||||
selectMode={selectMode}
|
||||
onOpen={handleOpen}
|
||||
onToggleSelect={handleToggleSelect}
|
||||
onContext={openContextMenu}
|
||||
onDownload={(e) => fileApi.download(e)}
|
||||
onRename={openRenameModal}
|
||||
onPermissions={openPermissionsModal}
|
||||
onDelete={(e) => handleDelete(e)}
|
||||
onOpen={rowOpen}
|
||||
onToggleSelect={rowToggleSelect}
|
||||
onContext={rowContext}
|
||||
onDownload={rowDownload}
|
||||
onRename={rowRename}
|
||||
onPermissions={rowPermissions}
|
||||
onDelete={rowDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user