Skip to content
Open
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
13 changes: 13 additions & 0 deletions examples/conditional-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Conditional hooks

This example makes ordinary React client hooks independent of call order without changing Bippy's package code. A small Vite transform inserts `useConditionalHooks()` at the start of each function component. That bootstrap uses Bippy's exported `useFiber()` API and one stable React reducer before the example virtualizes later hook calls by Fiber and callsite.

```bash
pnpm --filter @bippy/example-conditional-hooks dev
pnpm --filter @bippy/example-conditional-hooks build
pnpm --filter @bippy/example-conditional-hooks preview
```

The dev server exercises React's development build. The build and preview commands exercise its production build.

This is an experimental client-only example. It emulates hook state and effect lifecycles outside React's native positional hook list and is not intended for server rendering.
13 changes: 13 additions & 0 deletions examples/conditional-hooks/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Conditional hooks with Bippy</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions examples/conditional-hooks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@bippy/example-conditional-hooks",
"private": true,
"type": "module",
"scripts": {
"build": "vp build",
"dev": "vp dev",
"preview": "vp preview",
"test": "vp test --run",
"test:production": "NODE_ENV=production vp test --run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"bippy": "workspace:*",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@babel/core": "^7.28.0",
"@babel/types": "^7.28.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.0",
"happy-dom": "^15.11.7",
"typescript": "^5.9.0",
"vite-plus": "latest"
}
}
61 changes: 61 additions & 0 deletions examples/conditional-hooks/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useMemo, useRef, useState } from "react";

interface CounterProps {
isEnabled: boolean;
}

const ConditionalCounter = ({ isEnabled }: CounterProps) => {
if (!isEnabled) {
return <p className="empty">The component returned before calling any demo hooks.</p>;
}

const [count, setCount] = useState(0);
const renderCount = useRef(0);
renderCount.current++;

const doubledCount = useMemo(() => count * 2, [count]);

useEffect(() => {
document.title = `Conditional count: ${count}`;
return () => {
document.title = "Conditional hooks with Bippy";
};
}, [count]);

return (
<div className="counter">
<button onClick={() => setCount((value) => value + 1)}>Count: {count}</button>
<span>Doubled: {doubledCount}</span>
<span>Renders: {renderCount.current}</span>
</div>
);
};

const App = () => {
const [isEnabled, setIsEnabled] = useState(false);

return (
<main>
<p className="eyebrow">Bippy experiment</p>
<h1>Conditional hooks without changing component code</h1>
<p className="lede">
The Vite plugin initializes each function component through Bippy. The hooks below can be
skipped and re-entered in both development and production builds.
</p>
<label className="toggle">
<input
checked={isEnabled}
onChange={(event) => setIsEnabled(event.currentTarget.checked)}
type="checkbox"
/>
Call the conditional hooks
</label>
<ConditionalCounter isEnabled={isEnabled} />
<p className="note">
Increment the counter, disable the branch, then enable it again. Its state is preserved.
</p>
</main>
);
};

export default App;
73 changes: 73 additions & 0 deletions examples/conditional-hooks/src/conditional-hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import "bippy/install-hook-only";

import { getRDTHook } from "bippy";
import { createElement, useEffect, useState } from "react";
import { expect, it } from "vite-plus/test";

import { installConditionalHooks } from "./conditional-hooks";

interface ConditionalCounterProps {
effectEvents: string[];
isEnabled: boolean;
}

const ConditionalCounter = ({ effectEvents, isEnabled }: ConditionalCounterProps) => {
if (!isEnabled) return <span>disabled</span>;

const [count, setCount] = useState(0);
useEffect(() => {
effectEvents.push(`start:${count}`);
return () => {
effectEvents.push(`stop:${count}`);
};
}, [count, effectEvents]);

return <button onClick={() => setCount((value) => value + 1)}>{count}</button>;
};

installConditionalHooks();
getRDTHook().checkDCE = () => {};

const waitForEffects = (): Promise<void> =>
new Promise((resolve) => {
setTimeout(resolve, 0);
});

it("preserves conditional state and cleans up conditional effects", async () => {
const { createRoot } = await import("react-dom/client");
const { flushSync } = await import("react-dom");

const effectEvents: string[] = [];
const container = document.createElement("div");
const root = createRoot(container);

flushSync(() => {
root.render(createElement(ConditionalCounter, { effectEvents, isEnabled: false }));
});

expect(container.textContent).toBe("disabled");

flushSync(() => {
root.render(createElement(ConditionalCounter, { effectEvents, isEnabled: true }));
});
expect(container.querySelector("button")?.textContent).toBe("0");

flushSync(() => {
container.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.querySelector("button")?.textContent).toBe("1");

flushSync(() => {
root.render(createElement(ConditionalCounter, { effectEvents, isEnabled: false }));
});
await waitForEffects();
expect(container.textContent).toBe("disabled");
expect(effectEvents).toContain("stop:1");

flushSync(() => {
root.render(createElement(ConditionalCounter, { effectEvents, isEnabled: true }));
});
expect(container.querySelector("button")?.textContent).toBe("1");

flushSync(() => root.unmount());
});
Loading
Loading