remix-run / examples

A community-driven repository showcasing examples using Remix 💿
MIT License
1.09k stars 235 forks source link

Vite + Styled Components Example #375

Open seanmcquaid opened 1 year ago

seanmcquaid commented 1 year ago

More of a request but is there any chance we can get an ideal example for using Styled Components with Vite? I tried my hand at it but would love to see something here to go off of.

seanmcquaid commented 1 year ago

Update on this, might have a working example that follows some previous code ya'll added! Going to try and contribute an example for this when I get some time

Amadeo-Frontend commented 1 year ago

this one works for me import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite';

// https://vitejs.dev/config/ export default defineConfig({ plugins: [ react({ include: /.(jsx|tsx)$/, babel: { plugins: ['styled-components'], babelrc: false, configFile: false, }, }), ], });

SilencerWeb commented 9 months ago

@seanmcquaid hey, have you figured out how to make Remix + Vite work with styled-components?

seanmcquaid commented 9 months ago

I did! Here are the links to the entry.server.tsx + root.tsx files, it was really just following what was already in place.

https://github.com/seanmcquaid/remix-unstable-vite-playground/blob/main/app/entry.server.tsx#L38-L55

https://github.com/seanmcquaid/remix-unstable-vite-playground/blob/main/app/root.tsx#L37

wilk commented 3 months ago

This solution worked for me: https://github.com/styled-components/styled-components/issues/4275

HcroakerDev commented 1 month ago

I was having such as the style mismatches and "Identifier 'RefreshRuntime' has already been declared"

I was able to get this working fully with no errors by doing this.

vite.config.ts

Base babel plugin ensures there's no clash in react refresh with remix.

/\.(t|j)sx?$/, for the filter is also key, ensuring it matches typescript.

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      filter: /\.(t|j)sx?$/,
      babelConfig: {
        babelrc: false,
        configFile: false,
        presets: [
          '@babel/preset-typescript'
        ],
        plugins: [
          [
            'babel-plugin-styled-components',
            {
              displayName: true,
              ssr: true,
              fileName: false
            }
          ]
        ],
      }
    }),
    remix(),
    tsconfigPaths(),
  ],
});

root.tsx

import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import { createHead } from "remix-island";

export const Head = createHead(() => (
 <>
  <Meta />
  <Links />
 </>
));

export default function App() {
  return (
    <>
      <Head />
      <Outlet />
      <ScrollRestoration />
      <Scripts />
    </>
  )
}

entry.server.ts

import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { ServerStyleSheet } from "styled-components";
import { Head } from './root';
import { renderHeadToString } from "remix-island";

const ABORT_DELAY = 5_000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  // This is ignored so we can keep it in the template for visibility.  Feel
  // free to delete this parameter in your app if you're not using it!
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  loadContext: AppLoadContext
) {
  return isbot(request.headers.get("user-agent") || "")
    ? handleBotRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      )
    : handleBrowserRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      );
}

function handleBotRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let shellRendered = false;
    const sheet = new ServerStyleSheet();
    const jsx = sheet.collectStyles(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
    );
    const { pipe, abort } = renderToPipeableStream(
      jsx,
      {
        onAllReady() {
          shellRendered = true;
          const styles = sheet.getStyleTags();
          const head = renderHeadToString({ request, remixContext, Head });
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          );

          body.write(
            `<!DOCTYPE html><html lang="en"><head><!--start head-->${head}<!--end head-->${styles}</head><body><div id="root">`
          );
          pipe(body);
          body.end("</div></body></html>");
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          responseStatusCode = 500;
          // Log streaming rendering errors from inside the shell.  Don't log
          // errors encountered during initial shell rendering since they'll
          // reject and get logged in handleDocumentRequest.
          if (shellRendered) {
            console.error(error);
          }
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let shellRendered = false;
    const sheet = new ServerStyleSheet();
    const jsx = sheet.collectStyles(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
    );
    const { pipe, abort } = renderToPipeableStream(
      jsx,
      {
        onShellReady() {
          shellRendered = true;
          const styles = sheet.getStyleTags();
          const head = renderHeadToString({ request, remixContext, Head });
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          );

          body.write(
            `<!DOCTYPE html><html lang="en"><head><!--start head-->${head}<!--end head-->${styles}</head><body><div id="root">`
          );
          pipe(body);
          // This is key instead of body.write for suspense and lazy loading
          body.end("</div></body></html>");
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          responseStatusCode = 500;
          // Log streaming rendering errors from inside the shell.  Don't log
          // errors encountered during initial shell rendering since they'll
          // reject and get logged in handleDocumentRequest.
          if (shellRendered) {
            console.error(error);
          }
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

entry.client.ts

/**
 * By default, Remix will handle hydrating your app on the client for you.
 * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
 * For more information, see https://remix.run/file-conventions/entry.client
 */

import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
  hydrateRoot(
    document.getElementById("root")!,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});