How toUse 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:
server/index.js 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:
app/ws.client.ts 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:
app/ws.context.ts 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.
app/root.tsx 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.
app/root.tsx <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:
app/routes/something.tsx 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:
app/routes/something.tsx let socket = useContext(wsContext); return ( <div> <button onClick={() => socket.emit("something", "ping")}>Send ping</button> </div> );
And that's it!