热模块替换
本页内容

热模块替换

热模块替换是一种在应用程序中更新模块而无需重新加载页面的技术。它是一个很棒的开发者体验,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";

更改钩子

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

此外,如果您正在解构钩子的返回值,如果解构的键被删除或重命名,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 /> ❌ 的状态。

作为解决方法,您可以避免解构,而是直接使用钩子的返回值。

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 />)的兄弟节点,您可能需要使用下一节中描述的组件键。

组件键

在某些情况下,React 无法区分正在更改的现有组件和正在添加的新组件。React 需要 keys 来区分这些情况,并在修改兄弟节点时跟踪更改。

文档和示例已获得 MIT