Skip to content

ReactHooks学习笔记

为什么需要Hook?

案例

useState

useState体验

jsx
import {memo, useState} from "react";

const App = memo(() => {
    const [message, setMessage] = useState("helloo world")

    function handleMessage() {
        setMessage("你好张三")
    }

    return (
        <div>
            <h2>App</h2>
            <div>message: { message } </div>
            <button onClick={ () => { handleMessage() } }>改变</button>
        </div>
    )
})

export default App

useEffect

认识EffectHook

基本使用

假如我们现在有一个需求:浏览器页面标签的title总是显示counter的数字,分别使用类组件和函数式组件来实现这个需求:

类组件实现:

jsx
import React, {PureComponent} from 'react';

class App2 extends PureComponent {
    constructor() {
        super();
        this.state = {
            counter: 100
        }
    }

    componentDidMount() {
        document.title = this.state.counter
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        document.title = this.state.counter
    }

    changeCounter = (number) => {
        this.setState({
            counter: number + this.state.counter
        })
    }

    render() {
        return (
            <div>
                <button onClick={() => {
                    this.changeCounter(-1)
                }}>-1
                </button>
                <button onClick={() => {
                    this.changeCounter(1)
                }}>+1
                </button>
            </div>
        );
    }
}
export default App2;

userEffect名字由来,函数式组件本身也是函数,react推荐我们编写纯函数,像在这个函数中做一些发送网络请求、操作document对象、手动更新dom、一些事件的监听,这些业务本身不该放到纯函数中,属于函数的副作用,react为此专门提供了一个hook来管理这些副作用

useEffect的作用:

  • 1、告诉React需要在初次渲染后执行某些操作useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;

  • 2、useEffect第二个参数是个数组,如果第二个参数不传,那么当每次组件重新渲染的时候都会重新执行回调函数的逻辑。如果数组中传递了具体的变量参数,只有当数组中的变量值改变的才会重新执行useEffect中的第一个回调函数逻辑。

函数式组件结合useEffect来实现

jsx
import {memo, useEffect, useState} from "react";

const App = memo(() => {

    const [counter, setCounter] = useState(100)

    // 告诉react当前组件初次渲染时以后需要执行的副作用代码
    useEffect(() => {
        console.log("触发useEffect")
        document.title = counter
    });

    const changeCounter = (number) => {
        setCounter(counter + number)
    }

    return (
        <div>
            <button onClick={() => {
                changeCounter(1)
            }}>+1
            </button>
            <button onClick={() => {
                changeCounter(-1)
            }}>-1
            </button>
        </div>
    )
})

export default App

清除Effect

class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除,例如清除定时器、事件监听等,利用useEffect也可以做到,useEffect返回值可以是一个函数,可以在这里清除副作用

示例:

jsx
import {memo, useEffect, useState} from "react";

const App = memo(() => {
    const [counter, setCounter] = useState(100)

    useEffect(() => {

        console.log("进行事件监听")

        // 返回一个回调函数,当组件被重新渲染或者组件卸载的时候执行
        return () => {
            console.log("执行重新渲染或卸载了,取消事件监听")
        }
    })

    const changeCounter = (number) => {
        setCounter(counter + number)
    }

    return (
        <div>
            <div>
                counter: {counter}
            </div>
            <button onClick={() => {
                changeCounter(1)
            }}>+1
            </button>
        </div>
    )
})

export default App

初次渲染打印结果:

js
进行事件监听

初次渲染完成后,我们点击按钮更新了counter打印结果:

js
进行事件监听

执行重新渲染或卸载了,取消事件监听
进行事件监听

也就是说,当组件被重新渲染或者组件卸载的时候,会先执行useEffect返回的回调函数中的逻辑,清除effect,然后才执行useEffect中的代码逻辑,这就避免了初始化或者更新的时候设置多个副作用的问题

使用多个effect

jsx
import {memo, useEffect, useState} from "react";

const App = memo(() => {

    useEffect(() => {
        console.log("修改title")
    })

    useEffect(() => {
        console.log("进行事件监听")
        // 返回一个回调函数,当组件被重新渲染或者组件卸载的时候执行
        return () => {
            console.log("执行重新渲染或卸载了,取消事件监听")
        }
    })

    useEffect(() => {
        console.log("监听redux数据变化")
        // 返回一个回调函数,当组件被重新渲染或者组件卸载的时候执行
        return () => {
            console.log("取消redux监听")
        }
    })

    return (
        <div>
        </div>
    )
})

export default App

Effect性能优化

前面的示例只要触发了组件的render那么三个useEffect都会重新被执行,明显会有性能问题

示例代码:

jsx
import {memo, useEffect, useState} from "react";

const App = memo(() => {

    const [counter, setCounter] = useState(100)

    useEffect(() => {
        console.log("监听redux数据")
    });

    useEffect(() => {
        console.log("监听eventBus数据")
    });

    useEffect(() => {
        console.log("触发counteruseEffect")
        document.title = counter
    });

    const changeCounter = (number) => {
        setCounter(counter + number)
    }

    return (
        <div>
            <button onClick={() => {
                changeCounter(1)
            }}>+1
            </button>
            <button onClick={() => {
                changeCounter(-1)
            }}>-1
            </button>
        </div>
    )
})

export default App

可以利用useEffect进行优化

优化后的代码每个useEffect只有当依赖的值变化后才重新执行,传入空数组只会在首次初始化执行一次渲染

jsx
import {memo, useEffect, useState} from "react";

const App = memo(() => {

    const [counter, setCounter] = useState(100)

    // 如果只需要组件渲染初次完成后执行一次,类似mounted
    // 可以传递一个空数组,不传则组件每次渲染都会重新执行里面的逻辑
    // useEffect(() => {
    //     console.log("触发useEffect")
    //     document.title = counter
    // }, []);

    useEffect(() => {
        console.log("监听redux数据")
    }, []);

    useEffect(() => {
        console.log("监听eventBus数据")
    }, []);

    useEffect(() => {
        console.log("触发counteruseEffect")
        document.title = counter
    }, [counter]);

    const changeCounter = (number) => {
        setCounter(counter + number)
    }

    return (
        <div>
            <button onClick={() => {
                changeCounter(1)
            }}>+1
            </button>
            <button onClick={() => {
                changeCounter(-1)
            }}>-1
            </button>
        </div>
    )
})

export default App

useContext

代码示例: ThemeContext

jsx
import React from "react";

const ThemeContext = React.createContext({color: 'red', type: 'color'});

export default ThemeContext

UserContext

jsx
import React from "react";

const UserContext = React.createContext({name: '张三', age: 20});

export default UserContext

App.jsx

jsx
import {memo, useContext} from "react";
import ThemeContext from "../context/ThemeContext";
import UserContext from "../context/UserContext";

const App = memo(() => {
    const themeContext = useContext(ThemeContext);
    const userContext = useContext(UserContext);

    console.log("themeContext",themeContext)
    console.log("userContext",userContext)

    return (
        <div>
            context
        </div>
    )
})

export default App

useReducer

useReducer在实际项目中用的不多,了解即可

主要应用场景:

  • 1、state的处理逻辑比较复杂,使用useReducer进行拆分

  • 2、这次修改的state需要依赖之前的state时,也可以使用

useCallback

通常使用useCallback的目的是优化子组件进行多次渲染的问题

例如下面的例子,我们向子组件Child中传递了一个函数increment,用于修改父组件中的count,父组件中还有一个变量message,这时候我们发现修改setMessage后触发了父组件的重新渲染,父组件重新定义了increment函数,导致子组件也重新渲染了,这样当子组件很多的时候会存在性能问题,我们想达到的效果应该是修改message不会影响子组件重新渲染,因为子组件中没有用到message

利用useCallback优化子组件多次渲染的问题示例:

子组件,子组件中调用父组件传入的increment方法

jsx
import {memo} from "react";

const Child = memo((props) => {

    console.log("Child is render")

    return (
        <div>
            <button onClick={()=>{props.increment()}}>子组件increment</button>
        </div>
    )
})

export default Child

父组件代码如下

jsx
import {memo, useCallback, useState} from "react";
import Child from "./Child,jsx";

const App = memo(() => {

    const [count, setCount] = useState(100)
    const [message, setMessage] = useState("hello world")

    const increment = () => {
        setCount(count + 1)
    }

    console.log("App is render")

    return (
        <div>
            <button onClick={() => {
                increment()
            }}>+1
            </button>

            <div>message:{ message }</div>
            <button onClick={e=>{ setMessage(Math.random) }}>改变message</button>

            <Child increment={increment}></Child>
        </div>
    )
})

export default App

当点击按钮 子组件increment 时,会发现控制台打印了Child is render,也就是说每次点击都重新渲染了Child子组件,原因是因为执行了setMessage方法,触发App组件重新渲染,重新定义了一个increment函数,子组件Child发现props中的increment变化了,所以重新执行了渲染。我们改变的是message变量,正常来说子组件没用到message,不应该重新触发渲染,当页面子组件很多的时候可能会出现性能问题。

利用useCallback进行性能优化:

修改父组件代码

jsx
import {memo, useCallback, useState} from "react";
import Child from "./Child,jsx";

const App = memo(() => {

    const [count, setCount] = useState(100)
    const [message, setMessage] = useState("hello world")

    const increment = useCallback(() => {
        setCount(count + 1)
    }, [count])

    console.log("App is render")

    return (
        <div>
            <button onClick={() => {
                increment()
            }}>+1
            </button>

            <div>message:{ message }</div>
            <button onClick={e=>{ setMessage(Math.random) }}>改变message</button>

            <Child increment={increment}></Child>
        </div>
    )
})

export default App

useCallback第二个参数也就是count在值没有改变的情况下,多次定义increment的时候,返回的值是相同,也就是说每次点击 改变message 虽然执行了setMessage方法并重新出发了App的渲染,但是increment因为使用了useCallback的原因,返回的increment函数并没有发生改变(实际每次执行App的渲染方法,都会重新定义increment函数,但是useCallback检测到依赖变量值没有改变,就返回了原先记住的函数)。此时increment函数没有改变,子组件props没有改变,这样就不会再触发子组件Child的重新渲染

前面的代码当count改变的时候依然会定义新的increment函数,而导致子组件重新渲染,其实依然有办法使得count改变的时候依然使用的是同一个函数,避免子组件渲染

方式一

前面的示例代码如果useCallback依赖值是一个空数组,那么即使count发生变化,increment用的函数也依然是同一个值,这样虽然可以避免子组件重复渲染,但是会产生闭包陷阱,当一直点击 +1 按钮时发现 count 只增加了一次 ,一直都是 101,这是因为useCallback用法错误,导致useCallback函数内部形成闭包,词法环境中的count只增加了一次,始终都是101

jsx
import {memo, useCallback, useState} from "react";
import Child from "./Child,jsx";

const App = memo(() => {

    const [count, setCount] = useState(100)
    const [message, setMessage] = useState("hello world")

    const increment = useCallback(() => {
        setCount(count + 1)
    }, [])

    console.log("App is render")

    return (
        <div>
            <div>count:{count}</div>
            <button onClick={() => {
                increment()
            }}>+1
            </button>

            <div>message:{ message }</div>
            <button onClick={e=>{ setMessage(Math.random) }}>改变message</button>

            <Child increment={increment}></Child>
        </div>
    )
})

export default App

一些js中常见的闭包陷阱 常见的闭包陷阱

useCallback使用不当可能造成闭包陷阱

闭包陷阱

js
function foo(name){
    function bar(){
        console.log(name)
    }
    return bar
}

const bar1 = foo("why")
bar1() // why
bar1() // why

// 这里foo函数传入了新的参数,但是bar1依然是why,形成了闭包陷阱
const bar2 = foo("kobe")
bar2() // kobe
bar1() // why

正确的代码应该如下,但是这样当count改变的时候依然会触发子组件重新渲染

jsx
import {memo, useCallback, useState} from "react";
import Child from "./Child,jsx";

const App = memo(() => {

    const [count, setCount] = useState(100)
    const [message, setMessage] = useState("hello world")

    const increment = useCallback(() => {
        setCount(count + 1)
    }, [count])

    console.log("App is render")

    return (
        <div>
            <div>count:{count}</div>
            <button onClick={() => {
                increment()
            }}>+1
            </button>

            <div>message:{ message }</div>
            <button onClick={e=>{ setMessage(Math.random) }}>改变message</button>

            <Child increment={increment}></Child>
        </div>
    )
})

export default App

方式二

可以利用useRef结合useCallback进行优化,可以解决闭包陷阱,useRef,在组件多次渲染的时候,返回的是同一个值,通常useRef可以解决大部分闭包陷阱

jsx
import {memo, useCallback, useRef, useState} from "react";
import Child from "./Child,jsx";

const App = memo(() => {

    const [count, setCount] = useState(100)
    const [message, setMessage] = useState("hello world")

    const countRef = useRef();
    countRef.current = count;

    // 进一步优化:当count发生改变的时候,也使用同一个函数
    // 做法一:将count依赖移除掉,但是会发生闭包陷阱,导致count无法更新
    // 做法二:useRef,在组件多次渲染的时候,返回的是同一个值
    const increment = useCallback(() => {
        setCount(countRef.current + 1)
    }, [])

    console.log("App is render")

    return (
        <div>
            <div>count:{count}</div>
            <button onClick={() => {
                increment()
            }}>+1
            </button>

            <div>message:{ message }</div>
            <button onClick={e=>{ setMessage(Math.random) }}>改变message</button>

            <Child increment={increment}></Child>
        </div>
    )
})

export default App

总结

在父组件传递给子组件一个函数时,最好使用useCallback进行一个包裹,这样可以避免子组件多次渲染的问题

useMemo

useCallback缓存(记忆)的是函数,而useMemo缓存的则是函数的返回值,类似vue中的计算属性

以下示例每次我们点击count++按钮时,count改变,都会触发函数重新渲染,然后重新执行totalNum(100),存在性能上的浪费,可使用useMemo进行优化

代码示例:

jsx
import {memo, useMemo, useState} from "react";

function totalNum(num){
    console.log("totalNum 计算总和")
    let total = 0
    for(let i = 1; i <= num; i++){
        total += i
    }
    return total
}

const App = memo(() => {
    const [count, setCount] = useState(10)

    let result = totalNum(100)

    return (
        <div>
            <div>计算结果:{result}</div>
            <div>count:{count}</div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
        </div>
    )
})

export default App

使用useMemo进行优化,优化后totalNum函数只会执行一次,点击count++也只会执行一次,因为result值没有发生改变,这里useMemo第一个参数是回调函数,返回的是一个函数执行结果,第二个参数是依赖项,在依赖值没有改变的时候,回调函数咋不会重新执行,如果没有依赖项可以传入空数组

jsx
import {memo, useMemo, useState} from "react";

function totalNum(num){
    console.log("totalNum 计算总和")
    let total = 0
    for(let i = 1; i <= num; i++){
        total += i
    }
    return total
}

const App = memo(() => {
    const [count, setCount] = useState(10)

    let result = useMemo(() => {
        return totalNum(100)
    },[])

    return (
        <div>
            <div>计算结果:{result}</div>
            <div>count:{count}</div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
        </div>
    )
})

export default App

如果result结果依赖于count,那么可以添加count作为依赖项,这样count值发生改变就会重新执行totalNum函数,如果没有改变则不会重新执行

jsx
import {memo, useMemo, useState} from "react";

function totalNum(num){
    console.log("totalNum 计算总和")
    let total = 0
    for(let i = 1; i <= num; i++){
        total += i
    }
    return total
}

const App = memo(() => {
    const [count, setCount] = useState(10)

    let result = useMemo(() => {
        return totalNum(count)
    },[count])

    return (
        <div>
            <div>计算结果:{result}</div>
            <div>count:{count}</div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
        </div>
    )
})

export default App

useCallbackuseMemo对比:

jsx
const increment = useCallback(fn,[]) 
// 等同于
const increment = useMemo(() => fn,[])

对子组件渲染优化

假设我们有一个对象传递到子组件Child中,这时候点击count++那么会执行setCount,然后重新渲染App,那么子组件中也会重新执行渲染函数

jsx
import {memo, useMemo, useState} from "react";


const App = memo(() => {
    const [count, setCount] = useState(10)

    const info = {name: "张三",age:20}

    return (
        <div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
            <Child info={info}/>
        </div>
    )
})

export default App

可以使用useMemo进行优化,此时依赖没有改变,那么每次执行setCount就不会重新渲染

对子组件传递相同内容的对象时,使用useMemo进行性能的优化

jsx
import {memo, useMemo, useState} from "react";


const App = memo(() => {
    const [count, setCount] = useState(10)

    const info = useMemo(() => ({name: "张三",age:20}),[]

    return (
        <div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
            <Child info={info}/>
        </div>
    )
})

export default App

useRef

获取dom对象

jsx
import {memo, useRef, useState} from "react";

const App = memo(() => {
    const inputRef = useRef();

    const getInputDom = () => {
        console.log(inputRef.current);
        inputRef.current.style.color = 'red'
        inputRef.current.value = '张三';
        inputRef.current.focus()
    }

    return (
        <div>
            <input type="text" ref={inputRef}/>
            <button onClick={ () => { getInputDom() } }>获取ref</button>
        </div>
    )
})

export default App

解决闭包陷阱

useRef保存一个数据,这个对象在整个生命周期中可以保存不变

验证useRef的不变性,以下示例第一次输出false,后续点击都输出true

jsx
import {memo, useRef, useState} from "react";

let obj = null

const App = memo(() => {
    const [count, setCount] = useState(10)
    const nameRef = useRef()
    console.log(obj == nameRef)
    obj= nameRef

    return (
        <div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
        </div>
    )
})

export default App

解决闭包陷阱

利用useRef特性也可以解决闭包陷阱

jsx
import {memo, useCallback, useRef, useState} from "react";


const App = memo(() => {
    const [count, setCount] = useState(10)

    // 通过ref解决闭包陷阱
    const countRef = useRef()
    countRef.current = count

    const increment = useCallback(() => {
        console.log("执行increment")
        setCount(countRef.current + 1)
    },[])

    return (
        <div>
            <h2>count: { count }</h2>
            <button onClick={e=>{increment()}}>count++</button>
        </div>
    )
})

export default App

useImperativeHandle

回顾之前的forwardRef用法

jsx
import {forwardRef, memo, useRef} from "react";

const Input = memo(forwardRef((props, ref) => {
    return (
        <input type="text" ref={ref}/>
    )
}))

const App = memo(() => {
    const inputRef = useRef()

    function handleDOM() {
        inputRef.current.focus()
        inputRef.current.value = "张三"
    }

    return (
        <div>
            <Input ref={inputRef}/>
            <button onClick={() => { handleDOM() }}>focus</button>
        </div>
    )
})

export default App

forwardRef本身做法没有问题,但是我们将子组件DOM直接暴露给了父组件,这样父组件可以拿到DOM做任意操作,会导致情况不可控,开发中显然不建议这样做

使用useImperativeHandle我们可以给父组件的ref暴露指定的操作,例如只允许focus,不允许直接修改输入框的值,要使用子组件提供的setValue才可以正常修改,应该这样修改

jsx
import {forwardRef, memo, useImperativeHandle, useRef} from "react";

const Input = memo(forwardRef((props, ref) => {

    // 重新定义一个内部的ref,避免直接绑定父组件传入进来的ref
    const inputRef = useRef()

    useImperativeHandle(ref,() => {
        return {
            focus(){
                inputRef.current.focus()
            },
            setValue(value){
                inputRef.current.value = value
            }
        }
    })

    return (
        <input type="text" ref={inputRef}/>
    )
}))

const App = memo(() => {
    const inputRef = useRef()

    function handleDOM() {
        inputRef.current.focus()
        inputRef.current.setValue("张三")
    }

    return (
        <div>
            <Input ref={inputRef}/>
            <button onClick={() => { handleDOM() }}>focus</button>
        </div>
    )
})

export default App

useLayoutEffect

useLayoutEffectuseEffect的区别就在于执行完组件的render函数后,即将将DOM打印(呈现)到屏幕上,useLayoutEffect会在呈现在屏幕上之前执行,在这里可以进行一些数据更新调整,useEffect会在呈现在屏幕之后执行

App.jsx

jsx
import {memo, useEffect, useLayoutEffect, useState} from "react";

const App = memo(() => {
    const [count, setCount] = useState(10)

    useEffect(() => {
        console.log("useEffect")
    }, []);

    useLayoutEffect(() => {
        console.log("useLayoutEffect")
    }, []);

    console.log("render")

    return (
        <div>
            <div>count:{count}</div>
            <button onClick={e=>{setCount(count+1)}}>count++</button>
        </div>
    )
})

export default App

执行顺序:

render

useLayoutEffect

useEffect

一个简单示例,页面上有一个更新的操作将count设置为0,然后在useEffect中监听,如果设置为0则重新赋值,会发现这里用useEffect赋值的时候会出现屏幕闪烁的情况,可以发现count的值先渲染成为了0之后才重新渲染的

jsx
import {memo, useEffect, useLayoutEffect, useState} from "react";

const App = memo(() => {
    const [count, setCount] = useState(10)

    // 默认情况下不设置依赖项,初始化以及数据更新都会重新调用
    useEffect(() => {
        console.log("useEffect")
        if (count === 0) {
            setCount(Math.random() + 99)
        }
    });

    console.log("render")

    return (
        <div>
            <div>count:{count}</div>
            <button onClick={e => {
                setCount(0)
            }}>count++
            </button>
        </div>
    )
})

export default App

如果使用useLayoutEffect则不会出现这种情况

jsx
import {memo, useEffect, useLayoutEffect, useState} from "react";

const App = memo(() => {
    const [count, setCount] = useState(10)

    // 默认情况下不设置依赖项,初始化以及数据更新都会重新调用
    useLayoutEffect(() => {
        console.log("useLayoutEffect")
        if (count === 0) {
            setCount(Math.random() + 99)
        }
    });

    console.log("render")

    return (
        <div>
            <div>count:{count}</div>
            <button onClick={e => {
                setCount(0)
            }}>count++
            </button>
        </div>
    )
})

export default App

自定义hook

注意:自定义hook必须以use开头,例如useXXX

日志打印

需求:所有的组件在创建和销毁时都进行打印

jsx
import {memo, useEffect, useState} from "react";

function useComponentLog(name){
    useEffect(() => {
        console.log(`${name}组件创建`)
        return ()=>{
            console.log(`${name}组件销毁`)
        }
    },[])
}

const Home = memo(() => {
    useComponentLog("home")
    return (
        <h2>home</h2>
    )
})

const Profile = memo(() => {
    useComponentLog("profile")
    return (
        <h2>profile</h2>
    )
})


const App = memo(() => {
    const [showComponent, setShowComponent] = useState(true)

    return (
        <div>
            <button onClick={e => {
                setShowComponent(!showComponent)
            }}>切换显示
            </button>

            { showComponent && <Home/> }
            { showComponent && <Profile/> }
        </div>
    )
})

export default App

Context共享

需求:共享token以及用户信息

index.jsx

jsx
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <TokenContext.Provider value={"asasssdzxzxassass"}>
        <UserContext.Provider value={{name: '李四', age: 30}}>
            <App/>
        </UserContext.Provider>
    </TokenContext.Provider>
);

自定义hook

jsx
import {memo, useContext, useEffect, useState} from "react";
import userContext from "./context/UserContext";
import tokenContext from "./context/TokenContext";

/**
 * 获取token以及user信息
 */
function useUserAndToken() {
    const user = useContext(userContext)
    const token = useContext(tokenContext)

    return [user, token]
}

const Home = memo(() => {
    const [user, token] = useUserAndToken()
    return (
        <div>
            <h2>home</h2>
            <div>token: {token}</div>
            <div>user: {JSON.stringify(user)}</div>
        </div>
    )
})

const Profile = memo(() => {
    const [user, token] = useUserAndToken()
    return (
        <div>
            <h2>Profile</h2>
            <div>token: {token}</div>
            <div>user: {JSON.stringify(user)}</div>
        </div>
    )
})


const App = memo(() => {
    return (
        <div>
            <Home/>
            <Profile/>
        </div>
    )
})

export default App

获取滚动位置

需求:获取当前页面的滚动位置

jsx
import {memo, useEffect, useState} from "react";

/**
 * 获取页面滚动位置
 */
function useScrollPosition() {
    const [scrollPosition, setScrollPosition] = useState(0)

    const handleScroll = () => {
        setScrollPosition(window.scrollY)
    }

    useEffect(() => {
        document.addEventListener("scroll", handleScroll)
    }, []);

    return scrollPosition
}

const Home = memo(() => {
    const position = useScrollPosition()
    return (
        <div>
            <h2>home</h2>
            <h2>position: {position}</h2>
        </div>
    )
})

const Profile = memo(() => {
    const position = useScrollPosition()
    return (
        <div>
            <h2>Profile</h2>
            <h2>position: {position}</h2>
        </div>
    )
})


const App = memo(() => {
    return (
        <div style={{overflowY: 'auto'}}>
            <div style={{height: '2000px'}}>
                <Home/>
                <Profile/>
            </div>
        </div>
    )
})

export default App

localStorage数据存储

需求,封装一个storage存储工具函数,可以设置以及获取storage中的值

jsx
import {memo, useEffect, useState} from "react";

/**
 * 抛出两个返回结果,根据key获取到的值,设置值函数
 */
function useLocalStorage(key) {
    const [data, setData] = useState(() => {
        const item = localStorage.getItem(key)
        if(!item) return ""
        return JSON.parse(item);
    })

    useEffect(() => {
        console.log("执行useEffect")
        localStorage.setItem(key, JSON.stringify(data))
    }, [data]);

    return [data, setData]
}

const Home = memo(() => {
    const [token, setToken] = useLocalStorage("token")
    return (
        <div>
            <h2>home</h2>
            <h2>token: {token}</h2>
            <button onClick={() => {
                setToken("测试token")
            }}>重新设置值</button>
        </div>
    )
})


const App = memo(() => {
    return (
        <div>
            <Home/>
        </div>
    )
})

export default App

redux-hooks

react18新增了关于reduxhooks,之前我们组件中要使用redux需要使用connect函数,其原理是将statedispatch传入到props中,之前的使用方式较为繁琐,需要定义mapStateToPropsmapDispatchToProps

使用connect函数使用redux

jsx
import React, {memo, PureComponent} from 'react';
import {connect} from "react-redux";
import {addNumber} from "./store/modules/counter";

const App = memo((props) => {
    return (
        <div>
            <h2>
                Home counter: {props.count}
            </h2>
            <div>
                <button onClick={() => {
                    props.addNumber(1)
                }}>+1
                </button>

                <button onClick={() => {
                    props.addNumber(5)
                }}>+5
                </button>
            </div>
        </div>
    )
})

const mapStateToProps = (state) => ({
    count: state.counter.count
})

const mapDispatchToProps = (dispatch) => ({
    addNumber: (count) => dispatch(addNumber(count))
})

export default connect(mapStateToProps, mapDispatchToProps)(App);

接下来使用hooks进行改造

jsx
import React, {memo, PureComponent} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {addNumber} from "./store/modules/counter";

const App = memo((props) => {

    const { count } = useSelector((state) => ({
        count: state.counter.count
    }));

    const dispatch = useDispatch();
    function incrementCount(num){
        dispatch(addNumber(num))
    }

    return (
        <div>
            <h2>
                Home counter: {count}
            </h2>
            <div>
                <button onClick={() => {
                    incrementCount(1)
                }}>+1
                </button>

                <button onClick={() => {
                    incrementCount(5)
                }}>+5
                </button>
            </div>
        </div>
    )
})

export default App;

性能优化

在下面的例子中,我们点击+1按钮调用incrementCount函数,触发App组件重新渲染,同时我们发现子组件Home也重新渲染了,这也会引起页面性能问题,之所以会渲染是因为 useSelector机制是state中的任意一个数据变化都会重新引起组件的渲染

jsx
import React, {memo, PureComponent} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {addNumber} from "./store/modules/counter";

const Home = memo(() => {
    const { message } = useSelector((state) => ({
        message: state.counter.message
    }));

    console.log("Home render");

    return (
        <div>
            <h2>home</h2>
            <div>message: { message }</div>
        </div>
    )
})

const App = memo((props) => {

    const { count } = useSelector((state) => ({
        count: state.counter.count
    }));

    const dispatch = useDispatch();
    function incrementCount(num){
        dispatch(addNumber(num))
    }

    console.log("App render")

    return (
        <div>
            <h2>
                Home counter: {count}
            </h2>
            <div>
                <button onClick={() => {
                    incrementCount(1)
                }}>+1
                </button>

                <button onClick={() => {
                    incrementCount(5)
                }}>+5
                </button>
            </div>
            <div>
                <Home/>
            </div>
        </div>
    )
})

export default App;

进行优化,useSelector可以接收一个函数,用于比较两个值,可以用react-redux提供的shallowEqual(前面SCU有讲到,是做了一个浅层比较),优化或如果Home组件中使用的state值没有变化那么久不会再引起Home组件的重新渲染

jsx
import React, {memo, PureComponent} from 'react';
import {shallowEqual, useDispatch, useSelector} from "react-redux";
import {addNumber} from "./store/modules/counter";

const Home = memo(() => {
    const { message } = useSelector((state) => ({
        message: state.counter.message
    }),shallowEqual);

    console.log("Home render");

    return (
        <div>
            <h2>home</h2>
            <div>message: { message }</div>
        </div>
    )
})

const App = memo((props) => {

    const { count } = useSelector((state) => ({
        count: state.counter.count
    }));

    const dispatch = useDispatch();
    function incrementCount(num){
        dispatch(addNumber(num))
    }

    console.log("App render")

    return (
        <div>
            <h2>
                Home counter: {count}
            </h2>
            <div>
                <button onClick={() => {
                    incrementCount(1)
                }}>+1
                </button>

                <button onClick={() => {
                    incrementCount(5)
                }}>+5
                </button>
            </div>
            <div>
                <Home/>
            </div>
        </div>
    )
})

export default App;

上次更新于: