如何自己编写一个 react hook?react 允许我们自己编写 Hook。
我们有几个组件,它们都要先进行 ajax 请求,获取到数据,然后把数据渲染到页面上。总体代码:
class App extends Component{
state = {
isLaoded: false,
data: null
}
componentDidMount(){
// 发起网络请求
axios.get("/api/fruits")
.then(res => res.data)
.then(data => {
this.setState({
isLaoded: true,
data: data
});
}).catch(err => console.error(err));
}
render(){
if(!this.state.isLaoded){
return (
// 数据没有请求到之前,loading 状态
<h1>Loading...</h1>
);
}else{
return (
<div>
{ this.state.data.map((item, idx) => <p key={idx}>{item.name} --- {item.label}</p>) }
</div>
);
}
}
}
很多代码都是重复的,只有 isLoaded
变为 true
后渲染的 UI 不同,以及请求 URL 不同。我们完全可以将相同的部分提取到一个通用的地方。在 Hooks 出来之前,一般有两种提取公共代码的手段:HOC
高阶组件和 render-props
。
如果要使用高阶组件的形式复用代码逻辑,就需要写一个函数,这个函数接收 React 组件作为参数,然后再返回一个新的 React 组件。
// 首先会接受一个 url
export function withFetch(url){
// 然后接收组件
return function(Com){
return class extends React.Component{
state = {
isLoaded: false,
data: null,
}
componentDidMount(){
fetch(url) // 请求数据
.then(json => json.json())
.then(data => {
// 更新 state
this.setState({
isLoaded: true,
data,
});
});
}
render(){
if(!this.state.isLoaded){
// 数据没有获取到时,展示 loading
return <h1>Loading...</h1>
}else{
// 把请求数据传递给组件
return <Com data={this.state.data} />
}
}
}
}
}
上面的 App 组件就不用再那么写了:
import { withFetch } from "./withFetch";
class App extends Component{
render(){
return (
<div>
{/* 使用 this.props 获取到传递的属性 */}
{ this.props.data.map((item, idx) => <p key={idx}>{item.name} --- {item.label}</p>) }
</div>
);
}
}
// 导出时用 withFetch 高阶组件包裹一下
export default withFetch("/api/fruits")(App);
上面的高阶组件,增强了 App
组件,让 App 组件可以通过 this.props.data
拿到请求来的数据。假设我们使用 App
时也可能给它传一个 data
属性:
function Xxx(){
return <App data={[]} />
}
这个时候,Xxx
组件传入的 data
属性将会失效。也就是说,高阶组件可能会覆盖其他传入的属性值。尤其是多个高阶组件嵌套使用时,可能无法分清数据的来源。
// 多层嵌套 withRouter 和 withFetch 如果使用了同样的 props 时,会有冲突
export default withRouter(withFetch(MyComponent));
render-props
是另一种复用技术。我们改造上面的高阶函数,让它变成一个普通的组件:
class Fetch extends React.Component{
state = { // 初始化 state
isLoaded: false,
data: null,
}
componentDidMount(){
// url 是我们为这个组件传入的值
fetch(this.props.url)
.then(json => json.json())
.then(data => {
this.setState({
isLoaded: true,
data,
});
});
}
render(){
if(!this.state.isLoaded){
return <h1>Loading...</h1>
}else{
// 给 props.render 传入请求到的数据
// props.render 应返回一个 jsx 或组件
return this.props.render(this.state.data);
}
}
} export default Fetch;
改造 App
组件的代码:
import Fetch from "./Fetch";
function List({item}){
return <p >{item.name} --- {item.label}</p>
}
class App extends Component{
render(){
return (
<div>
{/* 给 Fetch 组件传入 url 和 render 函数,render函数就像一个回调函数 */}
{/* render 函数内部渲染数据 */}
<Fetch
url="/api/fruits"
render={(data) => data.map((item, idx) => <List item={item} key={idx} />)}
></Fetch>
</div>
);
}
}
export default App;
使用 render-props
解决了高阶组件的不足,使用 组件 + render
回调的方式避免的 props 的属性值覆盖问题。
但,render-props 也有一些缺点,比如如果 render
里渲染的数据也要使用 render-props
的方式渲染组件,就会出现多级嵌套。例如:
function App(){
return (
<Fetch
url="/api/fruits"
render={(data) => data.map((item,idx) =>
<List item={item} key={idx}>
<Fetch
url={`/api/${item}`}
render={data =>
<Fetch
url={`/api/${data}`}
render={data => <Entry data={data} />}
/>
}
/>
</List>
)}
/>
);
}
过度使用 render-props
会让代码比较丑陋。Fetch
组件把 state 的数据传递给了 render
函数,这会让 App
组件在其它地方很难使用到 render 函数中的数据(或者说只能在 render 函数中使用数据),比如 useEffect
等钩子函数或者其他的组件。
自定义 Hook 也可以达到组件逻辑复用的目的。自定义 Hook 需要遵循下面几点要求:
use
开头;改造 App
组件中内容:
import React, { useState, useEffect } from "react";
// 自定义的 hook,接收 url 作为参数
function useFetch(url){
let [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(json => json.json())
.then(data => {
setData(data);
})
},[]);
return data;
}
export function App(){
// 拿到数据
let data = useFetch("/api/fruits");
// 如果数组没有拿到,就渲染 加载组件
return data ? data.map((item, idx) => <h2 key={idx}>{item.name} --- {item.label}</h2>) : <h1>Loading...</h1>;
}
Hook 也可以返回 jsx,例如:
import React, { useEffect, useState } from "react";
function useFetch(url, RenderCom){
let [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(json => json.json())
.then(data => {
setData(data);
});
},[]);
// loading 组件
function Loading(){
return <h1>Loading...</h1>
}
if(!data){
return <Loading />;
}else{
// 给要渲染的组件传入数据
return <RenderCom data={data} />
}
}
export function App(){
function Render(props){
// 拿到数据,进行渲染
const { data } = props;
return data.map((item, idx) => <h2 key={idx}>{item.name} ---> {item.label}</h2>)
}
// 获取到组件
let R = useFetch("/api/fruits", Render);
return R;
}
使用自定义 Hook 也可以做到代码复用。而且比前两种方式要简洁。当然这里编写的 useFetch
钩子功能一般,类似异步请求的 Hook 可以下载 use-http
这个模块,它的功能很全面。
假如我们想要获取到文档可视区域的宽高,当窗口大小发生改变时也要获取到准确的宽度、高度数据,就可以自定义一个 Hook 来完成这个任务。
import { useState, useEffect } from 'react';
function useWinSize(){
let [width, setWidth] = useState(document.documentElement.clientWidth);
let [height, setHeight] = useState(document.documentElement.clientHeight);
const handleReset = function(){
setWidth(document.documentElement.clientWidth);
setHeight(document.documentElement.clientHeight);
}
useEffect(() => {
// 当触发 reset 事件时,就重新计算宽、高
window.addEventListener("reset", handleReset, false);
// 组件在将要卸载时会调用这个函数
return () => {
window.removeEventListener('reset', handleReset, false);
}
},[]);
// 返回数据
return { width, height };
}