Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 19 & concurrent rendering #656

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions package.json
Expand Up @@ -56,7 +56,7 @@
"is-in-ci": "^0.1.0",
"lodash": "^4.17.21",
"patch-console": "^2.0.0",
"react-reconciler": "^0.29.0",
"react-reconciler": "0.31.0-canary-4c12339ce-20240408",
"scheduler": "^0.23.0",
"signal-exit": "^3.0.7",
"slice-ansi": "^7.1.0",
Expand All @@ -74,10 +74,10 @@
"@types/benchmark": "^2.1.2",
"@types/lodash": "^4.14.202",
"@types/ms": "^0.7.31",
"@types/node": "^20.10.4",
"@types/react": "^18.2.43",
"@types/react-reconciler": "^0.28.2",
"@types/scheduler": "^0.16.8",
"@types/node": "*",
"@types/react": "^18.2.75",
"@types/react-reconciler": "^0.28.8",
"@types/scheduler": "^0.16.2",
"@types/signal-exit": "^3.0.0",
"@types/sinon": "^10.0.20",
"@types/stack-utils": "^2.0.2",
Expand All @@ -93,7 +93,7 @@
"node-pty": "^1.0.0",
"p-queue": "^8.0.0",
"prettier": "^3.1.1",
"react": "^18.0.0",
"react": "19.0.0-canary-4c12339ce-20240408",
"react-devtools-core": "^5.0.0",
"sinon": "^17.0.0",
"strip-ansi": "^7.1.0",
Expand Down
5 changes: 1 addition & 4 deletions src/hooks/use-input.ts
Expand Up @@ -182,10 +182,7 @@ const useInput = (inputHandler: Handler, options: Options = {}) => {

// If app is not supposed to exit on Ctrl+C, then let input listener handle it
if (!(input === 'c' && key.ctrl) || !internal_exitOnCtrlC) {
// @ts-expect-error TypeScript types for `batchedUpdates` require an argument, but React's codebase doesn't provide it and it works without it as exepected.
reconciler.batchedUpdates(() => {
inputHandler(input, key);
});
inputHandler(input, key);
}
};

Expand Down
28 changes: 20 additions & 8 deletions src/ink.tsx
Expand Up @@ -77,17 +77,29 @@ export default class Ink {
// so that it's rerendered every time, not just new static parts, like in non-debug mode
this.fullStaticOutput = '';

const rootTag = 1;
const hydrationCallbacks = null;
const isStrictMode = false;
const concurrentUpdatesByDefaultOverride = false;
const identifierPrefix = 'id';
// TODO: Change error handling to noop. I've added this to more easily develop the reconciler
const onUncaughtError = console.error;
const onCaughtError = console.error;
const onRecoverableError = console.error;
const transitionCallbacks = null;

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.container = reconciler.createContainer(
this.rootNode,
// Legacy mode
0,
null,
false,
null,
'id',
() => {},
null,
rootTag,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
);

// Unmount when process exits
Expand Down
32 changes: 29 additions & 3 deletions src/reconciler.ts
@@ -1,6 +1,9 @@
import process from 'node:process';
import createReconciler from 'react-reconciler';
import {DefaultEventPriority} from 'react-reconciler/constants.js';
import {
DefaultEventPriority,
NoEventPriority,
} from 'react-reconciler/constants.js';
import Yoga, {type Node as YogaNode} from 'yoga-wasm-web/auto';
import {
createTextNode,
Expand Down Expand Up @@ -92,6 +95,8 @@ type UpdatePayload = {
style: Styles | undefined;
};

let currentUpdatePriority = NoEventPriority;

export default createReconciler<
ElementNames,
Props,
Expand Down Expand Up @@ -231,7 +236,11 @@ export default createReconciler<
scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout,
noTimeout: -1,
getCurrentEventPriority: () => DefaultEventPriority,
setCurrentUpdatePriority: newPriority => {
currentUpdatePriority = newPriority;
},
getCurrentUpdatePriority: () => currentUpdatePriority,
resolveUpdatePriority: () => currentUpdatePriority || DefaultEventPriority,
beforeActiveInstanceBlur() {},
afterActiveInstanceBlur() {},
detachDeletedInstance() {},
Expand Down Expand Up @@ -262,7 +271,8 @@ export default createReconciler<

return {props, style};
},
commitUpdate(node, {props, style}) {
commitUpdate(node, payload, type, oldProps, newProps) {
const {props, style} = newProps;
if (props) {
for (const [key, value] of Object.entries(props)) {
if (key === 'style') {
Expand Down Expand Up @@ -295,4 +305,20 @@ export default createReconciler<
removeChildNode(node, removeNode);
cleanupYogaNode(removeNode.yogaNode);
},
maySuspendCommit() {
// TODO: May return false here if we are confident that we don't need to suspend
return true;
},
startSuspendingCommit() {},
waitForCommitToBeReady() {
return null;
},
preloadInstance() {
// Return true to indicate it's already loaded
return true;
},
suspendInstance() {},
shouldAttemptEagerTransition() {
return false;
},
});