使用钩子动态加载 React 组件

介绍

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

您还可以使用 lazy-loading 来服务最终用户在该特定时刻所需的代码包。

React 16.6.0+ 提供React.lazyReact.Suspsense来支持懒惰加载的 React 组件,而不是导入所有组件,懒惰加载只允许您在需要时导入额外的组件。

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

前提条件

要完成本教程,您将需要:

不需要地方发展。

CodeSandbox的例子提供给进一步的实验。

动态地加载组件

Reddit 是一个网站,有多个 subreddits 用于不同的主题. 每个 subreddit 都遵循具有r/前缀的模式. 假设您正在开发一个应用程序,该应用程序显示三个 subreddits 的视图:r/reactjs,r/learnreactjsr/javascript

假设您根据属性显示一个不同的组件,‘subredditsToShow’:

 1[label src/App.js]
 2import React from 'react';
 3import shortid from 'shortid';
 4
 5import LearnReactView from './views/learnreactView';
 6import ReactView from './views/reactView';
 7import JavaScriptView from './views/javascriptView';
 8import NullView from './views/NullView';
 9
10export default function App({ subredditsToShow }) {
11  const subredditElementList = subredditsToShow.map(
12    subreddit => {
13      switch (subreddit) {
14        case 'reactjs':
15          return <ReactView key={shortid.generate()} />;
16        case 'learnreactjs':
17          return (
18            <LearnReactView key={shortid.generate()} />
19          );
20        case 'javascript':
21          return (
22            <JavaScriptView key={shortid.generate()} />
23          );
24        default:
25          return (
26            <NullView key={shortid.generate()}>
27              {`"r/${subreddit}" - not implemented`}
28            </NullView>
29          );
30      }
31    }
32  );
33
34  return <div>{subredditElementList}</div>;
35}

此代码的实例可在 CodeSandbox 中找到.

<$>[注] **注:**本示例使用 shortid来生成独特的密钥. 截至本修订, shortid 现在已被贬值, nanoid是推荐的替代方案。

在本示例中,subredditsToShowindex.js中定义为:

 1const subredditsToShow = [
 2  'reactjs',
 3  'learnreactjs',
 4  'pics',
 5  'reactjs',
 6  'learnreactjs',
 7  'svelte',
 8  'javascript',
 9  'learnreactjs'
10];

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

1[secondary_label Output]
2r/reactjs
3r/learnreactjs
4"r/pics" - not implemented
5r/reactjs
6r/learnreactjs
7"r/svelte" - not implemented
8r/javascript
9r/learnreactjs

picssvelte 没有实现,没有处理它们的案例,你的应用程序中没有该 subreddit 的单独视图组件。

这种交换/案例方法适用于少数次元,但处理额外的次元将要求您:

  • 添加新的导入 - 即使是未使用的导入
  • 更新开关组件 - 产生不可维护的代码

您可以通过每 subreddit 动态加载组件来防止这些问题,并如下所示,使用useEffectuseState删除开关声明:

 1[label src/App.js]
 2import React, { lazy, useEffect, useState } from 'react';
 3import shortid from 'shortid';
 4
 5const importView = subreddit =>
 6  lazy(() =>
 7    import(`./views/${subreddit}View`).catch(() =>
 8      import(`./views/NullView`)
 9    )
10  );
11
12export default function App({ subredditsToShow }) {
13  const [views, setViews] = useState([]);
14
15  useEffect(() => {
16    async function loadViews() {
17      const componentPromises =
18        subredditsToShow.map(async subreddit => {
19          const View = await importView(subreddit);
20          return <View key={shortid.generate()} />;
21        });
22
23      Promise.all(componentPromises).then(setViews);
24    }
25
26    loadViews();
27  }, [subredditsToShow]);
28
29  return (
30    <React.Suspense fallback='Loading views...'>
31      <div className='container'>{views}</div>
32    </React.Suspense>
33  );
34}

此代码的实例可在 CodeSandbox 中找到.

让我们把上面的代码打破一下。

  • importView 动态导入视图. 它返回一个 NullView(Nullobject pattern)为未匹配的 subreddit.
  • 然后您将组件存储在 Views 中,以便在 useEffect 中完成导入后进行渲染.
  • loadViewsuseEffect 中导入视图,并将它们存储在 setViews 状态中。

这将动态地用字符串完成负载组件,下一步您将探索一个更先进的示例。

动态地与对象加载组件

让我们考虑一个情况,您通过对应数据属性来动态地加载不同的视图

Reddit API 暴露了搜索结果的 JSON 响应. 以下是搜索 "react hooks"时的响应的例子;

 1[seconary_label Output]
 2{
 3  "data": {
 4    "children": [
 5      {
 6        "data": {
 7          "subreddit": "reactjs",
 8          "title": "New tutorial for React hook",
 9          "url": "..."
10        }
11      },
12      {
13        "data": {
14          "subreddit": "javascript",
15          "title": "React Hook Form",
16          "url": "..."
17        }
18      },
19      {
20        "data": {
21          "subreddit": "Frontend",
22          "title": "React hook examples",
23          "url": "..."
24        }
25      }
26    ]
27  }
28}

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

 1[label src/App.js]
 2import React, { lazy, useEffect, useState } from 'react';
 3import shortid from 'shortid';
 4
 5const importView = subreddit =>
 6  lazy(() =>
 7    import(`./views/${subreddit}View`).catch(() =>
 8      import(`./views/NullView`)
 9    )
10  );
11
12const searchSubreddit = async query =>
13  fetch(
14    `https://www.reddit.com/search.json?q=${query}`
15  ).then(_ => _.json());
16
17export default function App({ subredditsToShow }) {
18  const [views, setViews] = useState([]);
19
20  const extractData = response =>
21    response.data.children.map(({ data }) => data);
22
23  useEffect(() => {
24    async function loadViews() {
25      const subredditsToShow = await searchSubreddit(
26        'react hooks'
27      ).then(extractData);
28      const componentPromises = subredditsToShow.map(
29        async data => {
30          const View = await importView(data.subreddit);
31          return (
32            <View key={shortid.generate()} {...data} />
33          );
34        }
35      );
36
37      Promise.all(componentPromises).then(setViews);
38    }
39
40    loadViews();
41  }, [subredditsToShow]);
42
43  return (
44    <React.Suspense fallback='Loading views...'>
45      <div className='container'>{views}</div>
46    </React.Suspense>
47  );
48}

此代码的实例可在 CodeSandbox 中找到.

与前一部分的差异是:

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

每一个视图现在都会收到一个副本的数据作为一个支持。

以下是「reactjsView」的例子,是专为「reactjs」子路由器设计的视图:

 1[label src/views/reactjsView.js]
 2import React from 'react';
 3import Layout from './Layout';
 4import styled, { css } from 'styled-components';
 5
 6const Container = styled.article`
 7  display: flex;
 8  flex-direction: column;
 9`;
10
11export default ({ subreddit, title, url }) => (
12  <Layout
13    css={css`
14      &:hover {
15        background-color: papayawhip;
16      }
17    `}
18  >
19    <Container>
20      <h3>{title}</h3>
21      <p>{`r/${subreddit}`}</p>
22      <a href={url}>-> Visit the site</a>
23    </Container>
24  </Layout>
25);

以下是「reactjsView」的例子,它是专为「javascript」次元设计的视图:

 1[label src/views/javascriptView.js]
 2import React from 'react';
 3import Layout from './Layout';
 4import styled, { css } from 'styled-components';
 5
 6const Container = styled.article`
 7  display: flex;
 8  flex-direction: row;
 9  background-color: rgba(0, 0, 0, 0.1);
10  padding: 2rem;
11  & > * {
12    padding-left: 1rem;
13  }
14`;
15
16export default ({ subreddit, title, url }) => (
17  <Layout
18    css={css`
19      background-color: papayawhip;
20    `}
21  >
22    <Container>
23      <h4>{title}</h4>
24      <p>({`r/${subreddit}`})</p>
25      <a href={url}>-> Visit the site</a>
26    </Container>
27  </Layout>
28);

这两种视图都可以使用由数据对象提供的subreddit,titleurl

这结束了与对象动态地加载组件。

按需装载部件

在上面的示例中,您已自动加载组件,而无需性能改进。

您可以通过在用户执行操作时仅在需要时发送JavaScript来改进此功能。

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

 1const data = [
 2  {
 3    id: 'php',
 4    label: 'php',
 5    value: 372,
 6    color: 'hsl(233, 70%, 50%)'
 7  },
 8  {
 9    id: 'scala',
10    label: 'scala',
11    value: 363,
12    color: 'hsl(15, 70%, 50%)'
13  },
14  {
15    id: 'go',
16    label: 'go',
17    value: 597,
18    color: 'hsl(79, 70%, 50%)'
19  },
20  {
21    id: 'css',
22    label: 'css',
23    value: 524,
24    color: 'hsl(142, 70%, 50%)'
25  },
26  {
27    id: 'hack',
28    label: 'hack',
29    value: 514,
30    color: 'hsl(198, 70%, 50%)'
31  }
32];

您可以更快地加载网站,而不需要发送未使用的JavaScript,并只在需要时加载图表:

 1import React, { lazy, useState } from 'react';
 2import shortid from 'shortid';
 3
 4const importView = chartName =>
 5  lazy(() =>
 6    import(`./charts/${chartName}`)
 7      .catch(() => import(`./charts/NullChart`))
 8  );
 9
10const data = [ ... ];
11
12const ChartList = ({ charts }) =>
13  Object.values(charts).map(Chart => (
14    <Chart key={shortid.generate()} data={data} />
15  ));
16
17export default function App() {
18  const [charts, setCharts] = useState({});
19
20  const addChart = chartName => {
21    if (charts[chartName]) return;
22
23    const Chart = importView(chartName);
24    setCharts(c => ({ ...c, [chartName]: Chart }));
25  };
26
27  const loadPieChart = () => addChart('Pie');
28  const loadWaffleChart = () => addChart('Waffle');
29
30  return (
31    <main>
32      <section className="container">
33        <button disabled={charts['Pie']}
34                onClick={loadPieChart}>
35          Pie Chart
36        </button>
37        <button disabled={charts['Waffle']}
38                onClick={loadWaffleChart}>
39          Waffle Chart
40        </button>
41      </section>
42      <section className="container">
43        <React.Suspense fallback="Loading charts...">
44          <div className="row">
45            <ChartList charts={charts} />
46          </div>
47        </React.Suspense>
48      </section>
49    </main>
50  );
51}

此代码的实例可在 CodeSandbox 中找到.

  • importView 与以前的示例相同,除了组件位置
  • ChartList 重复一个对象,名称是图名,值是导入的组件
  • App 状态, charts,是一个对象,用于追踪已经加载的组件
  • addChart 以动态方式以名义导入图表,并将图表添加到 charts 状态,这就是您渲染的值
  • load PieChartloadWaffleChart 是便利方法,您可以用 [Memouse(INKL0]]] 回忆。

「src/charts/Pie.js」和「src/charts/Waffle.js」仅在用户点击相应按钮时才能加载。

这结束了按需装载组件的React.lazyReact.Suspense

结论

在本文中,您介绍了动态装载部件和按需装载部件,这些技术可以帮助改善维护和性能。

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

Published At
Categories with 技术
Tagged with
comments powered by Disqus