Use Remix with socket.io
If you want to add real-time capabilities to your Remix app without using an external service, the easiest way is probably with socket.io. Let's see how to do a quick setup.
tl;dr: See the code in https://github.com/sergiodxa/remix-socket.io
First we need to create the Remix app, in the install options choose Express
> npx create-remix@latest
R E M I X
💿 Welcome to Remix! Let's get you set up with a new project.
? Where would you like to create your app? express-socket.io
? Where do you want to deploy? Choose Remix if you're unsure, it's easy to chang
e deployment targets. Express Server
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes
Now, install socket.io and socket.io-client
> npm install socket.io socket.io-client
Let's go to the server/index.js
file created by Remix and add put this:
const path = require("path");
const express = require("express");
const { createServer } = require("http"); // add this require
const { Server } = require("socket.io"); // and also require the socket.io module
const compression = require("compression");
const morgan = require("morgan");
const { createRequestHandler } = require("@remix-run/express");
const MODE = process.env.NODE_ENV;
const BUILD_DIR = path.join(process.cwd(), "server/build");
const app = express();
// create an httpServer from the Express app
const httpServer = createServer(app);
// and create the socket.io server from the httpServer
const io = new Server(httpServer);
// then list to the connection event and get a socket object
io.on("connection", (socket) => {
// here you can do whatever you want with the socket of the client, in this
// example I'm logging the socket.id of the client
console.log(socket.id, "connected");
// and I emit an event to the client called `event` with a simple message
socket.emit("event", "connected!");
// and I start listening for the event `something`
socket.on("something", (data) => {
// log the data together with the socket.id who send it
console.log(socket.id, data);
// and emeit the event again with the message pong
socket.emit("event", "pong");
});
});
app.use(compression());
app.use(express.static("public", { maxAge: "1h" }));
app.use(express.static("public/build", { immutable: true, maxAge: "1y" }));
app.use(morgan("tiny"));
app.all(
"*",
MODE === "production"
? createRequestHandler({ build: require("./build") })
: (req, res, next) => {
purgeRequireCache();
const build = require("./build");
return createRequestHandler({ build, mode: MODE })(req, res, next);
}
);
const port = process.env.PORT || 3000;
// instead of using `app.listen` we use `httpServer.listen`
httpServer.listen(port, () => {
console.log(`Express server listening on port ${port}`);
});
////////////////////////////////////////////////////////////////////////////////
function purgeRequireCache() {
// purge require cache on requests for "server side HMR" this won't let
// you have in-memory objects between requests in development,
// alternatively you can set up nodemon/pm2-dev to restart the server on
// file changes, we prefer the DX of this though, so we've included it
// for you by default
for (const key in require.cache) {
if (key.startsWith(BUILD_DIR)) {
delete require.cache[key];
}
}
}
That will be all for the WebSocket server, you can now emit or listen for more events as your app needs them.
Let's go to the app code, create a ws.client.ts
file somewhere outside routes, and add this:
import io from "socket.io-client";
export function connect() {
return io("http://localhost:3000");
}
This function will returnn a new connection to our WebSocket server.
Then, create some context in another file:
import { createContext } from "react";
import { Socket } from "socket.io-client";
import { DefaultEventsMap } from "socket.io/dist/typed-events";
export let wsContext = createContext<
Socket<DefaultEventsMap, DefaultEventsMap> | undefined
>(undefined);
You can also create a custom provider or hook to read it, for this example we will just export it.
Finally, in the root.tsx
file add a state and effect to connect to the WebSocket server.
let [socket, setSocket] =
useState<Socket<DefaultEventsMap, DefaultEventsMap>>();
useEffect(() => {
let connection = connect();
setSocket(connection);
return () => {
connection.close();
};
}, []);
useEffect(() => {
if (!socket) return;
socket.on("event", (data) => {
console.log(data);
});
}, [socket]);
And render the context provider wrapping the Outlet.
<wsContext.Provider value={socket}>
<Outlet />
</wsContext.Provider>
Now, inside any route you can access this context, get the socket connection object and start emitting or listening to events. For example we could go to routes/index.tsx
and add this effect:
let socket = useContext(wsContext);
useEffect(() => {
if (!socket) return;
socket.on("event", (data) => {
console.log(data);
});
socket.emit("something", "ping");
}, [socket]);
You could also emit events inside an event handler, like this:
let socket = useContext(wsContext);
return (
<div>
<button onClick={() => socket.emit("something", "ping")}>Send ping</button>
</div>
);
And that's it!