remix-run / remix Goto Github PK
View Code? Open in Web Editor NEWBuild Better Websites. Create modern, resilient user experiences with web fundamentals.
Home Page: https://remix.run
License: MIT License
Build Better Websites. Create modern, resilient user experiences with web fundamentals.
Home Page: https://remix.run
License: MIT License
Browser extensions mess w/ <html>
and <body>
sometimes and React hates it, and kicks out errors in the console.
Definitely to document what this is about and how to suppress these (and avoid issues with plugins messing w/ your document):
<html suppressHydrationWarnings>
And maybe add that to the starter boilerplate.
Deploying Remix on Digitial Ocean is super simple and straightforward. The only drawback for hobbyists is the $5 a month price tag, but maybe that's a positive depending on your traffic and bandwidth. (Will see if I can move some things over and get an idea of a price comparison for 2 Remix platforms with comparable usage. App engine vs DO, will circle back in 2-3 months)
Before with DO you would spin up a droplet and manage the server at an admin level. Now (maybe since they acquired nanobox), you can just select their "App" service, select your repo, select $5 plan, etc. hit next next next on the defaults and deploy.
And routing works without any changes in the custom routes in remix.config
Two approaches, I don't know pros and cons yet, but probably have some.
<div>
<Enhancement file="some/file">
<div>Server rendered content</div>
</Enhancement>
<Enhancement
src={`
let el = document.getElementById("thing")
el.style.color = "red"
`}
>
<div id="thing">Server rendered content</div>
</Enhancement>
</div>
import * as enhance from "@remix-run/enhance";
let Thing1 = enhance.file("some/file");
let Thing2 = enhance.src(`
let el = document.getElementById("thing")
el.style.color = "red"
`);
// then render
<div>
<Thing1>
<div>Server rendered content</div>
</Thing1>
<Thing2>
<div id="thing">Server rendered content</div>
</Thing2>
</div>;
You probably want access to the root element almost every time, could be like __filename
.
<Enhance src={`__element.style.color = "red"`}>
<div>Howdy</div>
</Enhance>
You probably want to pass in some stuff you know from the outer world.
let handleClick = () => setStateInOuterWorld();
<Enhance
context={{ onClick: handleClick }}
src={`__element.onclick = __context.onClick`}
>
<div>Howdy</div>
</Enhance>;
Or require an enhancement to be a function, then we can pass stuff in rather than use magic globals:
let handleClick = () => setStateInOuterWorld();
<Enhance
args={[onClick]}
src={`(onClick, element) => {
// element is always last (or first, whatever)
element.onclick = onClick;
`}
>
<div>Howdy</div>
</Enhance>;
In React they export default a component and we pass the props through.
let handleClick = () => setStateInOuterWorld();
<EnhanceReact componentFile="some/file" onClick={handleClick} />;
If children are provided, they are the server placeholder.
For React/Svelte and friends, we can just call renderToString() on them.
// some/file.js
export default function Something({ onClick }) {
return <button onClick={onClick}>I am a button</button>;
}
// outer app
<EnhanceReact file="some/file" />;
// server output
ReactDOMServer.renderToString(<Something {...__context} />);
// <button>I am a button</button>
We thought we wanted it in the beginning, but we're not sure now. With the full-stack nature of Remix it's quite nice to have the full page reload so you don't break your app in production.
For example, if you do this:
let [state, setState] = useState(0)
Fire up your app, start working and then change it to this:
let [state, setState] = useState(localStorage.foo)
With HMR, your app continues to work just fine. If you don't hit "refresh" and try to server render that code, you'll never know you broke your app.
Our current live reload approach ensures that every change runs through a full server render pass so you don't end up with those kinds of surprises.
Additionally, when you're changing loader code, HMR won't update the UI, live reload does.
Finally, usually HMR is more about hiding the slowness of front-end build tools than about maintaining client state on code changes. ESBuild is fast, so it doesn't matter.
All that said, we've got some experimental stuff working from a while back, but it's really not high priority right now because there are some tradeoffs to consider, it's not just an obvious thing to add. Also the live reload experience right now is so good that some people think we already have HMR ๐คซ
This issue is here, and left open, because we haven't decided to do HMR, but we haven't decided not to either.
Steps to repro
Follow the tutorial through until just before "Meta Tags"
https://remix.run/dashboard/docs/tutorial/defining-routes
For me, on app/routes/index.js
I had the following
import React from "react";
import { useRouteData } from "@remix-run/react";
import { Link } from "react-router-dom";
export function meta() {
return {
title: "Remix Starter",
description: "Welcome to remix!",
};
}
export default function Index() {
let data = useRouteData();
return (
<div style={{ textAlign: "center", padding: 20 }}>
<h2>Welcome to Remix!</h2>
<p>
<a href="https://remix.run/docs">Check out the docs</a> to get started.
</p>
<Link to="/gists">Gists</Link>
<p>Message from the loader: {data.message}</p>
</div>
);
}
Click on the link.
See the "Gists" page.
Click on the back button or "cmd + left"
Expected - reloads the "index.js" page
Observed - The url changes but the page does not (tested on mac firefox)((works on mac chrome and safari))
I cloned the starter-express
repo and I changed the file extension of the files inside routes/
from .js
to .tsx
and "normal" routes seem to be working just fine but not the 404 one, the following error is thrown when visiting a route that doesn't exist (in this case the route is /dasdsad
):
Error: Could not resolve entry module (app/routes/404.js).
[2] at error (/Users/gabe/code/learning/remix/my-remix-app/node_modules/rollup/dist/shared/rollup.js:5251:30)
[2] at ModuleLoader.loadEntryModule (/Users/gabe/code/learning/remix/my-remix-app/node_modules/rollup/dist/shared/rollup.js:18410:20)
[2] at async Promise.all (index 1) {
[2] code: 'UNRESOLVED_ENTRY',
[2] watchFiles: [
[2] '/Users/gabe/code/learning/remix/my-remix-app/app/entry-server.tsx'
[2] ]
[2] }
[2] GET /dasdsad 500 313.463 ms - -
The app's structure is the following:
When the user visits "/some/page" we stick the data in the HTML payload.
If they navigate to "/other/page" we do a client side fetch for the data.
If they then click "back", we fetch the data client side, even though we already got it in the initial payload.
With our old data cache we didn't do this.
Service worker caching can help us here. I don't know the APIs but it's goes something like this:
__remixContext
and shove it into a "fetch" cache with the __remix_data?...
url and the headers from the loadersBoom! Now when the user clicks back to the initial html page, it's as though they fetched from the client.
A few small enhancements I'd like to make to the global.fetch
we provide in node:
{ compress: false }
by default. This lets people more easily return the result of a fetch
directly from a data loader with the proper encoding..cache/fetch
directory by default for caching fetch
results. In a long-running server process, this can really speed up the results of a fetch
.The morgan output for the remix server makes it difficult for me to find my own logging output:
I already see all of these requests in my network tab. It's just noise for me. Could we get an option to disable the remix server morgan output?
I know that I could start the remix server in a separate tab or just silence the output, but then that would hide much-needed error messages.
We were discussing in Remix Chat #help how to get parent data into child loaders (remix.run already needs this).
I was talking about how we might have some way to indicate that a child needs the parent data, and then rearrange our async work to load those loaders serially, instead of in parallel like we normally do:
exports.needsParentData = true;
module.exports = ({ params }, parentData) => {
// can use parentData now
}
We could use an exports flag, or check the length of the function args to decide to do serial or parallel.
Then Tima had a really great idea: pass the parent loader promise to the child
module.exports = ({ params}, parentPromise) => {
let parentData = await parentPromise();
// tada!
}
It's really great because now you can do your own data fetching that isn't dependent in parallel, and then await the stuff that is.
module.exports = ({ params }, parent) => {
let [myStuff, parentStuff] = await Promise.all([
fetchMyStuff(params.whatev),
parent
]);
let more = await getMore(parent.thing, myStuff.more);
return more
}
We'd want to unwrap the response for them, so that parentPromise
is awaiting the unwrapped response, not the response itself (otherwise the loader is going to have to become an http consumer, but we've already got code in remix to do that part).
In remix run we fetch the user for the dashboard/
layout to make sure they're authenticated, and then we fetch the user again in dashboard/index
to then be able to fetch their licenses, stripe customer data, etc.
Instead of verifying their session, hitting the google oauth endpoint, and hitting our DB in both routes, we'd only do that work in the parent route and the child would look like this:
module.exports = async (_, parent) => {
let user = await parent
let [licenses, stripeProfile] = await Promise.all([
getLicenses(user),
getStripeProfile(user)
]);
return { licenses, stripeProfile };
}
Also consider redirecting (after we fix it). Let's say you have a master/detail list and you don't want an index route, but instead want to redirect to the first post in the list. So if the user lands at /forum/remix
you want to redirect them to the first post forum/remix/123
.
With our (coming soon) fixed loader redirect handling, this could be done pretty easily in your forum/$forum/index.js
loader.
module.exports = async ({ params }, parent) => {
let parentData = await parent;
return redirect(`/forum/${params.forum}/${parentData[0].id}`);
}
This seem straightforward for the initial HTML page since we call all the matching route loaders together, but the client transitions may prove difficult.
If routes A -> B -> C
match, and we transition to A -> B -> D
in the client, it's unclear to me how we'll get B
's data into D
on the server when we're only calling D
.
.DS_Store is killing me
Browsers now support preloading with <link rel="preload">
and preload headers. The browser will automatically download the resources in idle time and then use the cached resource when the user performs an action that causes the resource to be loaded for real (like clicking a link).
We'd like to take advantage of that.
When Remix navigates to a new page, it loads three categories of resources, and since multiple nested routes can match, there might be multiple resources in each category:
Unlike next/gatsby, Remix doesn't automatically preload the JavaScript modules for every link on the page. We have a strong philosphy that the size of your app will not effect the footprint of any single page. Preloading all of the modules for every link on the page is just not a default behavior we want.
However, we do want to give you the ability to preload whatever resources you want to speed up the transition to that page.
Enter: <link rel=preload>
. You can read more about it here: https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content
The trouble with <link rel=preload>
is that it's pretty useless without intimate knowledge of your build. Remix has exaclty that, so it's positioned wonderfully to expose a simple API for you to preload resources.
Like the route module headers
and meta
functions, you can export a links
function to define the links you'd like to preload (or any other kind of link).
While tempting to use JSX for these links, JSX has limited static analysis (we can't know what's inside a component until it's rendered, we need to know it sooner). Because of this, the API is just objects, but the attributes are the same as <link>
.
You can put any kind of link you want:
export function links() {
return [
{
rel: "preload",
href: "/img/some-image.jpg",
as: "image",
},
];
}
It's more interesting however to preload resources that Remix itself built, like route modules, css, and even data.
export function links() {
return [
// preload just the JS modules
{
route: "routes/pricing",
as: "script",
},
// preload script and styles
{
route: "routes/pricing",
as: "script,style",
},
// preload script, styles, and data
{
route: "routes/pricing",
as: "script,style,fetch",
},
];
}
We use script
, style
, and fetch
because that's what the link preload api uses.
If you use script,style,fetch
, then when the user clicks the link, the transition will be immediate!
export function links() {
return [
// loading script,style is the same
{
route: "routes/projects/$projectId",
as: "script,style",
},
// inject params to use for data
{
route: "routes/projects/$projectId",
as: "script,style,fetch",
params: { projectId: 123 },
},
];
}
Like meta()
, the links function will receive the route data, so if you wanted to preload a bunch of data driven stuff you can:
export function links({ data }) {
return data.map((project) => ({
route: "routes/projects/$projectId",
as: "script,style,fetch",
params: { projectId: project.id },
}));
}
Ofc, be careful with that one, you don't want to make the user download your entire website all at once!
partialManifest
_remix/manifest?
requests.fetch
into _remix/data?
URLs, no need to cache anywhere ourselves though because that's the whole point of <link rel=preload as=fetch>
! The browser does it.head
Right now we merge meta
down the route tree, but with headers
we pass in the parent headers and let the child route do the merge or not.
Maybe headers
is the right approach for everything, let the app decide to merge or not. And if so, we can combine meta
and links
into head, keeping api surface area of route modules a little smaller:
interface RouteHead {
title: string,
meta: { [name: string]: string };
links: HeadLink[]
}
export function head(parent: RouteHead) {
return {
// override title:
title: "My title",
// merge meta, change description
meta: {
...parent.meta,
description: "My description"
},
// concat links:
links: parent.concat([
{ route: "routes/whatev", as: "script" }
])
}
}
Same as babel, open up and let people configure their own postcss, and they can use our presets with a plugin if they want (but won't need to):
{
"plugins": [
"@remix-run/core/postcss",
"other-plugins-here"
]
}
In the readme there is a link to the starter firebase.
| Firebase | remix-run/starter-firebase |
The link is broken.
---
meta:
title: Remix Rocks
description: A solid description of this document.
headers:
og:image: './some-awesome-image.jpg'
---
This is the content for the blog post
That's right it does!!!
import {SlimLayout} from '../../shared/slim-layout'
export default SlimLayout
I would like it if the SlimLayout
component could access the meta
(and everything else in the frontmatter) so it can display the title
, description
, and the headers['og:image']
.
I want to use typeorm as orm, the typeorm setup in done in server.ts But as soon as i import the Entities in the loaders the loaders stop rebuilding. I have created a small repo where the issue can be seen.
[https://github.com/pascal-codetaal/starter-express.git](https://github.com/pascal-codetaal/starter-express.git
https://github.com/pascal-codetaal/starter-express/blob/7a265e8cfa601ddee900614b02935dc6d1b6acf5/loaders/routes/index.ts#L2
if i uncomment this line -> the loader stops building
The doc says
when I did that, I saw:
and
looking at package.json, it seems the command should be npm run dev
:
which was also mentioned in the README.md. So maybe the documentation needs to be fixed?
[UPDATE] : see the more recent comments below for how simple this is today and all routing working great etc out of box
I deployed to google app engine and routing is working great once I started using the routes configuration. Before that was going in circle for routing in production. Was trying to update handlers in app.yaml
and looking for something like that familiar _redirects
step for netlify deployments etc.
Anyhow i have a basic example linked below showing the necessary code changes for google app engine deployment. I can list out more steps re: app engine setup if that helps for an example in the docs. It's pretty easy (compared to other cloud providers), but still some stuff missing that could be listed in a clear 1,2,3 steps format for the docs.
NickFoden/starter-express-app-engine#1
MUCHAS GRACIAS!
Between Remix launching and testing out subscriptions/streaming data in faunaDB this week is 11/10.
What if you could run Remix w/out a server? We can keep the current architecture of Remix, but serve everything out of a service worker instead of our usual server.
The main motivation for this right now would be to run Remix in Cloudflare Workers, which is built around the service worker API. The beauty of being able to deploy to Cloudflare Workers is that you can run your entire app at "the edge."
One nice side effect of Cloudflare choosing to build Workers using the service worker API is that if we can build Remix to run there it should be a relatively small jump to run Remix entirely in the browser in a real service worker. Now instead of running at the edge, you get run run the whole thing locally.
So basically, run all of Remix either at the edge, or straight up in your browser if you don't want/need a traditional server.
We can provide a similar API in @remix-run/service-worker
to what we currently do in @remix-run/express
. Something like:
import { createFetchEventHandler } from "@remix-run/service-worker";
addEventListener(
"fetch",
createFetchEventHandler({
getLoadContext(request: Request) {
// Return whatever you want to be the `context` arg in your loaders...
}
})
);
Think of this file like server.js
for an Express app.
We can build a small server with the addEventListener
API that lets you run this locally, and then we'll use this file as input to generate all the relevant pieces we'll need to run the app in a real service worker.
Since neither Cloudflare Workers nor service workers have support for modules (though it's being discussed for Chrome), we will need to bundle everything into a single file in the build: React, React DOM, React Router, Remix, etc.
In the bundled Remix code we'll need to avoid any node-isms (i.e. process.env.NODE_ENV
, fs
, etc.) and stick to just the service worker API.
In the build, we'll need to include:
ReactDOMServer.renderToString
in our entry-server.js
but I'm guessing react-dom/server
is not designed to be bundled...Was just thinking it'd be cool to have a really low-friction way to create and test a fixture.
Maybe you just want to test a little bit of behavior that requires only two routes. You don't care about the remix.config, or the App layout, or the server entry, etc. etc. Your fixture could just define the files it cares about:
โโโ app
โย ย โโโ routes
โย ย โโโ some-route.js
โโโ loaders
โโโ routes
โโโ some-route.js
Then the test fixture code will fill in the rest with defaults.
However, you don't even need to create files on the file system, just put them in your test!
describe("navigating and loading data on the next page", () => {
let fixture = createFixture({
"app/routes/index.js": `
export default function SomeForm() {
let data = useRouteData()
return (
<div>
<h1>Welcome</h1>
<Link to="/other-page">Other page</Link>
</div>
)
}
`,
"app/routes/other-page.js": `
import { useRouteData } from "@remix-run/react";
export default function SomeForm() {
let data = useRouteData()
return <div>{data.foo}</div>
}
`,
"loaders/routes/other-page.js": `
module.exports = () => ({ foo: 'bar' })
`
});
beforeAll(async () => await fixture.start({ mode: "production" ));
beforeEach(async () => await fixture.reset());
afterAll(async () => await fixture.close());
it("loads data on navigation", async () => {
fixture.visit("/");
expect(fixture.getDocumentResponse().status).toBe(200);
expect(fixture.getPageContent()).toMatchInlineSnapshot();
await fixture.isHydrated();
await fixture.clickLinkTo("/page-2");
expect(fixture.getDataRequests()[0].method).toBe("GET");
expect(fixture.getPageContent()).toMatchInlineSnapshot();
});
});
The thing would be a real app, actually running on a server on localhost:3000
that you can then go visit in the browser and click around to dig in when there are problems, or just as a sanity check.
This would be amazing.
createFixture
just barfs out those files into a tmp/
folder, and then builds and runs it.
We're working on handling this for you, since we know your async dependencies on location changes we can do the scroll restoration at the right time.
For now, here's an incomplete, but mostly good enoughโข solution that works around a bug in history.js that will be fixed soon also. You can put this in the top of your App.js
function useTemporaryScrollManagement() {
let location = useLocation();
let locations = React.useRef();
if (!locations.current) {
locations.current = new Set();
locations.current.add(location.key);
}
React.useEffect(() => {
let wasWeirdHistoryBug = location.key === "default";
if (wasWeirdHistoryBug || locations.current.has(location.key)) return;
locations.current.add(location.key);
requestAnimationFrame(() => {
window.scrollTo(0, 0);
});
}, [location]);
}
//
function App() {
useTemporaryScrollManagement();
// ...
}
First, thank you for all the work you've already done here! I'm really excited about Remix. I primarily use AWS for my infrastructure, and so I wanted to see if I could get this working with Lambdas deployed via CDK.
Thanks in part to some code posted by @shortjared on Discord (who has started this repo https://github.com/shortjared/remix-cdk-starter), I was able to make some pretty good progress. You can see it running here: https://y9beilufg0.execute-api.us-east-1.amazonaws.com/
I posted the code for this at m14t/starter-aws-cdk, which is a mono-repo with 3 projects:
serveStaticFileIfExists
- It would be great if this could stat the files to send the last modified header and calculate an etag, and to take those into account in the responsepublicPath
seems to be a part of the browserBuildDirectory
, which required some funky path/string manipulation to get working -- I'm sure this can be improved@remix-run/express
binding supported streams, but API Gateway doesn't. I'm not sure if its possible for handleRequest
to return a stream here, but if it is, we'll need to consolidate it here.event.headers
and event.multiValueHeaders
. The difference can be seen when someone sends the same header name twice, but with different values. Right now this code is only handling non-repeated headers.remix-run-apigateway
remix-starter-apigateway
Edit: Solved! See comment below.
However there seems to be one major limitation with API Gateway. From https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html:
Path segments can only contain alphanumeric characters, hyphens, periods, commas, and curly braces.
This is an issue for the way we are serving the build/_shared/node_modules
that contain an @
character in their package namespace. This effects the @babel/runtime
and @remis-run/react
dependencies specifically. If you look at the CloudWatch logs, the lambda function is never ever triggered for these requests, API Gateway seems to just drops them.
One thing that would make this setup slightly more complex, but follow best practices, is if we deployed the built assets to S3 and served them via CloudFront. I don't believe that CloudFront has the same path character limitations, which should allow us to work around this issue, and avoid a lambda execution for a static file.
That being said, we might need some support in Remix to be able to serve these files from a different (sub)domain.
Need to investigate further and will add more details, but just clicking around the docs page in firefox doesn't work. It imports the entries but then it doesn't follow the imports from there.
This is pretty low priority right now for us. We'll be creating a "nomodule" bundle with System.js for browser that don't support es modules, when we're in the think of things there we'll also investigate and fix this.
Until then however, if you have any ideas on why firefox won't follow the imports, let us know :)
I am trying to go through the tutorial and at step Your First Loader, after adding the file loaders/routes/gists.js, I am still seeing data being null
, not seeing the gist
Here is what I have: https://github.com/anglee/my-remix-app
I tried to console.log in the gists loader but didn't see the log.
I have confirmed that I do see the gist when I visit https://api.github.com/gists in the browser or cURL in terminal.
using the new JSX transform, we can skip having to import React, currently it (remix) breaks when you remove any react imports
https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html
The express starter has this example for a loader:
import type { DataLoader } from "@remix-run/core";
let loader: DataLoader = async () => {
return {
message: "this is awesome ๐"
};
};
export = loader;
I don't see anyone using the typescript export
like that. I'd much prefer using a default export or maybe a named export called "loader".
I tried the default export and got "TypeError: loader is not a function"
The installation docs say:
Edit .npmrc
In order for yarn/npm on your computer to be able to install Remix, you need to add your license key to the npmrc. You'll find it on the dashboard.
Itโs no clear what the โthe npmrcโ is. Perhaps the docs should change to:
Edit .npmrc
In order for yarn/npm on your computer to be able to install Remix, you need to add your license key to your projectโs
.npmrc
file. Create a.npmrc
in the root of your project if you donโt have one. You'll find your license key in the dashboard.
BTW have you considered having the docs as a repo we can PR to?
Today, to do a mutation, you typically need to create a server api route (express, or otherwise) and wire it all up with a bunch of JavaScript (that most of us do a poor job of cobbling together).
We've had multiple people already trying to use loaders as mutation endpoints, and we ourselves would like to as well.
A strong philosophy in Remix is that you should be able to build websites the "web 1.0 way". When it comes to mutations in HTML, the name of the game is <form method=post>
. The developer experience with HTML forms for mutations is actually really great.
<form method="post" action="/projects/create">
<input type="text" name="title" />
<input type="text" name="description" />
<button type="submit">Create Project</button>
</form>
app.post("/projects/create", async (req, res) => {
let project = await createProject(req.body);
res.redirect(`/projects/${project.id}`);
});
The end. It was so nice. No screwing around serializing the form, no loading states, or useEffect, or dealing with asynchronous APIs like fetch. Just make a form, the browser serializes the form, handles the asynchrony, and the server redirects to the new page. It's even a built-in state machine where the URLs are the states, which drastically simplifies the code you write.
If you go to https://remix.run/newsletter you can see our submit button animates between states and just feels really fun. Stripe checkout has wonderful loading/error states as well.
Additionally, many projects have "wizard" like flows to create things, moving through routes makes a lot of sense but you can't really animate that, or there's state in the UI that you don't want to lose in a browser navigation.
This kind of stuff is typically not possible with web 1.0 style posts.
Remix is positioned to allow for both the superior developer experience of writing mutations as forms (the way HTML and HTTP are designed) while also enabling great user experiences like stripe checkout.
Currently, you can post to a Remix loader if you set up your server that way. For example, in express you can do app.all("*", createRequestHandler())
. However, it pretty much only works if you're submit plain HTML forms w/o client side navigation.
With a new <Form/>
component, we can add support for mutations on route loaders without doing full page reloads so that you can preserve state on the page for improved UX.
This means error handling, pending states, etc. are all normal transition states of Remix, but now for form mutations.
The component:
import * as React from "react";
import { useRouteData, Form } from "@remix-run/react";
export default function NewProject() {
let data = useRouteData();
return (
<>
<h1>New Project</h1>
<Form to="/projects/new" method="post">
<p>
<label>
Name:
<br />
<input name="name" defaultValue={data?.body.name} />
</label>
{data?.errors.name && <Error>{data.errors.name}</Error>}
</p>
<p>
<label>
Description:
<br />
<textarea
cols={20}
name="description"
defaultValue={data?.body.description}
/>
</label>
{data?.errors.description && (
<Error>{data?.errors.description}</Error>
)}
</p>
<p>
<button type="submit">Create</button>
</p>
</Form>
</>
);
}
The loader:
import { redirect } from "@remix-run/loader";
import { createProject } from "../models/project";
export default function async Loader({ context: { req }}) {
let [project, errors] = await createProject(body);
if (project) {
return redirect(`/projects/${project.id}`);
} else if (errors) {
return { errors, body };
}
};
Remix Form
does a fetch(..., { method: "post" })
to the loader, the loader creates a record and redirects, or it returns the errors and body to the component--which happens to be the very same route thats already rendering. Because this is all React, these are all just state changes inside of the components already rendered.
That means you can use isPending
to do loading effects on the button, or even animate in the errors.
But the developer experience is identical to plain HTML forms! In fact, you could even disable JavaScript and it this would all still work. I guess that's what they meant by "progressive enhancement" ๐คช
React Router can't really ship with a <Form>
component because it doesn't know about a server. Since Remix is a server, we can complete the browser navigation picture with client side routing. <Link>
for get, <Form>
for mutations ๐
<Form>
could navigate(to, { state: { method, body }})
to trigger @remix-run/react.fetchData
to use the proper method.
navigate(to, { method, body })
or navigate.post(to, body)
, just some ideas.put
and delete
, can use a <input type="hidden" name="__method" value="put">
since HTML forms don't support it.Might be a few other things, but in general I think that's all we need.
If we wanted to take this a step further, each deployment wrapper (express, aws, etc.) could add a body parser, and do method branching on the loader, so loaders could end up having an interface of exporting get
, post
, put
, ad delete
methods:
exports.get = ({ params, context, url }) => {}
exports.post = ({ params, body, context, url }) => {}
exports.put = ({ params, body, context, url }) => {}
exports.delete = ({ params, context, url }) => {}
Can wait on this controller stuff though, all we need right now is <Form>
and navigate(to, { state: { method, body }})
OS/Browser: Android Chrome 86
When logging in with GitHub a popup (new tab) opens up. This is a firebase auth handler that redirects me to GitHub. After signing in on GitHub it redirects me back to the auth handler that shows The requested action is invalid.
. No error messages in the console of the popup.
On the desktop it works well - same flow with a popup.
Not sure what's going on here, as I haven't seen this error working with Firebase before.
On the link https://remix.run/dashboard/docs/tutorial/installation I wasn't able to copy-paste the link when using firefox.
I dug into the css and found I still wasn't able to select after manually setting the following on the <code>
bracket:
user-select: all;
-moz-user-select: all;
Not sure what's the right solution, but worth calling out as a roadblock for people wanting to copy/paste/clone/remix-run!
When it comes to error handling in Remix there are two categories of errors to deal with:
These are errors that prevent a production build from even happening (remix build
would fail). During development we can display messages in the terminal, the browser, or when possible, both places.
The following are development errors that may occur and where the user is notified:
remix build
won't work either)
Production errors don't prevent the app from being built and could happen in a production environment. For example, trying to read from an object member that doesn't exist in a loader.
There are two types of errors that can occur:
For rendering errors we use componentDidCatch
, but errors thrown during data loading happen in useEffect
, and those errors aren't caught by componentDidCatch
. For these, we'll render the 500.js file.
For more detailed user feedback than a top level error boundary for rendering and 500.js for uncaught data errors, it's up to the application to catch those errors and deal with them locally.
componentDidCatch
For loader errors, it would look like this:
export function loader() {
try {
let stuff = db.get("stuff");
return json(stuff);
} catch (error) {
return json({ error: error.message }, { status: 500 });
}
}
The route continues to render as usual, there's just a 500 status code on the fetch (or document request) and the component needs to account for it:
function SomeRoute() {
let data = useRouteData();
if (data.error) {
return <div>Oops, there was an error: {data.error.message}</div>;
}
}
Again, if apps don't catch any errors, loader errors will cause the 500.js file to render, and render errors will propagate up to the <ErrorBoundary>
in the starter template's App.tsx
file.
This work is not scheduled to be completed right now, but I think it would be a great direction to go.
We can take this a step farther and do better than rendering the 500.js
page with uncaught errors, as well as eliminate the 500.js
and 404.js
pages altogether.
In Suspense, errors in data loading are part of the component lifecycle, so error handling of component errors or data loading errors are all handled in componentDidCatch
. This gives you fine-grained control over uncaught errors.
During browser transition fetch requests, we can emulate this behavior by catching data errors, continuing the render, and throwing the error during the component render phase. We essentially "move" the error from useEffect
to render
.
Unfortunately, componentDidCatch
doesn't work on the server (because the features is stateful by nature). However, we can emulate it on our server rendered document requests by catching the error in a loader, then rerendering again with the error on context. Because we know the route whose loader threw an error, we can also have that route render the "error branch" of code by checking for the error on context and rendering the error branch UI--emulating what happens in the browser when an error is thrown and componentDidCatch
rerenders the error branch. To reiterate: when an error is thrown in the browser, componentDidCatch
catches it and rerenders the error branch. On the server, we try to render once, collect errors, render again and routes with errors take their error branch when they have an error on context--it's like an initial prop.
With this setup, instead of rendering the "500 page" on uncaught errors anywhere, the error will propagate up the React tree to the nearest componentDidCatch
. If none exist between the error and the root of the app, then you still have the same "500 page" behavior.
We could also turn "no match" into an error that also makes it way up to the nearest componentDidCatch
and the error page can handle both cases, completely eliminating our weird 404.js
and 500.js
files.
function App() {
let data = useGlobalData();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<Meta />
<Styles />
</head>
<body className="m-4">
<div data-test-id="content">
<Routes />
</div>
<Scripts />
</body>
</html>
);
}
function ErrorPage({ error }) {
// Would bikeshed this error API ofc
if (error.status === 404) {
return <div>Page not found</div>;
}
return <div>There was an error: {error.message}</div>;
}
export default function Root() {
<ErrorBoundary component={ErrorPage}>
<App />
</ErrorBoundary>;
}
This way all uncaught errors are handled. If routes wanted to handle uncaught errors more specifically, they could export an Error
component. Consider this route:
export async function loader() {
// OOPS! Database is offline and threw an error, and the app didn't catch it!
let project = await db.read("...");
return project;
}
export default function Project() {
let data = useRouteData();
return <div>{/* ... */}</div>;
}
Because the error was not handled by the loader, the top level <ErrorBoundary>
from earlier would handle this error. But if the route exports an Error
component, the error will be handled here, in this route, and any routes above w/o errors will continue to render normally, providing a much better experience for the user than changing the entire UI.
export async function loader() {
// OOPS! Database is offline and threw an error, and the app didn't catch it!
let project = await db.read("...");
return json(project);
}
export function Error({ error }) {
return <div>Oops! There was an error: {error.message}</div>;
}
export default function Project() {
let data = useRouteData();
return <div>{/* ... */}</div>;
}
We could even take the error path for any non-200 response, like 404s. 404s are probably going to need to be handled on every route that fetches data (rather than handled generically like any other non-200 status), so this would be really nice for application code.
export async function loader() {
let project = await db.read("...");
return project === null ? json("", { status: 404 }) : json(project);
}
export function Error({ error }) {
if (error.statusCode === 404) {
return <div>That project wasn't found</div>;
}
return <div>Oops! There was an error: {error.message}</div>;
}
export default function Project() {
let data = useRouteData();
return <div>{/* ... */}</div>;
}
The first bit of work needs to be done regardless. If developers remove the top level <ErrorBoundary>
from the App.tsx
file, then the normal 500.js code needs to be run. In the future that 500.js file will just be internal to Remix, but the code paths are likely to be identical. So it makes sense to get the first batch of work done to make error handling much better than it is today, and then make it even better in the future.
Do they work?
Maybe I don't want Firebase imported until they actually click the button.
let firebase = null;
function SigninButton() {
return (
<button
onClick={async () => {
if (!firebase) {
firebase = await import("../firebase");
}
firebase.signInWithPopup();
}}
>
Log in
</button>
);
}
Do we want our own React.lazy that code splits and server renders?
import { lazy } from "@remix-run/react";
let HugeModal = lazy(
() => import("../HugeModal"),
() => <LoadingSkeleton />
);
<HugeModal/>
Add a changelog page within the documentation to allow users to view updates within the dashboard.
Currently, release information is being added to the #releases channel in discord.
Suggested Tags: enhancement
| remix.run
In the readme there is a link to the starter firebase.
| Firebase | remix-run/starter-firebase |
The link is broken.
Open up and use an application babel config, apps will just need to make sure to include our preset:
{
"presets": [
"@remix-run/core/babel",
]
}
Steps to reproduce:
It seems that when navigating back, its reseting the scroll position before rerendering. That looks to be the cause of the jank. In addition its likely saving the position for the page at the "post jank" position which is why when going forward you end up at the wrong position.
Excellent product so far! Having the .npmrc config right there is super handy!
I imagine there will be times when a license key gets exposed, so having a button on the dashboard page to revoke and create a new license key would be really handy.
Don't have a good name for this API yet, but it's a lot like "partial hydration" or even "progressive enhancement".
A lot of the time your page is mostly static markup with just a few dynamic bits (a carousel, some tabs, a dropdown, a fancy form, a chart, etc.) At the moment, you have to bring in a lot of JavaScript and HTML on the initial page load to have a highly dynamic page.
Enhancements will allow you to render mostly static markup, and then just "enhance" a few roots.
Two approaches, I don't know pros and cons yet, but probably have some. In either case, Remix will statically analyze your code to find enhancements at build time and create bundled entry points out of them. At runtime, we know the asset manifest and can load that bundle with the same mechanisms that we load route bundles: /_remix/manifest
. When an enhancement is on the page, its JavaScript will be loaded and executed.
Point to a file in your app and Remix will bundle it up:
<Enhancement file="some/file/relative/to/app">
<div>Server rendered content</div>
</Enhancement>
Or use inline JavaScript:
<Enhancement
src={`
let el = document.getElementById("thing")
el.style.color = "red"
`}
>
<div id="thing">Server rendered content</div>
</Enhancement>
</div>
Alternative approach is a static function, unsure of pros-cons right now.
import * as enhance from "@remix-run/enhance";
let Thing1 = enhance.file("some/file");
let Thing2 = enhance.src(`
let el = document.getElementById("thing")
el.style.color = "red"
`);
// then render
<div>
<Thing1>
<div>Server rendered content</div>
</Thing1>
<Thing2>
<div id="thing">Server rendered content</div>
</Thing2>
</div>;
You probably want access to the root element almost every time, could be like __filename
.
<Enhance src={`__element.style.color = "red"`}>
<div>Howdy</div>
</Enhance>
You probably want to pass in some stuff you know from the outer world.
let handleClick = () => setStateInOuterWorld();
<Enhance
context={{ onClick: handleClick }}
src={`__element.onclick = __context.onClick`}
>
<div>Howdy</div>
</Enhance>;
Or require an enhancement to be a function, then we can pass stuff in rather than use magic globals:
let handleClick = () => setStateInOuterWorld();
<Enhance
args={[onClick]}
src={`(onClick, element) => {
// element is always last (or first, whatever)
element.onclick = onClick;
`}
>
<div>Howdy</div>
</Enhance>;
While the previous examples would just run some really basic JavaScript, if you want a React root, we could have a special component for that that would automatically create a react root, render into it, and pass props down from the server app to it.
let handleClick = () => setStateInOuterWorld();
<EnhanceReact componentFile="some/file" onClick={handleClick} />;
If children are provided, they are the server placeholder.
For EnhanceReact
, we can call renderToString() on it.
// some/file.js
export default function Something({ onClick }) {
return <button onClick={onClick}>I am a button</button>;
}
// outer app
<EnhanceReact file="some/file" />;
// server output
ReactDOMServer.renderToString(<Something {...__context} />);
// <button>I am a button</button>
While React will be rendering the server generated HTML, there's no reason we couldn't create something like a Svelte enhancer:
<div>
<h1>All just static markup</h1>
<SvelteEnhancer filename="some/svelte/component" />
</div>
There's a good chance some of your marketing pages would do well to not load all of React onto the page!
To get these scripts on the page we'll likely need a separate <Scripts/>
component, like:
<Scripts/>
<EnhancerScripts />
Because some pages don't need the React app scripts, but will need enhancers.
Hi there!
It was quite a surprise to open my-remix-app
and find out that some files are using the.tsx
extension and some others are not. I haven't found anything in the documentation related to Typescript.
From the dashboard, when I click:
Billing -> (redirect to stripe) -> Return to Remix
I am sent to this URL: http://us-central1-remix-run.cloudfunctions.net/dashboard
Chrome complains that this may be a phishing attempt:
If I click "ignore" it takes me to a google accounts oauth login flow which I have not completed since I assume it is unnecessary.
Edit: out of curiosity I did authorize the google accounts connection, and I get:
Not sure, but this would make moving routes around a lot easier.
Instead of:
// in some route
import Foo from "../../../../components/Foo";
You could have
import Foo from "~/components/Foo";
Then when you're moving routes around in directories you don't have to goof around with imports. I think Next does this and so that would make migrations a bit easier too.
I dunno.
Redirects work as intended on the initial HTML render, but not on a client side transition.
Typically we want to co-locate our tests to the files they're testing. Sometimes it's useful to test an individual component. Unfortunately, every file in app/routes
is considered a route, including app/routes/__tests__/dashboard.tsx
for example.
Curious what you think about allowing us to configure some ignorable files in that directory. Something like:
exports.routeIgnore = ['**/__tests__/**', '**/__mocks__/**']
With tests, you have some freedom to put them outside the routes
directory, but __mocks__
does have to be located next to the file it's mocking, so even if you disagree that putting tests in the routes
directory is necessary, we may still need this for __mocks__
.
Thoughts?
I'm getting intermittent errors when navigating to a route that has a data loader.
the /data? routes will return 304's but with no response
And in the console I see:
react-25ebc74b.js:36 Uncaught (in promise) SyntaxError: Unexpected end of JSON input
at fetchData (react-25ebc74b.js:36)
at async Promise.all (:3000/index 0)
at :3000/async http:/localhost:8002/_shared/node_modules/@remix-run/react-25ebc74b.js:271
fetchData @ react-25ebc74b.js:36
async function (async)
(anonymous) @ react-25ebc74b.js:264
(anonymous) @ react-25ebc74b.js:288
commitHookEffectListMount @ react-dom-9e9846e5.js:19805
commitPassiveHookEffects @ react-dom-9e9846e5.js:19843
callCallback @ react-dom-9e9846e5.js:262
invokeGuardedCallbackDev @ react-dom-9e9846e5.js:311
invokeGuardedCallback @ react-dom-9e9846e5.js:366
flushPassiveEffectsImpl @ react-dom-9e9846e5.js:22927
unstable_runWithPriority @ scheduler-ad10a139.js:657
runWithPriority$1 @ react-dom-9e9846e5.js:11113
flushPassiveEffects @ react-dom-9e9846e5.js:22894
(anonymous) @ react-dom-9e9846e5.js:22773
workLoop @ scheduler-ad10a139.js:601
flushWork @ scheduler-ad10a139.js:559
performWorkUntilDeadline @ scheduler-ad10a139.js:171
Mentioned it here and looks like another user had the same problem.
firefox latest! (also navigating only works with js disabled ๐ฌ )
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.