React Native 3:useEffect 自动请求接口 + 搜索功能 + 封装 Loading 组件

文章来源声明: 原文作者:是你的小橘呀; 来源站点:掘金; 原文链接:https://juejin.cn/post/7691141092238131240; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合正在学习 React Native 函数组件与 Hooks 的开发者,内容从踩坑到封装循序渐进,能快速掌握挂载请求、实时搜索和 Loading 复用,实用性强。

本篇是 React Native 学习系列,我们一次性完成三件事:
  1. 使用 useEffect 实现页面打开自动请求接口,解决无限循环坑
  2. 利用 useEffect 依赖项,实现输入框实时搜索课程
  3. 封装公共 Loading 加载组件,优化网络请求用户体验

前置:Expo 项目,本地后端接口 http://localhost:3000/search,支持 q 查询参数做搜索。

一、useEffect:页面挂载自动请求接口

之前我们写的案例,需要点击按钮才触发请求。真实 App,打开页面就要自动加载数据。

如果你写过 Vue,初始化请求会放在 onMounted;在 React / React Native 中对应的就是 useEffect 这个 Hook。 useEffect 能力很强,可以模拟 Vue 的 onMounted、onUpdated,甚至 watch,最常用场景就是组件挂载后请求接口。

1.1 基础写法(会踩坑版本)

先引入 Hook:

<span>import</span> { useState, useEffect } <span>from</span> <span>'react'</span>;

把接口请求函数放进 useEffect:

<span>const</span> [courses, setCourses] = <span>useState</span>([]);

<span>const</span> <span>fetchData</span> = <span>async</span> (<span></span>) => {
  <span>const</span> res = <span>await</span> <span>fetch</span>(<span>'http://localhost:3000/search'</span>);
  <span>const</span> { data } = <span>await</span> res.<span>json</span>();
  <span>setCourses</span>(data.<span>courses</span>);
};

<span>// ❌ 不写依赖数组,会无限循环</span>
<span>useEffect</span>(<span>() =></span> {
  <span>fetchData</span>();
});

渲染列表,删掉按钮:

<span>return</span> (
  <span><span><<span>View</span> <span>style</span>=<span>{styles.container}</span>></span>
    {courses.map((course) => (
      <span><<span>Text</span> <span>key</span>=<span>{course.id}</span>></span>{course.name}<span></<span>Text</span>></span>
    ))}
  <span></<span>View</span>></span></span>
);

运行后打开调试工具(终端按j),你会发现接口疯狂重复请求,无限循环。

问题根源

useEffect 默认规则:组件每一次重新渲染,都会执行 useEffect

  1. 组件初次渲染 → 执行 useEffect → 请求接口
  2. setCourses 修改 state → 组件重渲染
  3. 重渲染后再次执行 useEffect → 再次请求,循环往复

✅ 解决方案:给 useEffect 第二个参数,**空依赖数组 **[]

<span>// ✅ 空数组:仅组件挂载时执行一次</span>
<span>useEffect</span>(<span>() =></span> {
  <span>fetchData</span>();
}, []);

核心知识点:

  • 不写第二个参数:每次组件渲染都会执行
  • [] 空数组:只在组件首次挂载执行一次

完整基础版 App.js

<span>import</span> { <span>StyleSheet</span>, <span>Text</span>, <span>View</span> } <span>from</span> <span>'react-native'</span>;
<span>import</span> { useState, useEffect } <span>from</span> <span>'react'</span>;

<span>export</span> <span>default</span> <span>function</span> <span>App</span>(<span></span>) {
  <span>const</span> [courses, setCourses] = <span>useState</span>([]);

  <span>const</span> <span>fetchData</span> = <span>async</span> (<span></span>) => {
    <span>const</span> res = <span>await</span> <span>fetch</span>(<span>'http://localhost:3000/search'</span>);
    <span>const</span> { data } = <span>await</span> res.<span>json</span>();
    <span>setCourses</span>(data.<span>courses</span>);
  };

  <span>useEffect</span>(<span>() =></span> {
    <span>fetchData</span>();
  }, []);

  <span>return</span> (
    <span><span><<span>View</span> <span>style</span>=<span>{styles.container}</span>></span>
      {courses.map((course) => (
        <span><<span>Text</span> <span>key</span>=<span>{course.id}</span>></span>{course.name}<span></<span>Text</span>></span>
      ))}
    <span></<span>View</span>></span></span>
  );
}

<span>const</span> styles = <span>StyleSheet</span>.<span>create</span>({
  <span>container</span>: {
    <span>flex</span>: <span>1</span>,
    <span>backgroundColor</span>: <span>'#fff'</span>,
    <span>alignItems</span>: <span>'center'</span>,
    <span>justifyContent</span>: <span>'center'</span>,
  },
});

二、useEffect 依赖项:实现输入框实时搜索

学会挂载请求,我们进阶:利用依赖项实现搜索。

2.1 接口说明

后端接口支持查询参数 q

  • http://localhost:3000/search?q=node → node 相关课程
  • http://localhost:3000/search?q=react → react 相关课程

2.2 TextInput 输入框组件

React Native 使用 TextInput 实现输入框。

  • onChangeText:监听输入变化,拿到输入文本
  • placeholder:输入框提示文字
<span>import</span> { <span>TextInput</span>, <span>StyleSheet</span>, <span>Text</span>, <span>View</span> } <span>from</span> <span>'react-native'</span>;
<span>import</span> { useState, useEffect } <span>from</span> <span>'react'</span>;

<span>export</span> <span>default</span> <span>function</span> <span>App</span>(<span></span>) {
  <span>const</span> [courses, setCourses] = <span>useState</span>([]);
  <span>// 搜索关键词state</span>
  <span>const</span> [keyword, setKeyword] = <span>useState</span>(<span>''</span>);

  <span>const</span> <span>fetchData</span> = <span>async</span> (<span></span>) => {
    <span>// 把keyword拼接到url参数</span>
    <span>const</span> res = <span>await</span> <span>fetch</span>(<span>`http://localhost:3000/search?q=<span>${keyword}</span>`</span>);
    <span>const</span> { data } = <span>await</span> res.<span>json</span>();
    <span>setCourses</span>(data.<span>courses</span>);
  };

  <span>// 🎯 依赖项设置为keyword</span>
  <span>useEffect</span>(<span>() =></span> {
    <span>fetchData</span>();
  }, [keyword]);

  <span>return</span> (
    <span><span><<span>View</span> <span>style</span>=<span>{styles.container}</span>></span>
      <span><<span>Text</span>></span>您搜索的关键词是:{keyword}<span></<span>Text</span>></span>
      <span><<span>TextInput</span>
        <span>style</span>=<span>{styles.input}</span>
        <span>placeholder</span>=<span>"请填写要搜索的课程!"</span>
        <span>onChangeText</span>=<span>{(text)</span> =></span> setKeyword(text)}
        defaultValue={keyword}
      />
      {courses.map((course) => (
        <span><<span>Text</span> <span>key</span>=<span>{course.id}</span>></span>{course.name}<span></<span>Text</span>></span>
      ))}
    <span></<span>View</span>></span></span>
  );
}

<span>const</span> styles = <span>StyleSheet</span>.<span>create</span>({
  <span>container</span>: {
    <span>flex</span>: <span>1</span>,
    <span>backgroundColor</span>: <span>'#fff'</span>,
    <span>alignItems</span>: <span>'center'</span>,
    <span>justifyContent</span>: <span>'center'</span>,
  },
  <span>input</span>: {
    <span>height</span>: <span>40</span>,
    <span>width</span>: <span>300</span>,
    <span>margin</span>: <span>12</span>,
    <span>padding</span>: <span>10</span>,
    <span>borderWidth</span>: <span>1</span>,
    <span>borderColor</span>: <span>'#ccc'</span>,
    <span>borderRadius</span>: <span>5</span>,
  },
});

  • placeholder,就和HTML里表单的一样,是用来显示提示信息的。
  • onChangeText,看名字就知道了,和JS里的onchange事件一个意思。
  • 当输入框的数据改变了,可以用text来获取到用户填写的内容。
  • 再调用setKeyword将用户填写的内容,设置给keyword状态。
  • defaultValue就是表单里的默认值。
  • 为了确认到底获取到了没有?可以在最上面,加个显示keyword的代码。

image.png

2.3 依赖项原理

image.png

useEffect 第二个数组就是依赖项数组:

数组里面的变量一旦发生改变,useEffect 就自动重新执行。

这里我们写 [keyword]: 用户打字 → keyword 更新 → 触发 useEffect → 重新请求接口,刷新列表。 打开调试网络面板可以看到:每输入一个字符就发起一次请求。

知识点汇总

  1. 无第二个参数:每次渲染执行
  2. []:仅挂载执行一次
  3. [xxx]:xxx 变量改变,自动执行副作用

三、封装 Loading 加载组件,优化请求体验

网络请求是异步耗时操作,数据回来之前,页面空白会很影响体验。我们加上 loading 转圈动画。

image.png

3.1 增加 loading 状态

新增 loading state,初始值为 true,页面打开默认加载中。 使用 try...finally,无论请求成功 / 失败,finally 都会执行,关闭 loading。

<span>const</span> [loading, setLoading] = <span>useState</span>(<span>true</span>);

<span>const</span> <span>fetchData</span> = <span>async</span> (<span></span>) => {
  <span>try</span> {
    <span>const</span> res = <span>await</span> <span>fetch</span>(<span>`http://localhost:3000/search?q=<span>${keyword}</span>`</span>);
    <span>const</span> { data } = <span>await</span> res.<span>json</span>();
    <span>setCourses</span>(data.<span>courses</span>);
  } <span>finally</span> {
    <span>setLoading</span>(<span>false</span>);
  }
};

  • 给里面的内容,加上了try...fianlly。
  • 当请求完成后,在finally里面,将加载中状态设置为false。
  • 因为读取的是本地接口,请求的速度会非常非常快,会导致根本看不到什么效果。
  • 我们可以在请求之前后,加上个延迟 2 秒,便于观察效果。

3.2 ActivityIndicator 原生加载指示器

React Native 内置 ActivityIndicator,原生转圈加载动画。 增加判断:如果loading=true,直接返回加载组件,不再渲染搜索和列表。

3.3 抽离公共 Loading 组件

加载组件项目多个页面都会使用,直接抽成独立组件,避免重复代码。

新建文件 components/shared/Loading.js

<span>import</span> { <span>ActivityIndicator</span>, <span>StyleSheet</span> } <span>from</span> <span>'react-native'</span>;

<span>/**
 * 全局Loading加载组件
 */</span>
<span>export</span> <span>default</span> <span>function</span> <span>Loading</span>(<span></span>) {
  <span>return</span> <span><span><<span>ActivityIndicator</span> <span>size</span>=<span>"small"</span> <span>color</span>=<span>"#1f99b0"</span> <span>style</span>=<span>{styles.loading}</span> /></span></span>;
}

<span>const</span> styles = <span>StyleSheet</span>.<span>create</span>({
  <span>loading</span>: {
    <span>backgroundColor</span>: <span>'#fff'</span>,
    <span>position</span>: <span>'absolute'</span>,
    <span>top</span>: <span>0</span>,
    <span>left</span>: <span>0</span>,
    <span>right</span>: <span>0</span>,
    <span>bottom</span>: <span>0</span>,
    <span>zIndex</span>: <span>1</span>,
  },
});

3.4 最终完整版 App.js

<span>import</span> { <span>TextInput</span>, <span>StyleSheet</span>, <span>Text</span>, <span>View</span> } <span>from</span> <span>'react-native'</span>;
<span>import</span> { useState, useEffect } <span>from</span> <span>'react'</span>;
<span>import</span> <span>Loading</span> <span>from</span> <span>'./components/shared/Loading'</span>;

<span>export</span> <span>default</span> <span>function</span> <span>App</span>(<span></span>) {
  <span>const</span> [courses, setCourses] = <span>useState</span>([]);
  <span>const</span> [keyword, setKeyword] = <span>useState</span>(<span>''</span>);
  <span>const</span> [loading, setLoading] = <span>useState</span>(<span>true</span>);

  <span>const</span> <span>fetchData</span> = <span>async</span> (<span></span>) => {
    <span>try</span> {
      <span>const</span> res = <span>await</span> <span>fetch</span>(<span>`http://localhost:3000/search?q=<span>${keyword}</span>`</span>);
      <span>const</span> { data } = <span>await</span> res.<span>json</span>();
      <span>setCourses</span>(data.<span>courses</span>);
    } <span>finally</span> {
      <span>setLoading</span>(<span>false</span>);
    }
  };

  <span>useEffect</span>(<span>() =></span> {
    <span>fetchData</span>();
  }, [keyword]);

  <span>// 加载中渲染Loading组件</span>
  <span>if</span> (loading) {
    <span>return</span> <span><span><<span>Loading</span> /></span></span>;
  }

  <span>return</span> (
    <span><span><<span>View</span> <span>style</span>=<span>{styles.container}</span>></span>
      <span><<span>Text</span>></span>您搜索的关键词是:{keyword}<span></<span>Text</span>></span>
      <span><<span>TextInput</span>
        <span>style</span>=<span>{styles.input}</span>
        <span>placeholder</span>=<span>"请填写要搜索的课程!"</span>
        <span>onChangeText</span>=<span>{(text)</span> =></span> setKeyword(text)}
        defaultValue={keyword}
      />
      {courses.map((course) => (
        <span><<span>Text</span> <span>key</span>=<span>{course.id}</span>></span>{course.name}<span></<span>Text</span>></span>
      ))}
    <span></<span>View</span>></span></span>
  );
}

<span>const</span> styles = <span>StyleSheet</span>.<span>create</span>({
  <span>container</span>: {
    <span>flex</span>: <span>1</span>,
    <span>backgroundColor</span>: <span>'#fff'</span>,
    <span>alignItems</span>: <span>'center'</span>,
    <span>justifyContent</span>: <span>'center'</span>,
  },
  <span>input</span>: {
    <span>height</span>: <span>40</span>,
    <span>width</span>: <span>300</span>,
    <span>margin</span>: <span>12</span>,
    <span>padding</span>: <span>10</span>,
    <span>borderWidth</span>: <span>1</span>,
    <span>borderColor</span>: <span>'#ccc'</span>,
    <span>borderRadius</span>: <span>5</span>,
  },
});

⚠️ 踩坑提醒:Android 模拟器访问 localhost 会请求失败,接口地址要替换成电脑本机 IP。ActivityIndicator 在 iOS 和 Android 原生样式略有差异,属于正常现象。

总结

  1. useEffect 是 RN 函数组件处理副作用的核心,依赖数组是灵魂

    • 空数组 []:挂载执行一次
    • 写入变量:变量变更,自动执行请求
  2. TextInput 实现输入框,onChangeText 监听输入内容,配合 useEffect 依赖项轻松实现实时搜索

  3. 网络请求增加 loading 状态,使用ActivityIndicator;通用 UI 抽离为独立组件,减少重复代码,提升复用性