Coder Social home page Coder Social logo

remix-run / remix Goto Github PK

View Code? Open in Web Editor NEW
28.2K 233.0 2.4K 45.85 MB

Build Better Websites. Create modern, resilient user experiences with web fundamentals.

Home Page: https://remix.run

License: MIT License

JavaScript 6.80% TypeScript 92.32% Arc 0.01% Shell 0.12% CSS 0.76%

remix's Introduction

Welcome to Remix!

We are happy you're here!

Remix is a full stack web framework that lets you focus on the user interface and work back through web fundamentals to deliver a fast, slick, and resilient user experience that deploys to any Node.js server and even non-Node.js environments at the edge like Cloudflare Workers.

Want to know more? Read the Technical Explanation of Remix

This repository contains the Remix source code. This repo is a work in progress, so we appreciate your patience as we figure things out.

Documentation

For documentation about Remix, please visit our website.

Also, please join our community on Discord.

The documentation is automatically generated on each release from the files in the docs directory.

Contributing

If you're interested in contributing code and/or documentation, please see our guide to contributing.

Code of Conduct

Please see our code of conduct for any questions about the kind of community we are trying to build here and what to do if you need help with someone who is not acting professionally.

remix's People

Contributors

ascorbic avatar brookslybrand avatar brophdawg11 avatar canrau avatar chaance avatar deanmv avatar dependabot[bot] avatar donavon avatar girish21 avatar github-actions[bot] avatar hi-ogawa avatar hollandthomas avatar jacob-ebey avatar jenseng avatar kentcdodds avatar kumard3 avatar lpsinger avatar machour avatar markdalgleish avatar mcansh avatar michaeldeboey avatar mjackson avatar mkrtchian avatar ngbrown avatar pcattori avatar penx avatar remix-cla-bot[bot] avatar remix-run-bot avatar ryanflorence avatar sergiodxa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

remix's Issues

Fixture testing infrastructure

Was just thinking it'd be cool to have a really low-friction way to create and test a fixture.

  1. Allow fixtures to be incomplete
  2. Have a function that gives you a "fixture object" that you can then send actions to and assert

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.

Implementation thoughts

createFixture just barfs out those files into a tmp/ folder, and then builds and runs it.

Remix "Enhancers"

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.

Dynamic Component

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>

Static function

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>;

Magic variables

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>;

React Root Enhancers

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} />;

Server placeholder

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>

Bring our friends to the party

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!

Separate Scripts component

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.

npm start doesn't start the dev server

The doc says
image
when I did that, I saw:
image
and
image
looking at package.json, it seems the command should be npm run dev:
image
which was also mentioned in the README.md. So maybe the documentation needs to be fixed?

Cannot sign in to Remix dashboard on mobile

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.

404 route only works with .js extension

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:

image

Can't use typeorm in loaders

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

Could the MDX default export get the frontmatter as props?

---
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'].

Rewrite line number / file in error server error logging

Would be sweet too get these to point to the real file, then in VSCode you can just click the file and go right to the line :D

Would definitely need source maps, but I think I've seen this done before, I have no idea how to do it though.

image

Dynamic imports

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/>

Fix loader redirects

Redirects work as intended on the initial HTML render, but not on a client side transition.

Scroll position is not restored correctly when navigating to a shorter page

Steps to reproduce:

  1. https://remix.run/dashboard/docs/tutorial/defining-routes
  2. Scroll to the bottom of the page and click the "Next up: Loading Data" link
  3. Scroll to the bottom of the "Loading Data" page
  4. Click back
    • you are correctly brought to the bottom of the previous page (though with a bit of jank)
  5. Click forward
    • you'll be brought to the middle of the page

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.

Testing and the app/routes

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?

Clarify where .npmrc should live

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?

Clarify Typescript integration

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.

  • Is Typescript mandatory to use remix-run?
  • What files should use the .tsx extension?
  • I'm not familiar with TS, but I didn't notice anything in these files that justify the extension, am I wrong?

Better development error display

When it comes to error handling in Remix there are two categories of errors to deal with:

  1. Development errors (like rollup not being able to compile)
  2. Production errors (uncaught errors in loaders or render)

Development Errors

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:

  • error in readConfig
    • error in terminal, process exits
  • dev asset server fails to build on file change (indicates that remix build won't work either)
    • message in terminal
    • message in the browser through a websocket
  • app server running, dev asset server offline
    • message in terminal
    • message in the browser through a websocket

Production Errors

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:

  • An error during render
  • An error in data loading

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.

Specific error handling

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.

  • In loaders: catch the error and send the information as normal data with the proper status code
  • in render: use normal React error handling in the component by wrapping in another component with 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.

Future Next Step

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>;
}

Why not do the future stuff now?

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.

Automatically handle scroll position on location changes

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();
  // ...
}

HMR

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.

Google App Engine deployment example

[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.

AWS API Gateway / Lambda Support (via AWS CDK)

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.

Current Progress

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:

  • remix-run-apigateway - Remix bindings for AWS Lambda
    • I think this is a good start, but there are a few areas that could be improved:
      1. 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 response
      2. calculating the build directory - the publicPath 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
      3. Decide if its ok/useful to expose the error in some cases to the end user [1, 2
      4. Stream support - The @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.
      5. Duplicate Headers - ApiGateway has headers (and query parameters) parsed in two different ways: 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-starter-apigateway - An example project using remix-run-apigateway
  • cdk - An AWS CDK project to deploy remix-starter-apigateway

Issues

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.

image

Potential Solution

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.

Revoke the Remix License Key

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.

Loaders for mutations with `<Form/>`

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.

Web 1.0 Mutations

  1. Make a form
<form method="post" action="/projects/create">
  <input type="text" name="title" />
  <input type="text" name="description" />
  <button type="submit">Create Project</button>
</form>
  1. Handle it on the server
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.

Why we do it client side

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.

Why not both?

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.

Big example:

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" 🤪

Form is like Link

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 🎉

Implementation Notes

  • <Form> could navigate(to, { state: { method, body }}) to trigger @remix-run/react.fetchData to use the proper method.
    • could instead make a remix specific navigate like navigate(to, { method, body }) or navigate.post(to, body), just some ideas.
  • If we want to support 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.

Next level: turn loaders into http controllers

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 }})

Enhancements for global.fetch

A few small enhancements I'd like to make to the global.fetch we provide in node:

  • Use { compress: false } by default. This lets people more easily return the result of a fetch directly from a data loader with the proper encoding.
  • Use the .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.

Running Remix on Cloudflare Workers

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.

Motivation

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.

API

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.

Building

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:

  • A lookup for route modules
  • A lookup for loaders
  • The build manifest (lookup for assets)

Open Questions

  • Is this even doable in Cloudflare Workers? What are the resource constraints?
  • We'll need ReactDOMServer.renderToString in our entry-server.js but I'm guessing react-dom/server is not designed to be bundled...
  • Instead of compiling on-demand like the server now does, do we just compile everything up front?

Add a changelog page to the dashboard/docs

Proposal

Add a changelog page within the documentation to allow users to view updates within the dashboard.

Workaround

Currently, release information is being added to the #releases channel in discord.

Suggested Tags: enhancement | remix.run

Firefox doesn't follow es module imports on transitions? (I think?)

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 :)

Option to disable morgan output in remix server

The morgan output for the remix server makes it difficult for me to find my own logging output:

image

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.

Support default exports in loaders

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"

Remix Progressive Enhancement

Two approaches, I don't know pros and cons yet, but probably have some.

Dynamic Component

<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>

Static function

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>;

Magic variables

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} />;

Server placeholder

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>

Preloading Resources (`links` or `head` export)

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:

  1. Route JavaScript Module
  2. Route CSS
  3. Route Data

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.

The API

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>.

A basic preload link

You can put any kind of link you want:

export function links() {
  return [
    {
      rel: "preload",
      href: "/img/some-image.jpg",
      as: "image",
    },
  ];
}

Route specific links

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!

Preloading routes with params

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 },
    },
  ];
}

Preloading based on route data

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!

Implementation notes

  • use the asset manifest to look up assets by route id
  • include preload assets in the initial partialManifest
  • include preload assets in _remix/manifest? requests.
  • transform 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.

Perhaps just use 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" }
    ])
  }
}

error navigating to route that loads data

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.

Screenshot from 2020-10-28 13-57-19

Screenshot from 2020-10-28 14-56-51

PostCSS Config

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"
  ]
}

Getting parent data into child loaders

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).

Some use cases:

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}`);
}

Implementation notes

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.

"Extra attributes from the server" and other hydration warnings

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.

Cache initial HTML page data in service worker cache?

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 workers

Service worker caching can help us here. I don't know the APIs but it's goes something like this:

  1. put the loader's headers on __remixcontext
  2. take data on __remixContext and shove it into a "fetch" cache with the __remix_data?... url and the headers from the loaders

Boom! Now when the user clicks back to the initial html page, it's as though they fetched from the client.

Babel config

Open up and use an application babel config, apps will just need to make sure to include our preset:

{
  "presets": [
    "@remix-run/core/babel",
  ]
}

add ~ alias to imports?

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.

[Firefox OSX] - Navigating history is not updating the page

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))

Digital Ocean - Deployment Example

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

image

image

image

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.