文章系翻译,原文见
阅读原文
你肯定看过(或写过)这样的渲染模式:
-
通过
AJAX请求数据时渲染一个loading占位图标 -
当数据返回后重新渲染组件
让我们一个使用Fetch API的简单例子:
import React, { useState, useEffect } from 'react';
const SomeComponent = (props) => {
const [someData, setSomeData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/some-data')
.then(response => response.json())
.then(data => setSomeData(data))
.catch(error => setError(error))
.finally(() => setLoading(false));
}, []);
return (
<React.Fragment>
{loading && <div>{'Loading...'}</div>}
{!loading && error && <div>{`Error: ${error}`}</div>}
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>}
</React.Fragment>
);
};
当我们的应用逐渐庞大,假设有n个组件要使用同样的数据。
为了减少重复请求,我决定使用LocalStorage缓存服务端数据。
这是否意味着同样的渲染逻辑要重复写n次呢?
解耦数据请求
怎么可能,让我们将数据请求部分抽离为一个自定义hook——useSomeData。
import React, { useState, useEffect } from 'react';
const useSomeData = () => {
const cachedData = JSON.parse(localStorage.getItem('someData'));
const [someData, setSomeData] = useState(cachedData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!someData) {
setLoading(true);
fetch('/some-data')
.then(response => response.json())
.then(data => {
localStorage.setItem('someData', JSON.stringify(data));
return setSomeData(data);
})
.catch(error => setError(error))
.finally(() => setLoading(false));
}
}, []);
return { someData, loading, error };
};
使用useSomeData看起来像这样:
const SomeComponent = (props) => {
const { someData, loading, error } = useSomeData();
return (
<React.Fragment>
{loading && <div>{'Loading...'}</div>}
{!loading && error && <div>{`Error: ${error}`}</div>}
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>}
</React.Fragment>
);
};
const AnotherComponent = (props) => {
const { someData, loading, error } = useSomeData();
return (
<React.Fragment>
{loading && <div>{'Loading...'}</div>}
{!loading && error && <div>{`Error: ${error}`}</div>}
{!loading && !error && someData && <div>{/* INSERT ANOTHER AMAZING UI */}</div>}
</React.Fragment>
);
};
复用代码挺棒的,但就仅此而已吧?
定制数据请求
我们的应用越来越复杂,我决定上Redux。
此时只需要简单的修改下useSomeData,完全不需要改动业务组件:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectSomeData } from 'path/to/data/selectors';
import { fetchSomeData } from 'path/to/data/action';
const useSomeData = () => {
const dispatch = useDispatch();
const someData = useSelector(selectSomeData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!someData) {
setLoading(true);
dispatch(fetchSomeData())
.catch(error => setError(error))
.finally(() => setLoading(false));
}
}, []);
return { someData, loading, error };
};
某天我决定赶时髦上GraphQL。同样,只需要简单修改useSomeData而无需改动业务组件:
import { gql, useQuery } from '@apollo/client';
const FETCH_SOME_DATA = gql`
fetchSomeData {
data {
# some fields
}
}
`;
const useSomeData = () => {
const { data, loading, error } = useQuery(FETCH_SOME_DATA);
return { someData: data, loading, error };
};
每当我自愿(或被迫)修改数据请求/状态管理部分时,只需要修改对应的hook就行。
就像经典的依赖倒置原则(SOLID中的D)。尽管并非面向对象,但我们定义了一个抽象接口,并基于其实现了该接口的类。
useSomeData实际上为使用他的业务组件提供了一个接口。
开发者不需要关心useSomeData的实现原理,只需要关注接收到的数据、加载状态、错误信息即可。
理论上来说,只要定义合适的接口,就能将UI从数据层解耦出来,并随时迁移到任何数据层上。