Skip to content

Commit

Permalink
Merge branch 'dev' into markdalgleish/update-polyfills-plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish authored Jun 20, 2023
2 parents 2bda2d3 + b5bfcd1 commit fb67d93
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-zoos-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Fix bug with pathless layout routes beneath nested path segments
6 changes: 6 additions & 0 deletions .changeset/prefetch-viewport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"remix": minor
"@remix-run/react": minor
---

Add support for `<Link prefetch="viewport">` to prefetch links when they enter the viewport via an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
2 changes: 2 additions & 0 deletions docs/components/link.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ In the effort to remove all loading states from your UI, `Link` can automaticall
<Link prefetch="none" />
<Link prefetch="intent" />
<Link prefetch="render" />
<Link prefetch="viewport" />
</>
```

- **"none"** - Default behavior. This will prevent any prefetching from happening. This is recommended when linking to pages that require a user session that the browser won't be able to prefetch anyway.
- **"intent"** - Recommended if you want to prefetch. Fetches when Remix thinks the user intends to visit the link. Right now the behavior is simple: if they hover or focus the link it will prefetch the resources. In the future we hope to make this even smarter. Links with large click areas/padding get a bit of a head start. It is worth noting that when using `prefetch="intent"`, `<link rel="prefetch">` elements will be inserted on hover/focus and removed if the `<Link>` loses hover/focus. Without proper `cache-control` headers on your loaders, this could result in repeated prefetch loads if a user continually hovers on and off a link.
- **"render"** - Fetches when the link is rendered.
- **"viewport"** - Fetches while the link is in the viewport

<docs-error>You may need to use the <code>:last-of-type</code> selector instead of <code>:last-child</code> when styling child elements inside of your links</docs-error>

Expand Down
3 changes: 2 additions & 1 deletion docs/components/nav-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ toc: false

# `<NavLink>`

A `<NavLink>` is a special kind of `<Link>` that knows whether or not it is "active" or "pending". This is useful when building a navigation menu, such as a breadcrumb or a set of tabs where you'd like to show which of them is currently selected. It also provides useful context for assistive technology like screen readers.
A `<NavLink>` is a special kind of [`<Link>`][link] that knows whether or not it is "active" or "pending". This is useful when building a navigation menu, such as a breadcrumb or a set of tabs where you'd like to show which of them is currently selected. It also provides useful context for assistive technology like screen readers.

```tsx
import { NavLink } from "@remix-run/react";
Expand Down Expand Up @@ -122,3 +122,4 @@ Adding the `caseSensitive` prop changes the matching logic to make it case sensi
When a `NavLink` is active it will automatically apply `<a aria-current="page">` to the underlying anchor tag. See [aria-current][aria-current] on MDN.

[aria-current]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current
[link]: ./link.md
17 changes: 17 additions & 0 deletions docs/other-api/dev-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ The dev server will:
3. Restart your app server whenever rebuilds succeed
4. Send code updates to the browser via Live Reload and HMR + Hot Data Revalidation

<docs-info>

What is "Hot Data Revalidation"?

Like HMR, HDR is a way of hot updating your app without needing to refresh the page.
That way you can keep your app state as your edits are applied in your app.
HMR handles client-side code updates like when you change the components, markup, or styles in your app.
Likewise, HDR handles server-side code updates.

That means any time your change a `loader` on your current page (or any code that your `loader` depends on), Remix will re-fetch data from your changed loader.
That way your app is _always_ up-to-date with the latest code changes, client-side or server-side.

To learn more about how HMR and HDR work together, check out [Pedro's talk at Remix Conf 2023][legendary-dx].

</docs-info>

### With `remix-serve`

Enable the v2 dev server:
Expand Down Expand Up @@ -342,6 +358,7 @@ That way the dev server can detect loader changes on rebuilds.

While the initial build slowdown is inherently a cost for HDR, we plan to optimize rebuilds so that there is no perceivable slowdown for HDR rebuilds.

[legendary-dx]: https://www.youtube.com/watch?v=79M4vYZi-po
[templates]: https://github.com/remix-run/remix/tree/main/templates
[watch-paths]: https://remix.run/docs/en/1.17.1/file-conventions/remix-config#watchpaths
[jenseng-code]: https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
Expand Down
7 changes: 4 additions & 3 deletions docs/pages/v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ For configuration options, see the [`remix dev` docs][v2-dev-config].

### `remix-serve`

Enable the v2 dev server:
If you are using the Remix App Server (`remix-serve`), enable the v2 dev server:

```js filename=remix.config.js
module.exports = {
Expand All @@ -721,9 +721,10 @@ module.exports = {

That's it!

### custom app server
### Custom app server

Check out our [templates][templates] for examples of how to integrate with `v2_dev`
If you are using your own app server (`server.js`),
then check out our [templates][templates] for examples of how to integrate with `v2_dev`
or follow these steps:

1. Enable the v2 dev server:
Expand Down
71 changes: 71 additions & 0 deletions integration/prefetch-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,74 @@ test.describe("prefetch=intent (focus)", () => {
expect(await page.locator("#nav link").count()).toBe(1);
});
});

test.describe("prefetch=viewport", () => {
let fixture: Fixture;
let appFixture: AppFixture;

test.beforeAll(async () => {
fixture = await createFixture({
config: {
future: { v2_routeConvention: true },
},
files: {
"app/routes/_index.jsx": js`
import { Link } from "@remix-run/react";
export default function Component() {
return (
<>
<h1>Index Page - Scroll Down</h1>
<div style={{ marginTop: "150vh" }}>
<Link to="/test" prefetch="viewport">Click me!</Link>
</div>
</>
);
}
`,

"app/routes/test.jsx": js`
export function loader() {
return null;
}
export default function Component() {
return <h1>Test Page</h1>;
}
`,
},
});

// This creates an interactive app using puppeteer.
appFixture = await createAppFixture(fixture);
});

test.afterAll(() => {
appFixture.close();
});

test("should prefetch when the link enters the viewport", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");

// No preloads to start
await expect(page.locator("div link")).toHaveCount(0);

// Preloads render on scroll down
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));

await page.waitForSelector(
"div link[rel='prefetch'][as='fetch'][href='/test?_data=routes%2Ftest']",
{ state: "attached" }
);
await page.waitForSelector(
"div link[rel='modulepreload'][href^='/build/routes/test-']",
{ state: "attached" }
);

// Preloads removed on scroll up
await page.evaluate(() => window.scrollTo(0, 0));
await expect(page.locator("div link")).toHaveCount(0);
});
});
25 changes: 25 additions & 0 deletions packages/remix-dev/__tests__/flat-routes-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,31 @@ describe("flatRoutes", () => {
path: ":id",
},
],
[
"routes/app._pathless.tsx",
{
id: "routes/app._pathless",
parentId: "routes/app",
path: undefined,
},
],
[
"routes/app._pathless._index.tsx",
{
id: "routes/app._pathless._index",
parentId: "routes/app._pathless",
index: true,
path: undefined,
},
],
[
"routes/app._pathless.child.tsx",
{
id: "routes/app._pathless.child",
parentId: "routes/app._pathless",
path: "child",
},
],
[
"routes/folder/route.tsx",
{
Expand Down
3 changes: 1 addition & 2 deletions packages/remix-dev/config/flat-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export function flatRoutesUniversal(
}

if (!config.parentId) config.parentId = "root";
config.path = pathname || undefined;

/**
* We do not try to detect path collisions for pathless layout route
Expand Down Expand Up @@ -264,8 +265,6 @@ export function flatRoutesUniversal(

let conflictRouteId = originalPathname + (config.index ? "?index" : "");
let conflict = uniqueRoutes.get(conflictRouteId);

config.path = pathname || undefined;
uniqueRoutes.set(conflictRouteId, config);

if (conflict && (originalPathname || config.index)) {
Expand Down
50 changes: 41 additions & 9 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function RemixRouteError({ id }: { id: string }) {
* - "render": Fetched when the link is rendered
* - "none": Never fetched
*/
type PrefetchBehavior = "intent" | "render" | "none";
type PrefetchBehavior = "intent" | "render" | "none" | "viewport";

export interface RemixLinkProps extends LinkProps {
prefetch?: PrefetchBehavior;
Expand All @@ -219,19 +219,35 @@ interface PrefetchHandlers {
onTouchStart?: TouchEventHandler;
}

function usePrefetchBehavior(
function usePrefetchBehavior<T extends HTMLAnchorElement>(
prefetch: PrefetchBehavior,
theirElementProps: PrefetchHandlers
): [boolean, Required<PrefetchHandlers>] {
): [boolean, React.RefObject<T>, Required<PrefetchHandlers>] {
let [maybePrefetch, setMaybePrefetch] = React.useState(false);
let [shouldPrefetch, setShouldPrefetch] = React.useState(false);
let { onFocus, onBlur, onMouseEnter, onMouseLeave, onTouchStart } =
theirElementProps;

let ref = React.useRef<T>(null);

React.useEffect(() => {
if (prefetch === "render") {
setShouldPrefetch(true);
}

if (prefetch === "viewport") {
let callback: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
setShouldPrefetch(entry.isIntersecting);
});
};
let observer = new IntersectionObserver(callback, { threshold: 0.5 });
if (ref.current) observer.observe(ref.current);

return () => {
observer.disconnect();
};
}
}, [prefetch]);

let setIntent = () => {
Expand Down Expand Up @@ -260,6 +276,7 @@ function usePrefetchBehavior(

return [
shouldPrefetch,
ref,
{
onFocus: composeEventHandlers(onFocus, setIntent),
onBlur: composeEventHandlers(onBlur, cancelIntent),
Expand All @@ -282,17 +299,18 @@ let NavLink = React.forwardRef<HTMLAnchorElement, RemixNavLinkProps>(
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);

let href = useHref(to);
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior(
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(
prefetch,
props
);

return (
<>
<RouterNavLink
ref={forwardedRef}
to={to}
{...props}
{...prefetchHandlers}
ref={mergeRefs(forwardedRef, ref)}
to={to}
/>
{shouldPrefetch && !isAbsolute ? (
<PrefetchPageLinks page={href} />
Expand All @@ -315,18 +333,18 @@ let Link = React.forwardRef<HTMLAnchorElement, RemixLinkProps>(
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);

let href = useHref(to);
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior(
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(
prefetch,
props
);

return (
<>
<RouterLink
ref={forwardedRef}
to={to}
{...props}
{...prefetchHandlers}
ref={mergeRefs(forwardedRef, ref)}
to={to}
/>
{shouldPrefetch && !isAbsolute ? (
<PrefetchPageLinks page={href} />
Expand Down Expand Up @@ -1820,3 +1838,17 @@ export const LiveReload =
/>
);
};

function mergeRefs<T = any>(
...refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current = value;
}
});
};
}

0 comments on commit fb67d93

Please sign in to comment.