Skip to content

Instantly share code, notes, and snippets.

@jacob-ebey
Created May 8, 2022 22:20
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jacob-ebey/02142c80adba98967a61987be51ade26 to your computer and use it in GitHub Desktop.
Save jacob-ebey/02142c80adba98967a61987be51ade26 to your computer and use it in GitHub Desktop.
Simple Remix SSE Chat Application on new fetch polyfill
import type { LoaderFunction } from "@remix-run/node";
import type { ChatMessageEvent } from "~/events.server";
import { chatEvents } from "~/events.server";
export let loader: LoaderFunction = ({ request }) => {
if (!request.signal) {
throw new Error("No request signal provided by the platform");
}
let encoder = new TextEncoder();
let body = new ReadableStream<Uint8Array>({
start(controller) {
const handleMessage = (event: ChatMessageEvent) => {
const message = event.message;
controller.enqueue(encoder.encode(`id: ${message.id}\n`));
controller.enqueue(encoder.encode("event: message\n"));
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(message)}\n\n`)
);
};
chatEvents.addEventListener("message", handleMessage as any);
request.signal.addEventListener("abort", () => {
chatEvents.removeEventListener("message", handleMessage as any);
controller.close();
});
},
});
let response = new Response(body, {
headers: {
"Content-Type": "text/event-stream",
},
});
return response;
};
import { useEffect, useState, useRef } from "react";
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import cuid from "cuid";
import type { ChatMessage } from "~/events.server";
import { chatEvents, ChatMessageEvent } from "~/events.server";
import { requireUsername } from "~/session.server";
export let action: ActionFunction = async ({ request }) => {
let formData = await request.formData();
let username = formData.get("username");
let message = formData.get("message");
if (typeof username !== "string" || typeof message !== "string") {
return json({ error: "username and message are required" });
}
let chatMessage: ChatMessage = {
id: cuid(),
username,
message,
timestamp: Date.now(),
};
chatEvents.dispatchEvent(new ChatMessageEvent(chatMessage));
return json({});
};
type LoaderData = {
username: string;
};
export let loader: LoaderFunction = async ({ request }) => {
let username = await requireUsername(request);
return json<LoaderData>({ username });
};
export default function Chat() {
let { username } = useLoaderData() as LoaderData;
return (
<main>
<h1>Welcome {username}</h1>
<section>
<Form method="post">
<input type="hidden" name="username" value={username} />
<label>
Message
<br />
<textarea name="message" />
</label>
<br />
<button type="submit">Send</button>
</Form>
</section>
<section>
<ChatMessages />
</section>
</main>
);
}
function ChatMessages() {
let messagesRef = useRef<ChatMessage[]>([]);
let [, rerender] = useState({});
useEffect(() => {
const eventSource = new EventSource("/api/chat");
eventSource.addEventListener("message", (event) => {
if (event.type === "message") {
messagesRef.current.push(JSON.parse(event.data));
rerender({});
}
});
return () => {
eventSource.close();
};
}, []);
return (
<ul>
{messagesRef.current.map((message) => (
<li key={message.id}>
{message.username}: {message.message}
</li>
))}
</ul>
);
}
@cevr
Copy link

cevr commented May 9, 2022

What's the reasoning behind using a ref but forcing a rerender whenever the ref is updated?

@mabdullahsari
Copy link

What's the reasoning behind using a ref but forcing a rerender whenever the ref is updated?

The only thing I can think of is performance.

Creating an empty object will be much faster than copying over an entire array with 500 items and pushing a new message at the end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment