Skip to content

Commit

Permalink
Feat (core): Merge sync state updates in a single re-render event (Fix
Browse files Browse the repository at this point in the history
  • Loading branch information
SBoudrias committed Jun 23, 2023
1 parent f47c859 commit 165d6d8
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 10 deletions.
61 changes: 61 additions & 0 deletions packages/core/core.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,67 @@ describe('createPrompt()', () => {
await expect(answer).resolves.toEqual('up');
});

it('useKeypress: only re-render once on state changes', async () => {
const renderSpy = vi.fn();
const Prompt = (config: { message: string }, done: (value: string) => void) => {
renderSpy();

const [value, setValue] = useState('value');
const [key, setKey] = useState('key');

useKeypress((key: KeypressEvent) => {
if (isEnterKey(key)) {
done(value);
} else {
setValue('foo');
setKey('bar');
}
});

return `${config.message} ${key}:${value}`;
};

const prompt = createPrompt(Prompt);
const { answer, events } = await render(prompt, { message: 'Question' });
expect(renderSpy).toHaveBeenCalledTimes(1);

events.keypress('down');
expect(renderSpy).toHaveBeenCalledTimes(2);

events.keypress('enter');
await expect(answer).resolves.toEqual('foo');
});

it('useEffect: only re-render once on state changes', async () => {
const renderSpy = vi.fn();
const Prompt = (config: { message: string }, done: (value: string) => void) => {
renderSpy();

const [value, setValue] = useState('value');
const [key, setKey] = useState('key');

useEffect(() => {
setValue('foo');
setKey('bar');
}, []);

useKeypress((key: KeypressEvent) => {
if (isEnterKey(key)) {
done(value);
}
});

return `${config.message} ${key}:${value}`;
};

const prompt = createPrompt(Prompt);
const { answer, events } = await render(prompt, { message: 'Question' });
expect(renderSpy).toHaveBeenCalledTimes(2);

events.keypress('enter');
await expect(answer).resolves.toEqual('foo');
});

it('allow cancelling the prompt', async () => {
const Prompt = (config: { message: string }, done: (value: string) => void) => {
useKeypress((key: KeypressEvent) => {
Expand Down
47 changes: 37 additions & 10 deletions packages/core/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ const cleanupHook = (index: number) => {
}
};

function mergeStateUpdates<T extends (...args: any) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
const wrapped = (...args: any): ReturnType<T> => {
let shouldUpdate = false;
const oldHandleChange = handleChange;
handleChange = () => {
shouldUpdate = true;
};

const returnValue = fn(...args);

if (shouldUpdate) {
oldHandleChange();
}
handleChange = oldHandleChange;

return returnValue;
};

return wrapped;
}

export function useState<Value>(
defaultValue: NotFunction<Value> | (() => Value)
): [Value, (newValue: Value) => void] {
Expand Down Expand Up @@ -92,14 +115,18 @@ export function useEffect(
hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
}
if (hasChanged) {
hooksEffect.push(() => {
cleanupHook(_idx);
const cleanFn = cb(rl);
if (cleanFn != null && typeof cleanFn !== 'function') {
throw new Error('useEffect return value must be a cleanup function or nothing.');
}
hooksCleanup[_idx] = cleanFn;
});
hooksEffect.push(
mergeStateUpdates(() => {
cleanupHook(_idx);
const cleanFn = cb(rl);
if (cleanFn != null && typeof cleanFn !== 'function') {
throw new Error(
'useEffect return value must be a cleanup function or nothing.'
);
}
hooksCleanup[_idx] = cleanFn;
})
);
}
hooks[_idx] = depArray;
}
Expand All @@ -114,9 +141,9 @@ export function useKeypress(
}

useEffect(() => {
const handler = (_input: string, event: KeypressEvent) => {
const handler = mergeStateUpdates((_input: string, event: KeypressEvent) => {
userHandler(event, rl);
};
});

rl.input.on('keypress', handler);
return () => {
Expand Down

0 comments on commit 165d6d8

Please sign in to comment.