React Hooks之--useEffect和自定义HOOK

6,843 阅读8分钟

遇到的场景:在目前开发过程中,简单的无状态组件可以换成hook组件,但是class组件不能简单的用hook写法替换,(不是很懂到底有没有达到性能优化的作用),但在同事看了之后说可读性不高。所以专门再次学习了hooks,着重了解hook的动机。在组件之间复用状态逻辑很难复杂组件变得难以理解难以理解的 class

Hook的动机

  1. Hook 使你在无需修改组件结构的情况下复用状态逻辑。
  2. Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。
  3. Hook 使你在非 class 的情况下可以使用更多的 React 特性。
  • 熟悉useEffect的用法,体会是如何解决复杂组件变得难以理解Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 熟悉自定义HOOK的用法,体会如何解决在组件之间复用状态逻辑很难:Hook 使你在无需修改组件结构的情况下复用状态逻辑

因此该文章着重讲述useEffect自定义HOOK

UseEffect

hooks-effectuseEffect完整指南

  1. 可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个生命周期函数的组合。而不是代替
  2. useEffect会在每次渲染后都执行,在第一次渲染之后和每次更新之后都会执行,发生在DOM渲染结束之后执行
  3. 需要清除的effect
  • 为什么要在effect中返回一个函数?:这是effect可选的清除机制,每个 effect 都可以返回一个清除函数。
  • React何时清除 effect?:在组件卸载的时候执行清除操作。因为effect在每次渲染的时候都会执行,所以React会在执行当前effect之前对上一个effect进行清除。 在计时器demo中,需要在清除函数中clearInterval(timer)
  1. useEffect可以在组件渲染后实现各种不同的副作用,有些副作用需要清除,所以要返回一个函数;有的副作用不必清除,所以不需要返回。
  2. effect可以像使用多个state 一样,使用多个effect
  3. 挂载和取消挂载,更适合用UseEffect
  4. 通过跳过 Effect 进行性能优化,只要传递数组作为 useEffect 的第二个可选参数即可因为每次渲染effect都会渲染,所以传递第二个参数告诉React,前后值进行比较,如果参数相等,会跳过这个effect渲染,实现性能优化
  5. 如果我的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
}
  1. 那么问题出来了,如果有多个依赖该怎么办?
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];
};
  1. useEffect会有死循环情况?
  • 设置data=[],Effect在后一次渲染时data也为[],那么[]===[]为false,所以会造成useEffect会一直不停的渲染,因此我把data的初始值改为undefined,试了一下果然可以。
  • useEffect在传入第二个参数时一定注意:第二个参数不能为引用类型,引用类型比较不出来数据的变化,会造成死循环。
  1. 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} 即可.

  1. useEffect 注意事项
  • useEffect 对外部依赖苛刻的要求,只有在整体项目都注意保持正确的引用时才能优雅生效。

2019.11.21

  1. 未完待续...

自定义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>
    )
}

再接再厉~