使用 RxJS 和 <$> 片段在 React 中获取数据
我们经常需要在组件中获取数据。以下是使用 useState hook 和fetch API获取并显示数据的示例:
function App(){
const [data, setData] = useState(null);
// useEffect for fetching data on mount
useEffect(() => {
fetch('//...')
.then(response => response.json())
.then(data => setData(data));
}, []);
return <div>Data: { data }</div>
}
response.ok
为了简洁起见, 我将在这里和以后不再检查
看起来还好吗?
嗯,这种方法缺少一些重要特征:
- 取消组件卸载时的获取(例如,如果用户离开当前页面)
- 处理错误
- 显示加载指示器
为了很好地处理所有这些问题,我们将使用 RxJS!
RxJS是一款非常强大的工具,用于管理和协调异步事件(例如获取和 UI 事件)。学习它会让你受益匪浅!
请不要惊慌,我会指导您如何添加和使用它🙂
tl;dr: 生成的应用程序游乐场和<$> fragment 库
让我们开始更新我们的应用程序以使用 RxJS!
🔋 加电
首先,我们将切换到 RxJS 的fromFetch — 它是原生 fetch的包装器:
function App(){
const [data, setData] = useState(null);
useEffect(() => {
fromFetch('//...')
.subscribe(response =>
response.json().then(data => setData(data))
);
}, []);
return <div>Data: { data }</div>
}
.subscribe
方法是 Promises 中的类似物.then
——它将从 RxJS 流接收值更新(目前它只能处理一个更新,但会有更多)
还.subscribe
返回一个对象,我们可以用它来取消“订阅”。这将帮助我们解决第一个问题:在组件卸载时取消获取。
function App(){
const [data, setData] = useState(null);
useEffect(() => {
const subscription = fromFetch('//...')
.subscribe(response =>
response.json().then(data => setData(data))
);
// this function will be called on component unmount
// it will terminate the fetching
return () => subscription.unsubscribe();
}, []);
return <div>Data: { data }</div>
}
有关详细信息,请参阅 React 的useEffect#cleaning-up-an-effect文档部分
好哇:已完成 1 个,还剩 2 个!
在继续之前,我们先来做一点清理工作:
🔧 重构和 <$> 片段
如您所见,我们在函数response.json()
内部使用异步操作subscribe
——这是一种不好的做法,原因如下:如果我们已经处于response.json()
解析阶段,则此流将不可重用,并且取消将不起作用。
我们将使用mergeMap
RxJS 操作符来解决这个问题:
function App(){
const [data, setData] = useState(null);
useEffect(() => {
const subscription = fromFetch('//...')
.pipe(
// mergeMap is an operator to do another async task
mergeMap(response => response.json())
)
.subscribe(data => setData(data));
return () => subscription.unsubscribe();
}, []);
return <div>Data: { data }</div>
}
更新:@benlesh提出了一个很好的观点,可以使用 RxJS 的ajax.getJSON来代替 fetch 包装器,并且跳过mergeMap
。例如: 。出于教育和懒惰的原因,ajax.getJSON(url).subscribe(/* etc. */)
我会保留这种fromFetch
方法🙂
我们已经将response.json()
操作与结果处理分离。现在我们的subscribe
处理程序只负责显示数据——我们现在可以使用<$>
fragment 了!
<$> — 是一个小型(1Kb)包,用于在我们的 React 组件中显示 RxJS 值。
它会为我们订阅提供的流并显示更新。它还会在组件卸载时取消订阅,所以我们也不用担心这个!
function App(){
// we need useMemo to ensure stream$ persist
// between App re-renders
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json())
)
, []);
return <div>Data: <$>{ stream$ }</$></div>
}
请注意,我们已经删除了useState
并且.subscribe
:<$>完成了所有这些!
现在,我们准备添加更多运算符来继续解决我们的任务。让我们添加一个加载指示器!
⏳ 加载指示器
function App(){
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json()),
// immediately show a loading text
startWith('loading...')
)
, []);
return <div>Data: <$>{ stream$ }</$></div>
}
startWith
将在异步数据流前面添加提供的值。在我们的例子中,它看起来有点像这样:
start -o---------------------------o- end
^ show 'loading' ^ receive and display
| immediately | response later
太棒了:已完成 2 个,还剩 1 个!
接下来我们将处理错误:
⚠️ 错误处理
另一个操作符catchError
可以让我们处理获取时的错误:
function App(){
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json()),
catchError(() => of('ERROR')),
startWith('loading...')
)
, []);
return <div>Data: <$>{ stream$ }</$></div>
}
现在,如果获取失败 - 我们将显示“错误”文本。
如果您想深入了解,我写了一篇关于管理错误的详细文章:“RxJS 中的错误处理或如何使用 Observables 避免失败” ——抑制、战略性回退、简单重试和指数延迟——一切都在那里。
已完成 3 个,剩余 0 个!
让我们最后移动一些div
s :
🖼 更好的用户界面
我们很可能希望正确显示高亮的错误,并设置样式(甚至动画)的加载指示。为此,我们只需将 JSX 代码直接移到流中即可:
function App(){
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json()),
// now we'll map not only to text
// but to JSX
map(data => <div className="data">Data: { data }</div>),
catchError(() => of(<div className="err">ERROR</div>)),
startWith(<div className="loading">loading...</div>)
)
, []);
return <$>{ stream$ }</$>
}
请注意,现在我们可以完全自定义每个状态的视图!
🍰 额外功能:防闪烁
有时,如果响应太快,我们会看到加载指示器闪烁一瞬间。这通常是不受欢迎的,因为我们花了很长时间来制作加载指示器动画,并且希望确保用户能够看到它🙂
为了解决这个问题,我们将分离出 Observable 的创建和加入延迟 500ms 的获取:
function App(){
const stream$ = useMemo(() =>
customFetch('//...').pipe(
map(data => <div className="data">Data: { data }</div>),
catchError(() => of(<div className="err">ERROR</div>)),
startWith(<div className="loading">loading...</div>)
)
, []);
return <$>{ stream$ }</$>
}
function customFetch(URL) {
// wait for both fetch and a 500ms timer to finish
return zip(
fromFetch(URL).pipe( mergeMap(r => r.json()) ),
timer(500) // set a timer for 500ms
).pipe(
// then take only the first value (fetch result)
map(([data]) => data)
)
}
现在我们亲爱的用户将看到至少 500 毫秒的加载动画!
完成 4 个,还剩🍰!
最后说几句:
🎉 结尾
如果您想试用一下,这就是我们最终的应用程序。
要在 React 组件中开始使用 RxJS,只需执行以下操作:
npm i rxjs react-rxjs-elements
然后将流放入其中<$>
:
import { timer } from 'rxjs';
import { $ } from 'react-rxjs-elements';
function App() {
return <$>{ timer(0, 1000) } ms</$>
}
就是这样,我希望你学到了一些新东西!
感谢您阅读本文!保持关注,祝您拥有美好的一天🙂
如果您喜欢阅读 — — 请用 ❤️ 🦄 📘 按钮表示
在 Twitter 上关注我,了解更多 React、RxJS 和 JS 帖子:
结束
感谢@niklas_wortmann和@sharlatta的审核!
鏂囩珷鏉ユ簮锛�https://dev.to/rxjs/fetching-data-in-react-with-rxjs-and-lt-gt-fragment-54h7