Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions app/packages/events/src/demo-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Fragment, useState } from "react";
import {
createUseSynchronizedEventState,
createUseEventHandler,
useEventBus,
useEventState,
} from "./hooks";

/**
* Demo showing the difference between useEventHandler (side effects)
* and useEventState (state derivation with tearing prevention).
*/

type CounterEventGroup = {
"counter:updated": { count: number };
"counter:reset": undefined;
};

const useCounterState = createUseSynchronizedEventState<CounterEventGroup>();
const useCounterHandler = createUseEventHandler<CounterEventGroup>();

/**
* Component that dispatches counter events.
*/
const CounterSource = () => {
const [count, setCount] = useState(0);
const bus = useEventBus<CounterEventGroup>();

return (
<>
<button
onClick={() => {
const newCount = count + 1;
setCount(newCount);
bus.dispatch("counter:updated", { count: newCount });
}}
>
Increment and Dispatch
</button>
<button
onClick={() => {
setCount(0);
bus.dispatch("counter:reset");
}}
>
Reset
</button>
</>
);
};

/**
* Component using useEventState - reads state during render.
* Multiple instances will always see the same value (no tearing).
*/
const CounterDisplay = ({ label }: { label: string }) => {
// Read latest count from events - safe from tearing
const latestEvent = useCounterState("counter:updated");
const count = latestEvent?.count ?? 0;

return (
<div>
{label}: {count}
</div>
);
};

/**
* Component using useEventHandler - reacts with side effects.
* This is for side effects, not for displaying state.
*/
const CounterLogger = () => {
// Side effects - use createUseEventHandler
useCounterHandler("counter:updated", (data) => {
console.log("Counter updated to:", data.count);
});

useCounterHandler("counter:reset", () => {
console.log("Counter reset");
});

return <Fragment />;
};

/**
* Component using useEventState directly (without factory).
*/
const DirectCounterDisplay = () => {
const count =
useEventState<CounterEventGroup, "counter:updated">("counter:updated")
?.count ?? 0;

return <div>Direct: {count}</div>;
};

export const StateDemo = () => {
return (
<>
<CounterSource />
<CounterDisplay label="Display 1" />
<CounterDisplay label="Display 2" />
<DirectCounterDisplay />
<CounterLogger />
</>
);
};
19 changes: 18 additions & 1 deletion app/packages/events/src/hooks/createUseEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import { useEventBus } from "./useEventBus";
* Factory function that creates a type-safe event handler hook.
* The returned hook automatically registers/unregisters handlers on mount/unmount.
*
* **When to use this vs createUseSynchronizedEventState:**
* - Use `createUseEventHandler` when you need to **react to events with side effects**
* (e.g., logging, API calls, navigation, updating local component state via setState)
* - Use `createUseSynchronizedEventState` when you need to **read and display** the latest event payload
* in your component's render (prevents tearing in concurrent React rendering)
*
* **Key differences:**
* - `createUseEventHandler`: Registers callbacks that run when events occur. Useful for
* side effects but doesn't provide synchronized state reading across components.
* - `createUseSynchronizedEventState`: Returns the latest event payload synchronously during render.
* Multiple components reading the same event will always see the same synchronized value.
*
* @template T - EventGroup type defining event types and payloads
* @returns A hook function that registers event handlers with automatic cleanup
*
Expand All @@ -18,7 +30,12 @@ import { useEventBus } from "./useEventBus";
* const useDemoEventHandler = createUseEventHandler<DemoEventGroup>();
*
* function Component() {
* useDemoEventHandler("demo:eventA", (data) => console.log(data.id, data.name));
* // Side effects - use createUseEventHandler
* useDemoEventHandler("demo:eventA", (data) => {
* console.log(data.id, data.name);
* // Could also call setState here, but for state derivation,
* // prefer createUseSynchronizedEventState to prevent tearing
* });
* useDemoEventHandler("demo:eventD", () => console.log("Event D received"));
* return <div>...</div>;
* }
Expand Down
Loading
Loading