点击关注“有赞coder”
获取更多技术干货哦~
前言:React Hooks被越来越多的人认可,整个社区都以积极的态度去拥抱它。在最近的一段时间笔者也开始在一些项目中尝试去使用React Hooks。原本以为React Hooks很简单,和类组件差不多,看看API就能用起来了。结果在使用中遇到了各种各样的坑,通过阅读React Hooks相关的文章发现React Hooks和类组件有很多不同。由此,想和大家做一些分享。
eslint-plugin-react-hooks
(由React官方发布)。在很多时候,这个eslint插件在我们使用React Hooks的过程中,会帮我们避免很多问题。展示现在的值
按钮三秒后,会alert点击次数:function Demo() {
const [num, setNum] = useState(0);
const handleClick = () => {
setTimeout(() => {
alert(num);
}, 3000);
};
return (
<div>
<div>当前点击了{num}次</div>
<button onClick={() => { setNum(num + 1) }}>点我</button>
<button onClick={handleClick}>展示现在的值</button>
</div>
);
};
num
到3展示现在的值
按钮num
到5。num
不是应该是5吗?num
仅是一个数字而已。它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何东西。它就是一个普通的数字像下面这个一样:const num = 0;
// ...
setTimeout(() => {
alert(num);
}, 3000);
// ...
useState()
拿到num
的初始值为0,当我们调用setNum(1)
,React会再次渲染组件,这一次num
是1。如此等等:// 第一次渲染
function Demo() {
const num = 0; // 从useState()获取
// ...
setTimeout(() => {
alert(num);
}, 3000);
// ...
}
// 在点击了一次按钮之后
function Demo() {
const num = 1; // 从useState()获取
// ...
setTimeout(() => {
alert(num);
}, 3000);
// ...
}
// 又一次点击按钮之后
function Demo() {
const num = 2; // 从useState()获取
// ...
setTimeout(() => {
alert(num);
}, 3000);
// ...
}
num
状态,这个状态值是函数中的一个常量。num
为3时,我们点击了展示现在的值
按钮,就相当于:function Demo() {
// ...
setTimeout(() => {
alert(3);
}, 3000)
// ...
}
num
值为3。class Demo extends Component {
state = {
num: 0,
}
handleClick = () => {
setTimeout(() => {
alert(this.state.num);
}, 3000);
}
render() {
const { num } = this.state;
return (
<div>
<p>当前点击了{num}次</p>
<button onClick={() => { this.setState({ num: num + 1 }) }}>点击</button>
<button onClick={this.handleClick}>展示现在的值</button>
</div>
);
}
};
num
到3展示现在的值
按钮num
到5handleClick = () => {
setTimeout(() => {
alert(this.state.num);
}, 3000)
}
this
,我们可以获取到最新的state和props。展示现在的值
按钮的情况下我们对点击
按钮又点击了几次,this.state
将会改变。handleClick
方法从一个“过于新”的state
中得到了num
。this.state
读取的数据并不能保证其是一个特定的state。handleClick
事件处理程序并没有与任何一个特定的渲染绑定在一起。function Demo() {
const [num, setNum] = useState(0);
const handleClick = useCallback(() => {
setNum(num + 1);
}, []);
return (
<div>
<p>当前点击了{num}次</p>
<button onClick={handleClick}>点击</button>
</div>
);
}
useCallback
本质上是添加了一层依赖检查。当我们函数本身只在需要的时候才改变。点击
按钮,num
的值始终为1。这是因为useCallback
中的函数被缓存了,其依赖数组为空数组,传入其中的函数会被一直缓存。handleClick
其实一直都是:const handleClick = () => {
setNum(0 + 1);
};
num
的值变为1,但是React并不知道你的函数中依赖了num
,需要去更新函数。num
,React才会知道你依赖了num
,在num
的值改变时,需要更新函数。function Demo() {
const [num, setNum] = useState(0);
const handleClick = useCallback(() => {
setNum(num + 1);
}, [num]); // 添加依赖num
return (
<div>
<p>当前点击了{num}次</p>
<button onClick={handleClick}>点击</button>
</div>
);
};
点击
按钮,num的值不断增加。function Demo(props) {
const { query } = props;
const [list, setList] = useState([]);
const fetchData = async () => {
const res = await axios(`/getList?query=${query}`);
setList(res);
};
useEffect(() => {
fetchData(); // 这样不安全(调用的fetchData函数使用了query)
}, []);
return (
<ul>
{list.map(({ text }) => {
return (
<li key={text}>{ text }</li>
);
})}
</ul>
);
};
function Demo(props) {
const { query } = props;
const [list, setList] = useState([]);
useEffect(() => {
const fetchData = async () => {
const res = await axios(`/getList?query=${query}`);
setList(res);
};
fetchData();
}, [query]);
return (
<ul>
{list.map(({ text }) => {
return (
<li key={text}>{ text }</li>
);
})}
</ul>
);
};
useCallBack
。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变。function Demo(props) {
const { query } = props;
const [id, setId] = useState();
const [list, setList] = useState([]);
const fetchData = async (newId) => {
const myId = newId || id;
if (!myId) {
return;
}
const res = await axios(`/getList?id=${myId}&query=${query}`);
setList(res);
};
const fetchId = async () => {
const res = await axios('/getId');
return res;
};
useEffect(() => {
fetchId().then(id => {
setId(id);
fetchData(id);
});
}, []);
useEffect(() => {
fetchData();
}, [query]);
return (
<ul>
{list.map(({ text }) => {
return (
<li key={text}>{ text }</li>
);
})}
</ul>
);
};
query
在异步获取id
期间变了,最后请求的入参,其query
将会用之前的值。(引起这个问题的原因还是闭包,这里就不再复述了)function Demo(props) {
const { query } = props;
const [id, setId] = useState();
const [list, setList] = useState([]);
useEffect(() => {
const fetchId = async () => {
const res = await axios('/getId');
setId(res);
};
fetchId();
}, []);
useEffect(() => {
const fetchData = async () => {
const res = await axios(`/getList?id=${id}&query=${query}`);
setList(res);
};
if (id) {
fetchData();
}
}, [id, query]);
return (
<ul>
{list.map(({ text }) => {
return (
<li key={text}>{ text }</li>
);
})}
</ul>
);
}
function Children(props) {
const { fetchData } = props;
return (
<div>
<button onClick={() => { fetchData(); }}>点击</button>
</div>
);
};
function Demo() {
const [list, setList] = useState([]);
const fetchData = useCallback(async () => {
const res = await axios(`/getList`);
setList([...list, ...res]);
}, [list]);
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<div>
<ul>
{list.map(({ text }) => {
return (
<li key={text}>{ text }</li>
);
})}
</ul>
<Children fetchData={fetchData} />
</div>
);
};
fetchData
函数会更新list
,list
更新后fetchData
函数就会被更新。fetchData
更新后useEffect
会被调用,useEffect
中又调用了fetchData
函数。fetchData
被调用导致list
更新...function Children(props) {
const { fetchData } = props;
return (
<div>
<button onClick={() => { fetchData(); }}>点击</button>
</div>
);
};
const initialList = [];
function reducer(state, action) {
switch (action.type) {
case 'increment':
return [...state, ...action.payload];
default:
throw new Error();
}
}
export default function Demo() {
const [list, dispatch] = useReducer(reducer, initialList);
const fetchData = useCallback(async () => {
const res = await axios(`/getList`);
dispatch({
type: 'increment',
payload: res
});
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<div>
<ul>
{list.map(({ text }) => {
return (
<li key={text}>{ text }</li>
);
})}
</ul>
<Children fetchData={fetchData} />
</div>
);
};
dispatch
在组件的声明周期内保持不变。所以上面的例子中不需要依赖dispatch
。useReducer
我们就可以移除list
依赖。不会再出现死循环的情况。fetchData
函数和list
状态解耦。我们的fetchData
函数不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由reducer去统一处理。useReducer
会比useState更适用。例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的state等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch
而不是回调函数。