React Router v7 已发布。 查看文档
热模块替换
本页内容

热模块替换

热模块替换是一种在无需重新加载页面的情况下更新应用程序中模块的技术。它提供了极佳的开发体验,并且 Remix 开箱即用地支持它。

值得注意的是,HMR 会尽力在更新之间保留浏览器状态。如果在模态框中有表单,并且您填写了所有字段,传统的实时重载会硬刷新页面。因此,您将丢失表单中的所有数据。每次进行更改时,您都必须再次打开模态框并再次填写表单。 😭

但是,使用 HMR,所有这些状态都可以在更新之间保留。 ✨

React 快速刷新

React 已经有通过其虚拟 DOM响应用户交互(例如单击按钮)来更新 DOM 的机制。如果 React 也能处理响应代码更改而更新 DOM,那岂不是很棒?

这正是React 快速刷新的全部内容!当然,React 的重点是组件,而不是通用的 JavaScript 代码,因此 RFR 本身仅处理导出的 React 组件的热更新。

但是 React 快速刷新确实有一些您应该注意的限制。

类组件状态

React 快速刷新不会保留类组件的状态。这包括内部返回类的高阶组件

export class ComponentA extends Component {} // ❌

export const ComponentB = HOC(ComponentC); // ❌ Won't work if HOC returns a class component

export function ComponentD() {} // ✅
export const ComponentE = () => {}; // ✅
export default function ComponentF() {} // ✅

命名函数组件

函数组件必须是命名的,而不是匿名的,以便 React 快速刷新跟踪更改

export default () => {}; // ❌
export default function () {} // ❌

const ComponentA = () => {};
export default ComponentA; // ✅

export default function ComponentB() {} // ✅

支持的导出

React 快速刷新只能处理组件导出。虽然 Remix 为您管理特殊的路由导出,例如actionheaderslinksloadermeta,但任何用户定义的导出都将导致完全重新加载

// These exports are handled by the Remix Vite plugin
// to be HMR-compatible
export const meta = { title: "Home" }; // ✅
export const links = [
  { rel: "stylesheet", href: "style.css" },
]; // ✅

// These exports are removed by the Remix Vite plugin
// so they never affect HMR
export const headers = { "Cache-Control": "max-age=3600" }; // ✅
export const loader = async () => {}; // ✅
export const action = async () => {}; // ✅

// This is not a Remix export, nor a component export,
// so it will cause a full reload for this route
export const myValue = "some value"; // ❌

export default function Route() {} // ✅

👆 无论如何,路由可能不应该导出这样的随机值。如果您想在路由之间重用值,请将它们放在它们自己的非路由模块中

export const myValue = "some value";

更改 Hooks

当向组件添加或从中删除 hook 时,React 快速刷新无法跟踪组件的更改,从而导致仅为下一次渲染进行完全重新加载。更新 hook 后,更改应再次导致热更新。例如,如果将 useLoaderData 添加到您的组件,则可能会丢失该组件的局部状态以进行该渲染。

此外,如果您正在解构 hook 的返回值,如果删除或重命名解构的键,React 快速刷新将无法为该组件保留状态。例如

export const loader = async () => {
  return json({ stuff: "some things" });
};

export default function Component() {
  const { stuff } = useLoaderData<typeof loader>();
  return (
    <div>
      <input />
      <p>{stuff}</p>
    </div>
  );
}

如果您将键 stuff 更改为 things

  export const loader = async () => {
-   return json({ stuff: "some things" })
+   return json({ things: "some things" })
  }

  export default Component() {
-   const { stuff } = useLoaderData<typeof loader>()
+   const { things } = useLoaderData<typeof loader>()
    return (
      <div>
        <input />
-       <p>{stuff}</p>
+       <p>{things}</p>
      </div>
    )
  }

那么 React 快速刷新将无法保留状态 <input /> ❌。

作为一种解决方法,您可以避免解构,而是直接使用 hook 的返回值

export const loader = async () => {
  return json({ stuff: "some things" });
};

export default function Component() {
  const data = useLoaderData<typeof loader>();
  return (
    <div>
      <input />
      <p>{data.stuff}</p>
    </div>
  );
}

现在,如果您将键 stuff 更改为 things

  export const loader = async () => {
-   return json({ stuff: "some things" })
+   return json({ things: "some things" })
  }

  export default Component() {
    const data = useLoaderData<typeof loader>()
    return (
      <div>
        <input />
-       <p>{data.stuff}</p>
+       <p>{data.things}</p>
      </div>
    )
  }

那么 React 快速刷新将为 <input /> 保留状态,但如果状态元素(例如 <input />)是已更改元素的同级元素,您可能需要使用下一节中描述的组件键。

组件 Key

在某些情况下,React 无法区分现有组件被更改和添加新组件。React 需要 key 来消除这些情况的歧义,并在修改同级元素时跟踪更改。

文档和示例在以下许可下获得许可 MIT