diff --git a/js/components/src/livestream-store/hooks.tsx b/js/components/src/livestream-store/hooks.tsx
index 2a7d3bac..81d2c3cd 100644
--- a/js/components/src/livestream-store/hooks.tsx
+++ b/js/components/src/livestream-store/hooks.tsx
@@ -275,7 +275,6 @@ export const useReportChatMessage = () => {
export const usePinChatMessage = () => {
const agent = usePDSAgent();
- const store = getStoreFromContext();
return async (
messageUri: string,
diff --git a/js/web/src/components/stream/livestream-provider.test.tsx b/js/web/src/components/stream/livestream-provider.test.tsx
new file mode 100644
index 00000000..71d3abd5
--- /dev/null
+++ b/js/web/src/components/stream/livestream-provider.test.tsx
@@ -0,0 +1,59 @@
+import type { LivestreamStore } from "@streamplace/core";
+import { act, type ReactNode } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { LivestreamProvider } from "./livestream-provider";
+
+class MockWebSocket {
+ onopen: (() => void) | null = null;
+ onmessage: ((event: MessageEvent) => void) | null = null;
+ onclose: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+
+ close() {}
+}
+
+describe("LivestreamProvider", () => {
+ let container: HTMLDivElement;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ vi.stubGlobal("WebSocket", MockWebSocket);
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+ });
+
+ afterEach(() => {
+ container.remove();
+ vi.unstubAllGlobals();
+ });
+
+ it("creates fresh stream state when the user changes", async () => {
+ const root = createRoot(container);
+ let currentStore: LivestreamStore | undefined;
+ const captureStore = (store: LivestreamStore): ReactNode => {
+ currentStore = store;
+ return null;
+ };
+
+ await act(async () => {
+ root.render(
+ {captureStore},
+ );
+ });
+
+ const aliceStore = currentStore!;
+ act(() => aliceStore.setState({ viewers: 42 }));
+
+ await act(async () => {
+ root.render(
+ {captureStore},
+ );
+ });
+
+ expect(currentStore).not.toBe(aliceStore);
+ expect(currentStore?.getState().viewers).toBeNull();
+
+ await act(async () => root.unmount());
+ });
+});
diff --git a/js/web/src/components/stream/livestream-provider.tsx b/js/web/src/components/stream/livestream-provider.tsx
index d3ea15eb..1e8c537e 100644
--- a/js/web/src/components/stream/livestream-provider.tsx
+++ b/js/web/src/components/stream/livestream-provider.tsx
@@ -4,7 +4,7 @@ import {
makeLivestreamStore,
type LivestreamStore,
} from "@streamplace/core";
-import { useEffect, useRef, useState, type ReactNode } from "react";
+import { useEffect, useMemo, type ReactNode } from "react";
interface LivestreamProviderProps {
/** The DID or handle of the user to connect to. */
@@ -22,19 +22,10 @@ export function LivestreamProvider({
user,
children,
}: LivestreamProviderProps) {
- const store = useRef(null);
- const [initialized, setInitialized] = useState(false);
-
- // Initialize store once
- useEffect(() => {
- store.current = makeLivestreamStore();
- setInitialized(true);
- }, []);
+ const store = useMemo(() => makeLivestreamStore(), [user]);
// WebSocket connection
useEffect(() => {
- if (!store.current) return;
-
const wsUrl = `${getStreamplaceUrl()}/api/websocket/${user}`;
let ws: WebSocket | null = null;
@@ -65,7 +56,7 @@ export function LivestreamProvider({
flushTimer = null;
const batch = messageBuffer;
messageBuffer = [];
- store.current?.setState((s) => handleWebSocketMessages(s, batch));
+ store.setState((s) => handleWebSocketMessages(s, batch));
};
ws.onmessage = (event) => {
@@ -82,13 +73,13 @@ export function LivestreamProvider({
ws.onclose = () => {
if (connectId !== currentConnectId) return;
- store.current?.setState((s) => ({ ...s, websocketConnected: false }));
+ store.setState((s) => ({ ...s, websocketConnected: false }));
scheduleReconnect();
};
ws.onerror = () => {
if (connectId !== currentConnectId) return;
- store.current?.setState((s) => ({ ...s, websocketConnected: false }));
+ store.setState((s) => ({ ...s, websocketConnected: false }));
ws?.close();
scheduleReconnect();
};
@@ -102,15 +93,7 @@ export function LivestreamProvider({
if (reconnectTimeout) clearTimeout(reconnectTimeout);
ws?.close();
};
- }, [user]);
-
- if (!initialized || !store.current) {
- return (
-
- );
- }
+ }, [store, user]);
- return <>{children(store.current)}>;
+ return <>{children(store)}>;
}
diff --git a/js/web/src/lib/store/slices/blueskySlice.ts b/js/web/src/lib/store/slices/blueskySlice.ts
index 6d340875..ad893f66 100644
--- a/js/web/src/lib/store/slices/blueskySlice.ts
+++ b/js/web/src/lib/store/slices/blueskySlice.ts
@@ -362,7 +362,7 @@ export const createBlueskySlice: StateCreator<
},
logout: async () => {
- await storage.removeItem("did");
+ await storage.removeItem(DID_KEY);
await storage.removeItem(STORED_KEY_KEY);
const state = get() as BlueskySlice;
if (!state.oauthSession) {