Skip to content

Commit 4e2b620

Browse files
committed
feat: Activity component polyfill
1 parent 4bf698b commit 4e2b620

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

src/components/Activity.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as React from "react";
2+
import { Show } from "./Show";
3+
4+
function ActivityPolyfill({ children, mode }: React.PropsWithChildren<{ mode: "visible" | "hidden" }>) {
5+
return <Show when={mode === "visible"} mode="visibility">{children}</Show>
6+
}
7+
8+
/**
9+
* **`Activity`**: Polyfill of the React 19 `<Activity>` component for earlier versions.
10+
*
11+
* Keeps the subtree always mounted in the DOM, preserving state and context
12+
* across visibility changes. When `mode="hidden"` the subtree is hidden via
13+
* `display: none`; when `mode="visible"` it is rendered normally.
14+
*
15+
* ### Differences from the native React 19 `<Activity>`
16+
*
17+
* - **Effects are not deactivated**: the native `<Activity>` tears down effects
18+
* (e.g. `useEffect` cleanup) when hidden and replays them on re-show, enabling
19+
* things like pausing network requests or timers. This polyfill keeps the
20+
* subtree fully active — effects continue running while hidden.
21+
* - **No Suspense integration**: the native `<Activity>` can pre-render hidden
22+
* subtrees in the background and reveal them instantly. This polyfill has no
23+
* such capability.
24+
* - **No transition support**: the native `<Activity>` integrates with
25+
* `startTransition` to defer hiding/showing. This polyfill applies changes
26+
* synchronously.
27+
*
28+
* @see [React 19 Activity docs](https://react.dev/reference/react/Activity)
29+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/Activity)
30+
* @param {{ mode: "visible" | "hidden", children?: React.ReactNode }} props
31+
* @returns {JSX.Element} element
32+
*/
33+
export const Activity: React.ComponentType<{ mode: "visible" | "hidden"; children?: React.ReactNode }> =(React as any).Activity ?? ActivityPolyfill;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useReducer, useState } from "react";
2+
import { Activity } from "../../..";
3+
4+
const Panel = ({ label, color }: { label: string; color: string }) => {
5+
const [count, inc] = useReducer(c => c + 1, 0);
6+
return (
7+
<div style={{ padding: 24, textAlign: "center" }}>
8+
<p style={{ margin: "0 0 12px", fontWeight: "bold", color }}>{label}</p>
9+
<p style={{ margin: "0 0 8px", fontSize: 13, color: "#666" }}>
10+
This panel stays mounted even when inactive.
11+
</p>
12+
<strong style={{ fontSize: 28 }}>{count}</strong>
13+
<br />
14+
<button style={{ marginTop: 8 }} onClick={inc}>+1</button>
15+
</div>
16+
);
17+
};
18+
19+
const TABS = [
20+
{ id: 0, label: "Panel A", color: "#e53935" },
21+
{ id: 1, label: "Panel B", color: "#1e88e5" },
22+
{ id: 2, label: "Panel C", color: "#43a047" },
23+
];
24+
25+
/**
26+
The component demonstrates _Activity_ as a polyfill of the experimental React `<Activity>` component.
27+
- Three tabs represent three independent panels, each rendered inside an `<Activity>` with `mode="hidden"`.
28+
- Switching tabs sets the active panel — inactive panels receive `mode="hidden"`.
29+
- Each panel contains a counter. Because `mode="hidden"` keeps the subtree mounted, counters preserve their values across tab switches — unlike conditional rendering which would reset them.
30+
- A note under the tabs confirms that all three panels remain in the DOM at all times.
31+
*/
32+
export default function ActivityDemo() {
33+
const [active, setActive] = useState(0);
34+
35+
return (
36+
<div style={{ maxWidth: 400, margin: "0 auto" }}>
37+
<div style={{ display: "flex", gap: 4, marginBottom: 0 }}>
38+
{TABS.map(t => (
39+
<button
40+
key={t.id}
41+
onClick={() => setActive(t.id)}
42+
style={{
43+
flex: 1,
44+
fontWeight: active === t.id ? "bold" : "normal",
45+
borderBottom: active === t.id ? `2px solid ${t.color}` : "2px solid transparent",
46+
}}
47+
>
48+
{t.label}
49+
</button>
50+
))}
51+
</div>
52+
<div style={{ border: "1px solid #e0e0e0", borderRadius: "0 0 8px 8px", overflow: "hidden" }}>
53+
{TABS.map(t => (
54+
<Activity key={t.id} mode={active === t.id ? "visible" : "hidden"}>
55+
<Panel label={t.label} color={t.color} />
56+
</Activity>
57+
))}
58+
</div>
59+
<p style={{ fontSize: 12, color: "#999", textAlign: "center", marginTop: 8 }}>
60+
All 3 panels are always in the DOM — counters never reset on tab switch.
61+
</p>
62+
</div>
63+
);
64+
}

0 commit comments

Comments
 (0)