遇到的场景:在目前开发过程中,简单的无状态组件可以换成hook组件,但是class组件不能简单的用hook写法替换,(不是很懂到底有没有达到性能优化的作用),但在同事看了之后说可读性不高。所以专门再次学习了hooks,着重了解hook的动机。
在组件之间复用状态逻辑很难,复杂组件变得难以理解,难以理解的 class
Hook的动机
- Hook 使你在无需修改组件结构的情况下复用状态逻辑。
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。
- Hook 使你在非 class 的情况下可以使用更多的 React 特性。
- 熟悉useEffect的用法,体会是如何解决
复杂组件变得难以理解:Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) - 熟悉自定义HOOK的用法,体会如何解决
在组件之间复用状态逻辑很难:Hook 使你在无需修改组件结构的情况下复用状态逻辑。
因此该文章着重讲述
useEffect和自定义HOOK
UseEffect
- 可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个生命周期函数的组合。
而不是代替 - useEffect会在每次渲染后都执行,
在第一次渲染之后和每次更新之后都会执行,发生在DOM渲染结束之后执行 - 需要清除的effect
- 为什么要在effect中返回一个函数?:这是effect可选的清除机制,每个 effect 都可以返回一个清除函数。
- React何时清除 effect?:在组件卸载的时候执行清除操作。因为effect在每次渲染的时候都会执行,所以React会在执行当前effect之前对上一个effect进行清除。
在计时器demo中,需要在清除函数中clearInterval(timer)
- useEffect可以在组件渲染后实现各种不同的副作用,有些副作用需要清除,所以要返回一个函数;有的副作用不必清除,所以不需要返回。
- effect可以像使用多个state 一样,使用多个effect
- 挂载和取消挂载,更适合用
UseEffect。 - 通过跳过 Effect 进行性能优化,只要传递数组作为 useEffect 的第二个可选参数即可
因为每次渲染effect都会渲染,所以传递第二个参数告诉React,前后值进行比较,如果参数相等,会跳过这个effect渲染,实现性能优化 - 如果我的effect的依赖频繁变化,该怎么办?
- 启用第二参数,添加依赖
下面案例中没有添加依赖,在setCount时,拿到的 count 值始终为0,effect对比结果相等,所以跳过。 因此计时器变化到1,不在改变,但是effect 还是会持续的渲染
useEffect(() => {
const timer = setInterval(() => {
console.log(count,'count')//0 "count"
setCount(count+1) //这个 effect 依赖于 `count` state
}, 1000)
return () => clearInterval(timer)
}, [])// 🔴 Bug: `count` 没有被指定为依赖
添加依赖,或者修改setCount,setCount(c => c + 1)方法,
useEffect(() => {
const timer = setInterval(() => {
setCount(count+1)
}, 1000)
return () => clearInterval(timer)
}, [count])
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)//✅ 在这不依赖于外部的 `count` 变量
}, 1000)
return () => clearInterval(timer)
}, [])//✅ 我们的 effect 不适用组件作用域中的任何变量
注意:
- 如果你要通过第二个参数优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在effect中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
- 数据请求处理的方式如何处理函数
- 将函数移动到你的 effect 内部
- 可以尝试把那个函数移动到你的组件之外。
- 在 effect 之外调用函数, 并让 effect 依赖于它的返回值。
- 可以 把函数加入 effect 的依赖但 把它的定义包裹进useCallback Hook
function getFetchUrl2(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
const [data, setData] = useState([])
const [data1, setData1] = useState([])
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const getFetchUrl = useCallback((query)=> {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
},[])
useEffect(() => {
// console.log(1)
const result = axios.get(getFetchUrl('react')).then(res=>{
setData(res.data.hits)
})
//console.log(2)
async function fetchData_redux() {
try{
const result = await axios(getFetchUrl('redux'))
setData1(result.data.hits)
}catch (e) {
setIsError(true)
}
setIsLoading(true)
}
fetchData_redux()
}, [])
}
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
}
const refresh = useCallback(() => {
// ...
}, [name, searchState, address, status, personA, personB, progress, page, size]);
-
依赖数组依赖的值最好不要超过 3 个,否则会导致代码会难以维护。
-
如果发现依赖数组依赖的值过多,我们应该采取一些方法来减少它。
- 去掉不必要的依赖。
- 将 Hook 拆分为更小的单元,每个 Hook 依赖于各自的依赖数组。
放到不同 useEffect 中 - 通过合并相关的 state,将多个依赖值聚合为一个。
合并成一个state - 通过 setState回调函数获取最新的state,以减少外部依赖。
使用useCallback来减少一些依赖 - 通过 ref 来读取可变变量的值,不过需要注意控制修改它的途径。
ref
const useValues = () => {
const [values, setValues] = useState({});
const [updateData] = useCallback((nextData) => {
setValues((prevValues) => ({
data: nextData,
count: prevValues.count + 1, // 通过 setState 回调函数获取最新的 values 状态,这时 callback 不再依赖于外部的 values 变量了,因此依赖数组中不需要指定任何值
}));
}, []); // 这个 callback 永远不会重新创建
return [values, updateData];
};
- useEffect会有死循环情况?
- 设置data=[],Effect在后一次渲染时data也为[],那么[]===[]为false,所以会造成useEffect会一直不停的渲染,因此我把data的初始值改为undefined,试了一下果然可以。
- useEffect在传入第二个参数时一定注意:第二个参数不能为引用类型,引用类型比较不出来数据的变化,会造成死循环。
- useEffect出现死循环
useEffect(() => {
props.onChange(props.id)
}, [props.onChange, props.id])
如果 id 变化,则调用 onChange。但如果上层代码并没有对onChange进行合理的封装,导致每次刷新引用都会变动,则会产生严重后果。
假设父级代码这样写:
class App {
render() {
return <Child id={this.state.id} onChange={id => this.setState({ id })} />
}
}
这样会导致死循环。虽然看上去 只是将更新 id 的时机交给了子元素 ,但由于 onChange 函数在每次渲染时都会重新生成,因此引用总是在变化,就会出现一个无限死循环:
新 onChange -> useEffect 依赖更新 -> props.onChange -> 父级重渲染 -> 新 onChange...
想要阻止这个循环的发生,只要改为onChange={this.handleChange} 即可.
- useEffect 注意事项
useEffect对外部依赖苛刻的要求,只有在整体项目都注意保持正确的引用时才能优雅生效。
2019.11.21
- 未完待续...
自定义HOOK
hooks-custom , when to Use Custom React Hooks
Hook的高阶用法,在组件中复用状态逻辑(目前的理解)
2019.11.21
最近照猫画虎的写了一个自定义hooks,结果就是出现了useEddect死循环
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
//自定义hooks
function useRemarks(remarksList) {
const [list, setList] = useState([])
useEffect(()=>{
setList(remarksList)
},[remarksList])
const handleSelect = (item) => {
let newList = list.map(v => (v.id === item.id ? {...v, select: !v.select} : {...v, select: false}))
setList(newList)
}
return [list, handleSelect]
}
因为有两个组件都有用到同一个返回的remarksList数据,所以写成了这个样子,后期发现因为需要做数据处理,所以提取的这个hooks其实并不好。虽然功能也能实现,但是不是精简的hooks,最后应用useReducer写了数据处理,但是要求父组件是函数,能够支持useReducer写法,不适用于目前是class父组件。
2019.11.25
今天,同样的代码,没有进入死循环😢
所以以后这种接收数据,容易进入死循环的情况,还是不要写成自定义hooks,自定义hooks最好还是用于逻辑的处理。
function useSequence(initialNext) {
let nextRef = useRef(initialNext)
return () => nextRef.current++
}
export function useKeys(initial) {
let getKey = useSequence(initial + 1)
let [keys, setKeys] = useState(Array(initial).fill(0).map((x, i) => i + 1))
let add = () => setKeys(keys.concat(getKey()))
let remove = (key) => {
let newKeys = keys.slice(0)
let index = newKeys.indexOf(key)
if (index !== -1) {
newKeys.splice(index, 1)
}
setKeys(newKeys)
}
return [keys, add, remove]
}
export function useContactModel({defaultValue = {}} = {}) {
let [name, setName] = useState(defaultValue.name || '')
let [email, setEmail] = useState(defaultValue.email || '')
return {
inputProps: {
name: {
value: name,
onChange: e => setName(e.target.value)
},
email: {
value: email,
onChange: e => setEmail(e.target.value)
}
}
}
}
export function useWinSize() {
const [size,setSize] = useState({
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight
})
const onResize = useCallback((node)=>{
setSize({
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight
})
},[])
useEffect(()=>{
window.addEventListener('resize',onResize)
return()=>{
window.removeEventListener('resize',onResize)
}
},[])
return size
}
2019.12.02
const dataFetchReducer = (state, action) => {
switch (action.type) {
case "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();
// return state
}
}
export const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl)
const [state, dispatch] = useReducer(dataFetchReducer, {
data: initialData,
isLoading: false,
isError: false,
})
useEffect(() => {
let didCancel = false
const fetchdata = async () => {
dispatch({type: 'init'});
try {
const result = await axios(url)
if (!didCancel) {
dispatch({type: 'FETCH_SUCCESS', payload: result.data})
}
} catch (e) {
if (!didCancel) {
dispatch({type: 'FETCH_FAILURE'})
}
}
}
fetchdata()
return () => {
didCancel = true
}
}, [url])
const doFetch = url => setUrl(url)
return {...state, doFetch}
}
const App=()=>{
const {data,isLoading,isError}=useDataApi('https://hn.algolia.com/api/v1/search?query=react',{hits:[]})
// useEffect(()=>{
// console.log(data,isLoading,isError,'---')
//
// },[data,isLoading,isError])
return(
<div>
{
!isLoading? <div>loading....</div>
:data.hits.slice(0,3).map(v=>(<p key={v.title}>{v.title}</p>))
}
</div>
)
}
再接再厉~