Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(styled-components): fix SSR and error styles #311

Merged
merged 4 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions styled-components/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
/build
/public/build
.env
/app/components
10 changes: 8 additions & 2 deletions styled-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ Open this example on [CodeSandbox](https://codesandbox.com):

## Example

This example shows how to use Styled Components with Remix. Relevant files:
To support server-side rendering without hydration errors, [Styled Components uses a Babel plugin to ensure consistent class names across server and client.](https://styled-components.com/docs/advanced#tooling-setup) In this example the source code for our styled components is in `components/src`. This is compiled with the Babel CLI (to generate JavaScript files) and `tsc` (to generate `.d.ts` files), co-ordinated using [npm-run-all](https://www.npmjs.com/package/npm-run-all). The output from both of these tools is generated in `app/components` which is ignored by Git. The app can then import these components from `~/components/*`.

One notable aspect of Styled Components that we need to manage is the way in which it injects styles into the `head` element. This clashes with the model of using React to render the entire document from the root because Styled Components doesn't expect the `head` element to be remounted. When this happens, any CSS that Styled Components thinks is in the document is actually removed, resulting in a loss of styling. In order to avoid this issue when navigating between error routes and non-error routes, we need to wrap the entire app in a pathless route which in this example we're calling `__boundary`. This allows us to handle top-level errors without re-mounting the entire app.

## Relevant files

- [app/root.tsx](./app/root.tsx) - This is where we render the app and if we're rendering on the server we have placeholder text of `__STYLES__`.
- [app/entry.server.tsx](./app/entry.server.tsx) - This is where we render the app on the server and replace `__STYLES__` with the styles that styled-components collect.
- [app/routes/index.tsx](./app/routes/index.tsx) - Here's where we use the `styled` function to create a styled component.
- [app/routes/__boundary.tsx](./app/routes/__boundary.tsx) - The top-level error boundary for the app to avoid re-mounting the document.
- [app/routes/__boundary/\$.tsx](./app/routes/__boundary/$.tsx) - The top-level splat route that manually throws a 404 response so we can catch it in `__boundary.tsx`.
- [components/src/Box.tsx](./components/src/Box.tsx) - An example `styled` component.

## Related Links

Expand Down
4 changes: 2 additions & 2 deletions styled-components/app/entry.client.tsx
markdalgleish marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";
import { hydrateRoot } from "react-dom/client";

hydrate(<RemixBrowser />, document);
hydrateRoot(document, <RemixBrowser />);
30 changes: 30 additions & 0 deletions styled-components/app/routes/__boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Outlet, useCatch } from "@remix-run/react";

import { Box } from "~/components/Box";

export default function Boundary() {
return <Outlet />;
}

export function CatchBoundary() {
const caught = useCatch();

return (
<Box>
<h1>Catch Boundary</h1>
<p>
{caught.status} {caught.statusText}
</p>
</Box>
);
}

export function ErrorBoundary({ error }: { error: Error }) {
return (
<Box>
<h1>Error Boundary</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
</Box>
);
}
12 changes: 12 additions & 0 deletions styled-components/app/routes/__boundary/$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Any request for this top-level splat route is a 404
export const loader = () => {
throw new Response(null, {
status: 404,
statusText: "Not Found",
});
};

export default function NotFound() {
// This is needed to tell Remix it isn't a resource route
return <></>;
}
markdalgleish marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions styled-components/app/routes/__boundary/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ErrorComponent() {
throw new Error("This route throws on render!");
}
19 changes: 19 additions & 0 deletions styled-components/app/routes/__boundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Link } from "@remix-run/react";

import { Box } from "~/components/Box";

export default function Index() {
return (
<Box>
<h1>Welcome to Remix (With Styled Component)</h1>
<ul>
<li>
<Link to="/error">Error</Link>
</li>
<li>
<Link to="/404">404</Link>
</li>
</ul>
</Box>
);
}
14 changes: 0 additions & 14 deletions styled-components/app/routes/index.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions styled-components/components/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-typescript",
["@babel/preset-react", { "runtime": "automatic" }]
],
"plugins": ["babel-plugin-styled-components"],
"sourceMaps": "inline"
}
6 changes: 6 additions & 0 deletions styled-components/components/src/Box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import styled from "styled-components";

export const Box = styled("div")`
font-family: system-ui, sans-serif;
line-height: 1.4;
`;
12 changes: 12 additions & 0 deletions styled-components/components/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"outDir": "../app/components",

// Babel generates the JS files but we still need to emit .d.ts files
"declaration": true,
"noEmit": false,
"emitDeclarationOnly": true
}
}
28 changes: 22 additions & 6 deletions styled-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,42 @@
"private": true,
"sideEffects": false,
"scripts": {
"build": "remix build",
"dev": "remix dev",
"build": "run-s \"build:*\"",
"build:components": "run-p \"build:components:*\"",
"build:components:src": "npm run generate:components:src",
"build:components:types": "npm run generate:components:types",
"build:remix": "remix build",
"dev": "run-p \"dev:*\"",
"dev:components": "run-p \"dev:components:*\"",
"dev:components:src": "npm run generate:components:src -- --watch",
"dev:components:types": "npm run generate:components:types -- --watch",
"dev:remix": "remix dev",
"generate:components:src": "babel components/src --out-dir app/components --extensions .ts,.js,.tsx,.jsx,.cjs,.mjs",
"generate:components:types": "tsc --project components/tsconfig.json",
"start": "remix-serve build",
"typecheck": "tsc"
},
"dependencies": {
"@remix-run/node": "~1.14.2",
"@remix-run/react": "~1.14.2",
"@remix-run/serve": "~1.14.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^5.3.3"
},
"devDependencies": {
"@babel/cli": "^7.22.10",
"@babel/core": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@remix-run/dev": "~1.14.2",
"@remix-run/eslint-config": "~1.14.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/styled-components": "^5.1.24",
"babel-plugin-styled-components": "^2.1.4",
"eslint": "^8.27.0",
"npm-run-all": "^4.1.5",
"typescript": "^4.8.4"
},
"engines": {
Expand Down
Loading