使用钩子动态加载 React 组件

介绍

动态加载组件是一种可以替代import许多组件写入的技术您可以为组件的路径使用动态值,而不是声明可以使用的每个可能的组件。

您还可以使用延迟加载来为特定时刻的最终用户提供所需的代码包。最终用户的较小包大小应该会导致性能改进。

React 16.6.0+ 提供React.lazyReact.Suspsense支持延迟加载 React 组件。import延迟加载将允许您仅import在需要时加载其他组件,而不是加载所有组件

在本文中,您将探索如何动态加载组件的概念。您还将探索如何按需加载组件的概念。

先决条件

要完成本教程,您需要:

不需要本地开发。

提供了CodeSandbox示例以供进一步实验。

动态加载组件

Reddit 是一个网站,具有针对不同主题的多个subreddit每个 subreddit 都遵循具有r/前缀的模式假设你正在开发一个应用程序,三个subreddits显示访问量:r/reactjsr/learnreactjs,和r/javascript

假设您根据属性显示不同的组件subredditsToShow

源代码/App.js
import React from 'react';
import shortid from 'shortid';

import LearnReactView from './views/learnreactView';
import ReactView from './views/reactView';
import JavaScriptView from './views/javascriptView';
import NullView from './views/NullView';

export default function App({ subredditsToShow }) {
  const subredditElementList = subredditsToShow.map(
    subreddit => {
      switch (subreddit) {
        case 'reactjs':
          return <ReactView key={shortid.generate()} />;
        case 'learnreactjs':
          return (
            <LearnReactView key={shortid.generate()} />
          );
        case 'javascript':
          return (
            <JavaScriptView key={shortid.generate()} />
          );
        default:
          return (
            <NullView key={shortid.generate()}>
              {`"r/${subreddit}" - not implemented`}
            </NullView>
          );
      }
    }
  );

  return <div>{subredditElementList}</div>;
}

CodeSandbox 上提供了此代码的实时示例。

注意:此示例用于shortid生成唯一键。在此修订版中,shortid现已弃用,nanoid是推荐的替代方案。

在本例中,subredditsToShow定义index.js为:

const subredditsToShow = [
  'reactjs',
  'learnreactjs',
  'pics',
  'reactjs',
  'learnreactjs',
  'svelte',
  'javascript',
  'learnreactjs'
];

运行此应用程序时,您将观察到:

Output
r/reactjs r/learnreactjs "r/pics" - not implemented r/reactjs r/learnreactjs "r/svelte" - not implemented r/javascript r/learnreactjs

pics并且svelte没有实施。没有case处理它们,并且在您的应用程序中没有针对该 subreddit 的单独视图组件。NullView显示这些subreddits。

这种switch/case方法适用于少数 subreddit。但是,处理额外的 subreddits 需要您:

  • 添加新的导入 – 甚至导入未使用的导入。
  • 更新开关组件 – 产生不可维护的代码。

您可以通过按 subreddit 动态加载组件并删除 switch 语句来防止这些问题,如下所示,使用useEffectuseState

源代码/App.js
import React, { lazy, useEffect, useState } from 'react';
import shortid from 'shortid';

const importView = subreddit =>
  lazy(() =>
    import(`./views/${subreddit}View`).catch(() =>
      import(`./views/NullView`)
    )
  );

export default function App({ subredditsToShow }) {
  const [views, setViews] = useState([]);

  useEffect(() => {
    async function loadViews() {
      const componentPromises =
        subredditsToShow.map(async subreddit => {
          const View = await importView(subreddit);
          return <View key={shortid.generate()} />;
        });

      Promise.all(componentPromises).then(setViews);
    }

    loadViews();
  }, [subredditsToShow]);

  return (
    <React.Suspense fallback='Loading views...'>
      <div className='container'>{views}</div>
    </React.Suspense>
  );
}

CodeSandbox 上提供了此代码的实时示例。

让我们分解上面的代码。

  • importView动态导入视图。为不匹配的对象返回一个NullView空对象模式subreddit
  • 然后在views完成导入后将组件存储在中以进行渲染useEffect
  • loadViewsinsideuseEffect导入视图并将它们存储在状态中setViews
  • 最后,您需要使用Suspense包装视图并带有回退,以显示其中的组件何时views加载。

这将使用字符串动态地结束加载组件。接下来您将探索更高级的示例。

使用对象动态加载组件

让我们考虑一种情况,您通过匹配数据属性来动态加载不同的“视图”。

Reddit API 公开搜索结果的 JSON 响应。这是搜索“react hooks”时的响应示例

[seconary_label Output]
{
  "data": {
    "children": [
      {
        "data": {
          "subreddit": "reactjs",
          "title": "New tutorial for React hook",
          "url": "..."
        }
      },
      {
        "data": {
          "subreddit": "javascript",
          "title": "React Hook Form",
          "url": "..."
        }
      },
      {
        "data": {
          "subreddit": "Frontend",
          "title": "React hook examples",
          "url": "..."
        }
      }
    ]
  }
}

您可以subreddit通过仅加载已实现的视图来处理不同的s:

源代码/App.js
import React, { lazy, useEffect, useState } from 'react';
import shortid from 'shortid';

const importView = subreddit =>
  lazy(() =>
    import(`./views/${subreddit}View`).catch(() =>
      import(`./views/NullView`)
    )
  );

const searchSubreddit = async query =>
  fetch(
    `https://www.reddit.com/search.json?q=${query}`
  ).then(_ => _.json());

export default function App({ subredditsToShow }) {
  const [views, setViews] = useState([]);

  const extractData = response =>
    response.data.children.map(({ data }) => data);

  useEffect(() => {
    async function loadViews() {
      const subredditsToShow = await searchSubreddit(
        'react hooks'
      ).then(extractData);
      const componentPromises = subredditsToShow.map(
        async data => {
          const View = await importView(data.subreddit);
          return (
            <View key={shortid.generate()} {...data} />
          );
        }
      );

      Promise.all(componentPromises).then(setViews);
    }

    loadViews();
  }, [subredditsToShow]);

  return (
    <React.Suspense fallback='Loading views...'>
      <div className='container'>{views}</div>
    </React.Suspense>
  );
}

CodeSandbox 上提供了此代码的实时示例。

与上一节的不同之处在于:

  • 您现在正在处理一个对象, data, 而不是subreddit字符串。
  • 您正在将数据传递给每个动态视图 <View key={shortid.generate()} {...data} />

每个视图现在都获得一个副本data作为道具。

下面是一个例子reactjsView这是一个专为reactjssubreddit设计的视图

src/views/reactjsView.js
import React from 'react';
import Layout from './Layout';
import styled, { css } from 'styled-components';

const Container = styled.article`
  display: flex;
  flex-direction: column;
`;

export default ({ subreddit, title, url }) => (
  <Layout
    css={css`
      &:hover {
        background-color: papayawhip;
      }
    `}
  >
    <Container>
      <h3>{title}</h3>
      <p>{`r/${subreddit}`}</p>
      <a href={url}>-> Visit the site</a>
    </Container>
  </Layout>
);

下面是一个例子reactjsView这是一个专为javascriptsubreddit设计的视图

源代码/视图/javascriptView.js
import React from 'react';
import Layout from './Layout';
import styled, { css } from 'styled-components';

const Container = styled.article`
  display: flex;
  flex-direction: row;
  background-color: rgba(0, 0, 0, 0.1);
  padding: 2rem;
  & > * {
    padding-left: 1rem;
  }
`;

export default ({ subreddit, title, url }) => (
  <Layout
    css={css`
      background-color: papayawhip;
    `}
  >
    <Container>
      <h4>{title}</h4>
      <p>({`r/${subreddit}`})</p>
      <a href={url}>-> Visit the site</a>
    </Container>
  </Layout>
);

这两个视图可以使用subreddittitleurl通过所提供的data对象。

这结束了使用对象动态加载组件。

按需加载组件

在前面的示例中,您已经自动加载了组件,而没有提高性能。

您可以通过仅在用户执行操作时需要时发送 JavaScript 来改进这一点。

假设您需要为以下数据显示不同类型的图表:

const data = [
  {
    id: 'php',
    label: 'php',
    value: 372,
    color: 'hsl(233, 70%, 50%)'
  },
  {
    id: 'scala',
    label: 'scala',
    value: 363,
    color: 'hsl(15, 70%, 50%)'
  },
  {
    id: 'go',
    label: 'go',
    value: 597,
    color: 'hsl(79, 70%, 50%)'
  },
  {
    id: 'css',
    label: 'css',
    value: 524,
    color: 'hsl(142, 70%, 50%)'
  },
  {
    id: 'hack',
    label: 'hack',
    value: 514,
    color: 'hsl(198, 70%, 50%)'
  }
];

您可以更快地加载站点而无需发送未使用的 JavaScript 并仅在需要时加载图表:

import React, { lazy, useState } from 'react';
import shortid from 'shortid';

const importView = chartName =>
  lazy(() =>
    import(`./charts/${chartName}`)
      .catch(() => import(`./charts/NullChart`))
  );

const data = [ ... ];

const ChartList = ({ charts }) =>
  Object.values(charts).map(Chart => (
    <Chart key={shortid.generate()} data={data} />
  ));

export default function App() {
  const [charts, setCharts] = useState({});

  const addChart = chartName => {
    if (charts[chartName]) return;

    const Chart = importView(chartName);
    setCharts(c => ({ ...c, [chartName]: Chart }));
  };

  const loadPieChart = () => addChart('Pie');
  const loadWaffleChart = () => addChart('Waffle');

  return (
    <main>
      <section className="container">
        <button disabled={charts['Pie']}
                onClick={loadPieChart}>
          Pie Chart
        </button>
        <button disabled={charts['Waffle']}
                onClick={loadWaffleChart}>
          Waffle Chart
        </button>
      </section>
      <section className="container">
        <React.Suspense fallback="Loading charts...">
          <div className="row">
            <ChartList charts={charts} />
          </div>
        </React.Suspense>
      </section>
    </main>
  );
}

CodeSandbox 上提供了此代码的实时示例。

  • importView 除了组件位置之外,与前面的示例中的相同。
  • ChartList 迭代一个对象,其中名称是图表名称,值是导入的组件。
  • App状态,charts是一个目的是跟踪已经加载组件。
  • addChart按名称动态导入图表并添加到charts状态,这就是您渲染的内容。
  • loadPieChartloadWaffleChart是方便的方法,您可以使用useMemo 进行记忆
  • return呈现两个按钮,您需要使用Suspense.

src/charts/Pie.js并且src/charts/Waffle.js仅在用户单击相应按钮时加载。

这结束了使用React.lazy按需加载组件React.Suspense

结论

在本文中,您了解了动态加载组件和按需加载组件。这些技术可以帮助改进维护和性能。

如果您想了解有关 React 的更多信息,请查看我们的 React 主题页面以获取练习和编程项目。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁