Sergio Xalambrí

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!