这是用户在 2024-3-29 10:23 为 https://buildui.com/posts/global-progress-in-nextjs#showing-progress-during-navigations 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

Global progress in Next.js
Next.js 在全球的进展

Sam Selikoff

Sam Selikoff 山姆·塞利科夫

Ryan Toronto

Ryan Toronto 瑞恩·多伦多

Introduction

The new App Router in Next.js doesn't expose router events, which makes it tricky to wire up libraries like NProgress to show global pending UI during page navigations.
Next.js 中的新 App Router 不会暴露路由事件,这使得将 NProgress 等库与页面导航期间显示全局待处理 UI 变得棘手。

The following demo uses a transition-aware Hook and a custom link component to show a progress bar while new pages are being loaded.
以下演示使用一个具有过渡感知的 Hook 和一个自定义链接组件,在加载新页面时显示进度条。

Try clicking the links to see it in action:
请点击链接以查看其效果:

The progress starts when a link is clicked and completes when the server responds with the new page. Because navigations in Next.js are marked as Transitions, the existing UI remains visible – and interactive – while the update is pending.
进度从点击链接开始,直到服务器响应新页面时完成。由于 Next.js 中的导航被标记为过渡,因此在更新挂起期间,现有的用户界面仍然可见且可交互。

Show me the code! 给我看代码!

Drop <ProgressBar> in a Layout, and any <ProgressBarLink> that's rendered as a child will animate the bar when clicked.
<ProgressBar> 放入布局中,任何作为子元素呈现的 <ProgressBarLink> 在点击时将会以动画方式显示栏。

"use client";

import {
  AnimatePresence,
  motion,
  useMotionTemplate,
  useSpring,
} from "framer-motion";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
  ComponentProps,
  ReactNode,
  createContext,
  startTransition,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";

const ProgressBarContext = createContext<ReturnType<typeof useProgress> | null>(
  null
);

export function useProgressBar() {
  let progress = useContext(ProgressBarContext);

  if (progress === null) {
    throw new Error("Need to be inside provider");
  }

  return progress;
}

export function ProgressBar({ className, children }: { className: string, children: ReactNode }) {
  let progress = useProgress(); 
  let width = useMotionTemplate`${progress.value}%`; 

  return (
    <ProgressBarContext.Provider value={progress}>
      <AnimatePresence onExitComplete={progress.reset}>
        {progress.state !== "complete" && (
          <motion.div
            style={{ width }}
            exit={{ opacity: 0 }}
            className={className}
          />
        )}
      </AnimatePresence>

      {children}
    </ProgressBarContext.Provider>
  );
}

export function ProgressBarLink({
  href,
  children,
  ...rest
}: ComponentProps<typeof Link>) {
  let progress = useProgressBar(); 
  let router = useRouter();

  return (
    <Link
      href={href}
      onClick={(e) => {
        e.preventDefault();
        progress.start(); 

        startTransition(() => {
          router.push(href.toString());
          progress.done(); 
        });
      }}
      {...rest}
    >
      {children}
    </Link>
  );
}

function useProgress() {
  const [state, setState] = useState<
    "initial" | "in-progress" | "completing" | "complete"
  >("initial");

  let value = useSpring(0, {
    damping: 25,
    mass: 0.5,
    stiffness: 300,
    restDelta: 0.1,
  });

  useInterval(
    () => {
      // If we start progress but the bar is currently complete, reset it first.
      if (value.get() === 100) {
        value.jump(0);
      }

      let current = value.get();

      let diff;
      if (current === 0) {
        diff = 15;
      } else if (current < 50) {
        diff = rand(1, 10);
      } else {
        diff = rand(1, 5);
      }

      value.set(Math.min(current + diff, 99));
    },
    state === "in-progress" ? 750 : null
  );

  useEffect(() => {
    if (state === "initial") {
      value.jump(0);
    } else if (state === "completing") {
      value.set(100);
    }

    return value.on("change", (latest) => {
      if (latest === 100) {
        setState("complete");
      }
    });
  }, [value, state]);

  function reset() {
    setState("initial");
  }

  function start() {
    setState("in-progress");
  }

  function done() {
    setState((state) =>
      state === "initial" || state === "in-progress" ? "completing" : state
    );
  }

  return { state, value, start, done, reset };
}

function rand(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function useInterval(callback: () => void, delay: number | null) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      tick();

      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

The progress bar is powered by a custom useProgress Hook. The Hook returns a Motion Value that's used to animate the bar's width, as well as start and done functions called by <ProgressLink>.
进度条由自定义的 useProgress Hook 驱动。该 Hook 返回一个 Motion Value,用于动画化进度条的宽度,以及由 <ProgressLink> 调用的 startdone 函数。

Since navigations in the App Router are marked as Transitions, the link uses startTransition to call done alongside router.push. Once the new page is ready and the UI has been updated, the bar completes its animation.
由于应用程序路由中的导航被标记为转换,链接使用 startTransition 来调用 done 以及 router.push 。一旦新页面准备好并且 UI 已更新,导航栏完成其动画。


Let's learn how it works by first looking at our custom useProgress Hook!
让我们通过首先查看我们自定义的 useProgress Hook 来学习它是如何工作的!

The useProgress Hook 使用进度钩子

useProgress exposes five values:
useProgress 暴露了五个值:

const { start, done, reset, state, value } = useProgress();

start, done, and reset control the progress's state; state is a string representing the current state of the Hook; and value is a Motion Value (from Framer Motion) that handles the animation.
startdonereset 控制进度的状态; state 是表示 Hook 当前状态的字符串; value 是处理动画的 Motion Value(来自 Framer Motion)。

Here's a simple UI wired up to useProgress. Try pressing the buttons to see the behavior:
这是一个简单的用户界面,已连接到 useProgress 。尝试按下按钮查看其行为:

Once you press Start, the Hook kicks off an interval that periodically animates value towards 100 – without ever reaching it. Pressing Done moves the Hook to the completing state, which animates the value all the way to 100.
一旦您按下开始,Hook 将启动一个间隔,定期将 value 动画化到 100,但永远不会达到。按下完成将将 Hook 移动到完成状态,该状态将值动画化到 100。

As soon as value reaches 100, the Hook's state updates to complete. From there, you can reset the Hook back to its initial state, or press Start to kick off the cycle all over again.
一旦数值达到 100,Hook 的状态就会更新为完成。然后,您可以将 Hook 重置回初始状态,或者按下“开始”重新启动整个循环。

You can also press Done from the initial state to animate value from 0 directly to 100, bypassing the in-progress state altogether.
您还可以从初始状态直接按下“完成”按钮,将 value 从 0 直接动画到 100,完全绕过正在进行的状态。

Let's see how to animate some UI with our new Hook!
让我们看看如何使用我们的新 Hook 来为一些 UI 添加动画效果!

Rendering animated loaders
渲染动画加载器

Now that we have a Motion Value that animates from 0 to 100, we can use it with any motion element.
现在我们有一个从 0 到 100 变化的动画值,我们可以将其与任何动画元素一起使用。

For example, we could render a spinner:
例如,我们可以渲染一个旋转器

Or a horizontal progress bar:
或者是一个水平进度条:

If we want to automatically fade out the bar when the progress completes, we can use <AnimatePresence> to add an exit animation:
如果我们想在进度完成时自动淡出该栏,我们可以使用 <AnimatePresence> 来添加退出动画

Calling reset after our progress fades out puts it back into its initial state, so we can immediately click Start or Done to kick off a new cycle.
在我们的进度消失后调用 reset 将其恢复到初始状态,这样我们就可以立即点击开始或完成来启动一个新的周期。

In fact, if we fix the bar to the top of the screen and drop the Reset button, we have exactly what we need for the UI from our final demo:
实际上,如果我们将工具栏固定在屏幕顶部并删除重置按钮,我们就得到了我们最终演示所需的用户界面

All that's left is to wire up the calls to start and done to link navigations in Next.js. Let's see how to do it!
剩下的就是将调用 startdone 连接到 Next.js 中的导航。让我们看看如何做到这一点!

Showing progress during navigations
导航过程中显示进度

Like we said at the beginning of this post, the App Router dropped support for router.events, which was a feature of the Pages Router. If it still exposed those events, we could just call start and done in response to the routeChangeStart and routeChangeComplete events, and call it a day.
就像我们在这篇文章开头所说的那样,App 路由器不再支持 router.events ,这是页面路由器的一个功能。如果它仍然暴露这些事件,我们可以在 routeChangeStartrouteChangeComplete 事件发生时调用 startdone ,然后就可以结束了。

Instead, the intended way to extend the behavior of next/link in the App Router is to make our own custom Link component that wraps it.
相反,扩展 App Router 中 next/link 行为的预期方式是创建自定义的 Link 组件来包装它。

So, let's start by creating a new component. We'll call it <ProgressLink>, since clicking it will eventually trigger our global progress bar.
所以,让我们从创建一个新的组件开始。我们将其称为 <ProgressLink> ,因为点击它最终会触发我们的全局进度条。

We'll give it an href prop and children, and have it return next/link:
我们会给它一个 href 属性和 children ,然后让它返回 next/link

// app/components/progress-link.tsx
"use client";

import Link from "next/link";
import { ReactNode } from "react";

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  return (
    <Link href={href}>
      {children}
    </Link>
  );
}

Great! So far it works just like <Link>.
太棒了!到目前为止,它的工作就像 <Link> 一样。

Next, let's prevent the default behavior by calling e.preventDefault in onClick:
接下来,通过在 onClick 中调用 e.preventDefault 来防止默认行为

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  return (
    <Link
      onClick={(e) => { 
        e.preventDefault(); 
      }} 
      href={href}
    >
      {children}
    </Link>
  );
}

<ProgressLink> is now inert – clicking it doesn't cause a navigation.
<ProgressLink> 现在是惰性的 - 点击它不会导致导航。

But, if we grab the router from the useRouter Hook, we can programmatically navigate to our href prop using router.push:
但是,如果我们从 useRouter Hook 中获取路由器,我们可以使用 router.push 编程方式导航到我们的 href 属性

import { useRouter } from "next/navigation";

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  let router = useRouter(); 

  return (
    <Link
      onClick={(e) => {
        e.preventDefault();
        router.push(href); 
      }}
      href={href}
    >
      {children}
    </Link>
  );
}

Cool! Our link's working again, but now we have programmatic control over the navigation.
酷!我们的链接又恢复正常了,但现在我们可以通过编程控制导航。

So – when should we start and stop our progress?
那么,我们应该何时开始和停止我们的进步?

You might be thinking that router.push() returns a Promise, in which case we should be able to do something like this:
你可能会认为 router.push() 返回一个 Promise,这样我们应该能够像这样做:

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  let router = useRouter();
  let { start, done } = useProgress(); 

  return (
    <Link
      onClick={async (e) => {
        e.preventDefault();

        start(); 
        await router.push(href); 
        done(); 
      }}
      href={href}
    >
      {children}
    </Link>
  );
}

But it doesn't. Its return type is void, meaning it doesn't return anything. And there's no state to read from the router itself to cue us in to the fact that a navigation is underway.
但它不会。它的返回类型是 void ,意味着它不返回任何内容。而且没有状态可以从 router 本身读取,以提示我们导航正在进行中。

So – how the heck can we tell when a navigation is finished?
那么,我们究竟如何判断导航是否完成呢?

It turns out that the App Router is built on React Transitions. Transitions were introduced in React 18, and even though they've been out for about two years, they're still making their way into libraries and frameworks throughout the ecosystem.
原来 App 路由器是建立在 React Transitions 之上的。过渡效果是在 React 18 中引入的,尽管已经过去了大约两年,但它们仍然在整个生态系统的库和框架中逐渐应用。

And conceptually, Transitions are very similar to Promises. They represent some potential future value – although instead of any JavaScript value, that future value is a React tree; and they also exist in either a pending or a settled state.
概念上,过渡与 Promise 非常相似。它们代表着一些潜在的未来值 - 尽管不是 JavaScript 的任何值,而是一个 React 树;并且它们也存在于等待或已解决的状态中。

But how we use them is a bit different. Let's see how with a simple example.
但是我们使用它们的方式有些不同。让我们通过一个简单的例子来看看。

How Transitions work 过渡效果的工作原理

Suppose we wanted to show how many times each <ProgressLink> has been clicked. Let's define some new React State called count, increment it on click, and display it next to our link's label:
假设我们想要显示每个 <ProgressLink> 被点击的次数。让我们定义一些新的 React 状态,称为 count ,在点击时递增它,并将其显示在链接标签旁边:

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  let router = useRouter()
  let [count, setCount] = useState(0); 

  return (
    <Link
      onClick={async (e) => {
        e.preventDefault();

        setCount(c => c + 1);
        router.push(href);
      }}
      href={href}
    >
      {children} {count}
    </Link>
  );
}

Simple enough. Let's check out the behavior:
简单明了。让我们来看看行为:

If you start at Home and click Page 1, you'll see the count update immediately. Then, once Page 1 is loaded, the rest of the UI updates.
如果你从主页开始,点击页面 1,你会立即看到计数更新。然后,一旦页面 1 加载完成,其余的界面也会更新。

Now check this out. Let's wrap our calls to setCount and router.push inside of startTransition, a function provided by React:
现在看看这个。让我们将对 setCountrouter.push 的调用包装在 React 提供的 startTransition 函数中:

import { startTransition } from 'react'; 

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  let router = useRouter()
  let [count, setCount] = useState(0);

  return (
    <Link
      onClick={async (e) => {
        e.preventDefault();

        startTransition(() => { 
          setCount(c => c + 1); 
          router.push(href); 
        }); 
      }}
      href={href}
    >
      {children} {count}
    </Link>
  );
}

Check out the behavior now:
现在看看行为

Isn't that wild? 这难道不是疯狂的吗?

If you start out at Home, refresh, and then click Page 1, the count doesn't update until router.push has finished loading the new page. The entire UI updates together with all the new state.
如果你从主页开始,刷新,然后点击第一页,计数直到 router.push 加载完新页面后才会更新。整个用户界面与所有新状态一起更新。

So, when we kick off a Transition with one or more State updates:
所以,当我们开始一个转换,并进行一个或多个状态更新时:

startTransition(() => { 
  setCount(c => c + 1); 
  router.push(href); 
}); 

React attempts to run all of the updates together, in the background. If any of those updates suspend – which in our case, router.push does – React will hold off applying the updates to our current UI until the new tree is fully ready to be rendered.
React 试图将所有的更新一起在后台运行。如果其中任何一个更新被挂起 - 在我们的情况下, router.push 是 - React 将推迟将更新应用于我们当前的 UI,直到新的树完全准备好进行渲染。

You can think of the new tree that's being prepared as a fork of our current UI. React keeps it in the background until it's fully ready, at which point it's merged back into our main UI, and we see the updates committed to the screen.
您可以将正在准备的新树视为我们当前用户界面的分支。React 将其保留在后台,直到完全准备好,然后将其合并回我们的主要用户界面,我们就可以看到更新内容提交到屏幕上。

Tracking navigations 跟踪导航

Now that we have a way to set some state once router.push has finished, let's replace our count state with an isNavigating boolean:
现在我们有了一种在 router.push 完成后设置一些状态的方法,让我们用一个 isNavigating 布尔值替换我们的 count 状态

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  let router = useRouter()
  let [isNavigating, setIsNavigating] = useState(false); 

  return (
    <Link
      onClick={async (e) => {
        e.preventDefault();

        router.push(href);
      }}
      href={href}
    >
      {children}
    </Link>
  );
}

Next, let's use a normal state update to set isNavigating to true when we click, and flip it back to false inside of a Transition:
接下来,让我们使用正常的状态更新,在点击时将 isNavigating 设置为 true,并在 Transition 中将其翻转回 false

export default function ProgressLink({
  href,
  children,
}: {
  href: string;
  children: ReactNode;
}) {
  let router = useRouter();
  let [isNavigating, setIsNavigating] = useState(false);

  return (
    <Link
      onClick={(e) => {
        e.preventDefault();
        setIsNavigating(true); 

        startTransition(() => { 
          router.push(href); 
          setIsNavigating(false); 
        }); 
      }}
      href={href}
    >
      {children}
    </Link>
  );
}

Our isNavigating state should now track our navigations!
我们的状态应该现在跟踪我们的导航!

Let's update the label to show three dots while isNavigating is true:
让我们更新标签,在 isNavigating 为真时显示三个点:

<Link
  onClick={(e) => {
    e.preventDefault();
    setIsNavigating(true);

    startTransition(() => {
      router.push(href);
      setIsNavigating(false);
    });
  }}
  href={href}
>
  {children} {isNavigating ? "..." : ""}
</Link>

and give it a shot!
试一试!

The dots render while the navigation is in progress, and disappear as soon as the new page is ready.
导航进行时显示点,新页面准备好后消失。

...meaning we now know exactly when to start and stop our progress bar!
意思是我们现在知道了何时开始和停止我们的进度条!

Rendering the progress bar
渲染进度条

Let's bring our <ProgressBar> back into our Layout, and pass the start and done functions into our links as props:
让我们将 <ProgressBar> 带回我们的布局中,并将 startdone 函数作为 props 传递给我们的链接

export default function Layout({ children }: { children: ReactNode }) {
  let { value, state, start, done, reset } = useProgress();

  return (
    <div>
      <AnimatePresence onExitComplete={reset}>
        {state !== "complete" && (
          <motion.div exit={{ opacity: 0 }} className="w-full">
            <ProgressBar progress={value} />
          </motion.div>
        )}
      </AnimatePresence>

      <nav>
        <ProgressLink start={start} done={done} href="/demo-8-progress-link">
          Home
        </ProgressLink>
        <ProgressLink start={start} done={done} href="/demo-8-progress-link/1">
          Page 1
        </ProgressLink>
        <ProgressLink start={start} done={done} href="/demo-8-progress-link/2">
          Page 2
        </ProgressLink>
        <ProgressLink start={start} done={done} href="/demo-8-progress-link/3">
          Page 3
        </ProgressLink>
      </nav>

      <div className="m-4">{children}</div>
    </div>
  );
}

Inside our <ProgressLink>, we can call start() immediately and done() inside the Transition:
在我们的 <ProgressLink> 内部,我们可以立即调用 start() 并在 Transition 内部调用 done()

<Link
  onClick={(e) => {
    e.preventDefault();
    start(); 

    startTransition(() => {
      router.push(href);
      done(); 
    });
  }}
  href={href}
>
  {children}
</Link>

The progress bar should now track our navigations.
进度条现在应该能够跟踪我们的导航。

Let's give it a shot:
让我们试一试:

Booyah! 哇哦!

Whenever we click one of our links, the <ProgressBar> starts animating, and when the new page finishes loading, our app updates, and the progress bar completes its animation.
每当我们点击其中一个链接时, <ProgressBar> 开始动画,当新页面加载完成时,我们的应用程序会更新,进度条完成其动画。

Interruptibility 可中断性

One neat benefit that falls out of Transitions is how our <ProgressLink> handles interruptions.
转换的一个很好的好处是我们的 <ProgressLink> 如何处理中断。

Try quickly pressing Page 1 and then Page 2:
尝试快速按下第一页,然后按下第二页

Notice how our app keeps the home page rendered – and the progress bar animating – without a flicker. Once Page 2 is ready, everything updates in one seamless re-render.
请注意我们的应用程序如何保持主页的渲染,并使进度条动画流畅无闪烁。一旦第二页准备好,所有内容将以无缝重新渲染的方式更新。

Pretty cool right? We got that behavior for free without even considering it, thanks to the behavior of Transitions.
很酷,对吧?我们得到了这种行为,甚至都没有考虑过,多亏了过渡的行为。

This is because Transitions are considered "low-priority updates":
这是因为过渡效果被认为是“低优先级的更新”

<Link
  onClick={(e) => {
    e.preventDefault();
    // Normal "high-priority" update
    start();

    startTransition(() => {
      // "Low-priority" updates
      router.push(href); 
      done(); 
    });
  }}
  href={href}
>
  {children}
</Link>

What this means is that any Transition has the potential to be discarded. If a new Transition is started that updates the same state of a currently pending Transition, the old update will be ignored.
这意味着任何转换都有可能被丢弃。如果启动了一个更新当前待处理转换的相同状态的新转换,旧的更新将被忽略。

You can think of it like this:
你可以这样想:

/*
  Clicking Page 1 does this...
*/
start();
startTransition(() => {
  router.push("/1");
  done();
});

/*
  ...and interrupting Page 1 by clicking Page 2 does this.
  The new Transition takes precedence over the first one.
*/
start();
startTransition(() => {
  router.push("/2");
  done();
});

Since our useProgress.start function can be called repeatedly without disrupting the in-progress animation, our UI avoids any unnecessary re-renders. Only once the second Transition settles is our done() function applied to the current UI.
由于我们的 useProgress.start 函数可以重复调用而不会中断正在进行的动画,因此我们的用户界面避免了任何不必要的重新渲染。只有在第二个过渡完成后,我们的 done() 函数才会应用于当前的用户界面。

Refactoring with Context 重构与上下文

Our new <ProgressLink> components work great, but there's a lot of prop passing going on. You can imagine that getting start and done to every link in our app could get pretty annoying, pretty fast.
我们的新 <ProgressLink> 组件效果很好,但是有很多属性传递。你可以想象,将 startdone 传递给我们应用程序中的每个链接可能会变得非常烦人,非常快速。

Let's extract our layout's <ProgressBar> into a provider component. It will still render the animated progress bar, but it will also use Context to provide start and done directly to <ProgressLink>.
让我们将布局的 <ProgressBar> 提取到一个提供者组件中。它仍然会渲染动画进度条,但它还将使用上下文直接提供 startdone<ProgressLink>

Awesome! Works just like before. And now <ProgressLink> has the same API as next/link, making it much easier to use throughout our app.
太棒了!功能和以前一样。现在 <ProgressLink>next/link 有相同的 API,在我们的应用程序中使用起来更加容易。

As an added bonus, our layout is back to being a Server Component, since the client code is only needed in the provider and links. This will make it easier to add data fetching to our layout, should the need arise in the future.
作为额外的奖励,我们的布局又回到了服务器组件,因为客户端代码只在提供者和链接中需要。这将使我们在将来需要时更容易向布局添加数据获取功能。

Why doesn't Next.js expose router events?
为什么 Next.js 不公开路由器事件?

We've built a pretty neat <ProgressLink> component that works well with React Transitions and the Next.js router – but admittedly, it was quite a bit of work. You might be wondering, why doesn't Next's new App Router just expose global navigation events and make this entire problem a whole lot easier?
我们已经构建了一个与 React Transitions 和 Next.js 路由器很好配合的很棒的 <ProgressLink> 组件-但是不可否认,这需要相当多的工作。你可能会想,为什么 Next 的新 App Router 不直接暴露全局导航事件,从而使整个问题变得更容易呢?

The answer: Composition. 答案是:构成。

Imagine the router did expose global events. We wire up our progress bar to animate on any navigation, and the good ol' <Link> component from Next works out of the box, showing our global progress any time its used.
想象一下路由器暴露了全局事件。我们将进度条与任何导航连接起来,而 Next 中的经典 <Link> 组件可以直接使用,每次使用时都会显示全局进度。

...but then someone comes along and builds a <Messenger> component that's docked to the bottom of the page:
...但是突然有人出现并构建了一个底部停靠的组件。

Oops. 哎呀。

If you click Sam or Ryan in the messenger, you'll see our global progress bar show up. Probably not what they intended.
如果你在 Messenger 中点击 Sam 或 Ryan,你会看到我们的全球进度条出现。可能不是他们想要的。

<Messenger> happens to use links as part of its implementation:
<Messenger> 的实现中使用了链接

"use client";

export default function Messenger() {
  return (
    <div className="fixed bottom-0 right-3">
      <p>Messages</p>

      <div className="mt-3">
        <Link href={`${pathname}?user=sam`}>Sam</Link>
        <Link href={`${pathname}?user=ryan`}>Ryan</Link>
      </div>
    </div>
  );
}

but because our <ProgressBar> is wired up to Next's global events, this new component – which was supposed to be an isolated component – is now triggering some global UI.
但是由于我们的 <ProgressBar> 与 Next 的全局事件相连,这个新组件 - 原本应该是一个独立的组件 - 现在触发了一些全局的用户界面。

This is the sort of refactoring hazard that the App Router is designed to help us avoid. By not exposing any APIs that can affect every usage of <Link>, Next.js is shielding us from this sort of "spooky action at a distance", and ensuring that any new feature we build using the framework's core primitives can be built in complete isolation from the rest of our app.
这是 App Router 旨在帮助我们避免的重构风险。通过不暴露任何可以影响 <Link> 的每个使用的 API,Next.js 使我们免受这种“远程幽灵行为”的影响,并确保我们使用框架的核心基元构建的任何新功能都可以完全独立于我们应用的其他部分进行构建。

Back in the App Router, the author of <Messenger> can just use next/link, and our progress never gets triggered:
回到应用路由器, <Messenger> 的作者只需使用 next/link ,我们的进展就不会被触发

Nice. 好的。

So, lack of global behavior is a common feature across Next's APIs. But it's not just about avoiding refactoring hazards...
因此,缺乏全局行为是 Next 的 API 中的一个共同特点。但这不仅仅是为了避免重构风险...

Let's say the author of <Messenger> saw our <ProgressBar> and wanted to try it out in their new component.
假设 <Messenger> 的作者看到了我们的 <ProgressBar> ,并想在他们的新组件中尝试一下。

No problem: 没问题

The <Messenger> renders its own <ProgressBar>, and since useContext reads from the nearest provider, its <ProgressBarLink> components update the state of its local progress bar, rather than the one in the root layout.
<Messenger> 渲染自己的 <ProgressBar> ,而 useContext 从最近的提供者读取,它的 <ProgressBarLink> 组件更新本地进度条的状态,而不是根布局中的进度条。

Very cool! 非常酷!

Framework-agnostic components
框架无关的组件

If you look at our final useProgress Hook and <ProgressBar> component, you'll notice that neither of them depend on anything from Next.js. This means that they can be used in any React app, regardless of the framework.
如果你看一下我们最终的 useProgress Hook 和 <ProgressBar> 组件,你会注意到它们都不依赖于 Next.js 的任何内容。这意味着它们可以在任何 React 应用中使用,无论使用的是哪个框架。

Even more, our Hook is robust to interruption, so it can be used to show pending UI for any state update marked as a Transition – including updates that have nothing to do with navigations.
此外,我们的 Hook 对中断具有很强的鲁棒性,因此它可以用于显示任何标记为过渡的状态更新的待处理 UI,包括与导航无关的更新。

Finally, because we were able to encapsulate all our logic and behavior inside of a React component, our <ProgressBar> is composable. Even if we start out by using it to render a "global" indicator near the root of our app, we can use it again and again throughout our tree, and none of the instances will disrupt each other.
最后,由于我们能够将所有的逻辑和行为封装在一个 React 组件中,我们的 <ProgressBar> 是可组合的。即使我们最初使用它在应用程序的根部渲染一个“全局”指示器,我们也可以在整个树中反复使用它,而且这些实例之间不会相互干扰。

It's a neat example of how React's design kept nudging us until we landed on a composable API, in spite of starting this whole journey by trying to add a single global progress bar to our app.
这是一个很好的例子,展示了 React 的设计如何不断引导我们,直到我们最终采用了可组合的 API,尽管我们最初只是试图在应用程序中添加一个全局进度条。


Transitions are incredibly powerful. If you haven't had a chance to use them, check out the docs on useTransition and play around with the interactive demos.
过渡效果非常强大。如果你还没有机会使用过它们,请查看有关 useTransition 的文档,并在交互式演示中尝试一下。

I learned a ton writing this article and came away from it feeling incredibly excited about React's future. I hope you did too – and thank you so much for reading!
我在写这篇文章的过程中学到了很多东西,对 React 的未来感到非常兴奋。希望你也有同样的感受 - 非常感谢你的阅读!

You can find the source for all the demos on GitHub.
您可以在 GitHub 上找到所有演示的源代码。

Last updated: 最后更新时间:

Get our latest in your inbox.
将我们的最新内容发送到您的收件箱。

Join our newsletter to hear about Sam and Ryan's newest blog posts, code recipes, and videos.
加入我们的通讯,了解 Sam 和 Ryan 最新的博客文章、代码配方和视频。

Drag to outliner or Upload
Close
  • Yellow
  • Blue
  • Green
  • Pink