接触react一个星期,也慢慢熟悉了一些概念,比如HOC(高阶组件)、jsx、函数式组件、HOOK,感觉react也没有别人说的学习曲线陡峭,难上手等等,给我的感觉,如果你会Vue,上手React真的会非常快,不要被这些概念给吓到,这样的一些概念的出现,一定是有着他的道理的,无外乎包含但不限于以下两点理由:
- 为了代码复用,比如HOC,自定义HOOK
- 为了代码更加简洁,更加好理解,比如jsx,函数式组件。
那么,我们所理解的React的模式,其实归根结底就是UI=Render(State)
,这其实和Vue乃至整个前端的哲学并无任何冲突,相反,是一个统一。说来说去,说简单点,一个web应用,应该是状态驱动的,而状态=数据 逻辑
所以,我们的UI=Render(Data Logic)
那么,Data从何而来,可以说99%的web应用的Data是从网络而已,俗称网络获取数据。
下面的代码段是一个很简单的显示列表数据模板,很简单,这里只用到了useState
这个Hook,如果需要填充数据,很明显,使用setData给到数据就可以了,数据从何而来,这是一个问题,带到今天来看,要讲的是如何从网络获取数据。
import React, { useState } from 'react';
function App() {
const [data, setData] = useState({ hits: [] });
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
温馨提示,下面是的列车将一步一步提速,请系好安全带~~。
引入axios请求网络数据,将请求放入useEffect中
代码语言:txt复制import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
});
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
如果你熟悉 React class 的生命周期函数,你可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合,上述代码你应该不会满意吧,你可能仅仅需要网络请求代码只在componentDidMount
的时候执行一次。如是
加了一个[]
代码语言:txt复制import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
通过Object.is(value1, value2);
成功挡住了componentDidUpdate
每次无脑的唤醒。
然而,上面的代码会有一个告警
那是因为useEffect要求要么返回一个清理函数
,要么啥都别返回,而上面返回的是一个Promise体,他将最终返回的是一个结果,这显然会受到一个告警,解决的办法如下。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
不过很快就会就想到,网络请求需要传递参数
所以,你加了一个query的useState,而且仅仅当query变化时触发重新获取网络数据,干得还不错。
代码语言:txt复制...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
...
);
}
export default App;
后面,你发现网络请求有点慢,需要加loading状态,如是
代码语言:txt复制import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'https://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
}
>
Search
</button>
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
export default App;
其实很简单,就加了一个loading的useState而已。所以,如果需要加什么错误状态,你应该也懂这个套路了吧。
请注意,要开车了,前面说到,更好复用才是推动技术变革的第一生产力,比如Docker,我瞎扯的。
假如其他业务需要用到你这个网络请求,如是,你写了一个自定义的Hook
代码语言:txt复制const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'https://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }, setUrl];
}
后面你知道了useReducer,你发现,loading,error这些状态应该交给他来做,而不是你通过useState来做,这样显得会更加清晰~~
因此,你引入了useReducer
代码语言:txt复制const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
default:
throw new Error();
}
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' });
}
};
fetchData();
}, [url]);
...
};
像这种语法
代码语言:txt复制return {
...state,
isLoading: true,
isError: false
};
我们已经见怪不怪了,但是别小看这种,说大点,这可能是最简洁的函数式编程
了,他返回的是一个全新的对象,函数式编程的好处?复用性无话可说,方便做备忘录,使用一个history数组记录每次变更的state就OK啦。anymore,自己YY吧。
最后,你可能会想,页面componentWillUnmount时,如果网络请求没回,是不是该”终止网络请求“
代码语言:txt复制const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
return [state, setUrl];
};
如是,你在useEffect中加了一个didCancel变量,并且返回一个闭包
,其实就是一个函数啦,只不过他让你可以改他母体的变量而已,因此,在componentWillUnmount时候,这个变量被置位false了,如是dispatch的将不会触发。
是不是和移动客户端开发灰常像,页面的destory的时候,如果网络请求的presenter还持有页面的context,那么页面将释放不掉,造成内存泄漏不说,还会导致在页面执行destory之后,网络数据回来,走触发变更ui的逻辑,导致crash的发生,因为你不能对一个已经destory的页面进行变更ui的操作。
当然,这里,网络请求其实并没有真的被cancel掉,cancel掉的之后网络请求回来之后的逻辑。