DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Rasmus Piho
Rasmus Piho

Posted on

React Cosmos with Remix

I recently discovered https://remix.run. Whoa, haven't been this excited over a framework for a long time. Naturally I already switched some pet projects over and the development flow has been very straightforward.

One topic of interest of mine is how to speed up and isolate developing of components that I use in my apps.

Enter https://reactcosmos.org. It's an alternative to Storybook and to me looks a bit cleaner with smaller amount of boilerplate needed to get running out of the box. It runs a separate dev server with a clean UI displaying all of your component fixtures.

So I tried to pair my Remix app with React Cosmos so I would not have to run a separate Webpack bundler in order to get the fixtures update as I work on the components. I got it working by following the Snowpack example from React-Cosmos Github space.

The first draft of this tutorial I posted also under an open issue about supporting StoryBook in Remix Github issues page: https://github.com/remix-run/remix/issues/214

Create cosmos.tsx under app/routes:

export default function Cosmos() {
    return null;
}
Enter fullscreen mode Exit fullscreen mode

Add cosmos.config.json in your project root directory:

{
    "staticPath": "public",
    "watchDirs": ["app"],
    "userDepsFilePath": "app/cosmos.userdeps.js",
    "experimentalRendererUrl": "http://localhost:3000/cosmos"
}
Enter fullscreen mode Exit fullscreen mode

Change your entry.client.tsx accordingly:

import { mountDomRenderer } from "react-cosmos/dom";
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";

import { decorators, fixtures, rendererConfig } from "./cosmos.userdeps.js";

if (
    process.env.NODE_ENV === "development" &&
    window.location.pathname.includes("cosmos")
) {
    mountDomRenderer({ rendererConfig, decorators, fixtures });
} else {
    hydrate(<RemixBrowser />, document);
}
Enter fullscreen mode Exit fullscreen mode

You might need to add // @ts-nocheck in the beginning of this file if using Typescript (you should), because TS will likely complain about not finding ./cosmos.userdeps.js which will be generated automatically by React Cosmos on each run. Oh and you should add that file to your .gitignore file as well!

Of course, add react-cosmos as a dev dependency:

$ npm i -D react-cosmos
Enter fullscreen mode Exit fullscreen mode

Add the following in your package.json scripts section:

    "cosmos": "cosmos",
    "cosmos:export": "cosmos-export"
Enter fullscreen mode Exit fullscreen mode

Start the remix dev server:

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

Start cosmos server in another terminal window:

$ npm run cosmos
Enter fullscreen mode Exit fullscreen mode

Now, although this works, I noticed in the developer console, that my remix app started polling itself and getting 404 periodically because of a socket.io route was not configured.

This started to bother me so I investigated further and found a cleaner solution:

In app/routes/cosmos.tsx make the following changes:

import { useCallback, useState } from "react";
import { useEffect } from "react";
import { HeadersFunction } from "remix";
import { decorators, fixtures, rendererConfig } from "~/cosmos.userdeps.js";

const shouldLoadCosmos =
    typeof window !== "undefined" && process.env.NODE_ENV === "development";

export const headers: HeadersFunction = () => {
    return { "Access-Control-Allow-Origin": "*" };
};

export default function Cosmos() {
    const [cosmosLoaded, setCosmosLoaded] = useState(false);
    const loadRenderer = useCallback(async () => {
        const { mountDomRenderer } = (await import("react-cosmos/dom")).default;
        mountDomRenderer({ decorators, fixtures, rendererConfig });
    }, []);

    useEffect(() => {
        if (shouldLoadCosmos && !cosmosLoaded) {
            loadRenderer();
            setCosmosLoaded(true);
        }
    }, []);
    return null;
}
Enter fullscreen mode Exit fullscreen mode

And restore your entry.client.ts file to it's original state:

import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";

hydrate(<RemixBrowser />, document);
Enter fullscreen mode Exit fullscreen mode

So there you have it - Remix dev server running on localhost:3000 and React Cosmos server running on localhost:5000.

Notice the headers function export in app/routes/cosmos.tsx - I added that so there would be no annoying cors errors in your dev console, although it seemed to work perfectly fine without it as well.

Top comments (2)

Collapse
 
tkovis profile image
tkovis

I tried to make a fixture of a component that used remix/Link but got errors. I got it rendered by wrapping the default export in a router with a router and passing some props to avoid errors:

# List.fixture.jsx
import React from "react";
import { Router } from "react-router";
import { List } from "./List";

export default (
  <Router location={{}} navigator={{ createHref: () => {} }}>
    <List id="list-id" items={[{ title: "title", to: "to" }]} />
  </Router>
);


Enter fullscreen mode Exit fullscreen mode
Collapse
 
tkovis profile image
tkovis

Also if you have problems with prod builds failing due to importing non-existent cosmos.userdeps.js, you can get around it by ignoring the route in prod:

const prodIgnores = process.env.NODE_ENV === "production" ? ["cosmos.tsx"] : [];
/**
 * @type {import('@remix-run/dev').AppConfig}
 */
module.exports = {
  cacheDirectory: "./node_modules/.cache/remix",
  ignoredRouteFiles: [
    ".*",
    "**/*.css",
    "**/*.test.{js,jsx,ts,tsx}",
    ...prodIgnores,
  ],
};
Enter fullscreen mode Exit fullscreen mode

An Animated Guide to Node.js Event Loop

>> Check out this classic DEV post <<