react Hook之useState、useEffect、useContext

2,145 阅读5分钟

useState

基本结构

const [state, setState] = useState(initState);

1,state为你要设置的状态

2,setState为更新state的方法,命名随意

3,initState为初始的state,可以是任意数据类型,也可以是回调函数,但必须有返回值

import { useState } from "react";
const Greeting = (props) => {
    const [name, setName] = useState('li');
    const [subname, setSubName] = useState('lisha');

    function handleNameChange(e){
        setName(e.target.value)
    }  
    function handleSubNameChange(e) {
        setSubName(e.target.value)
    }  
    return (
        <React.Fragment>
            <p>{name}</p>
            <input onChange={handleNameChange}/>
            <p>{subname}</p>
            <input onChange={handleSubNameChange} />
        </React.Fragment>
    )
}
export default Greeeting;

调用useState,传入初始值,通过数组的结构赋值得到独立的local state name,及setName。name可以理解为class.component中的state,可见这里的state不局限于对象,可以为number,string,当然也可以是一个对象。而setCount可以理解为class.component中的setState,不同的是setState会merge新老state,而hook中的set函数会直接替换,这就意味着如果state是对象时,每次set应该传入所有属性,而不能像class.component那样仅传入变化的值。所以在使用useState时,尽量将相关联的,会共同变化的值放入一个object。

我们知道当函数执行完毕,函数作用域内的变量都会销毁,hooks中的state在component首次render后被React保留下来了。那么在下一次render时,React如何将这些保留的state与component中的local state对应起来呢。

const stateArr = []
const setterArr = []
let cursor = 0
let isFirstRender = true

function createStateSetter(cursor) {
  return state => {
    stateArr[cursor] = state
  }
}

function useState(initState) {
  if (isFirstRender) {
    stateArr.push(initState)
    setterArr.push(createStateSetter(cursor))

    isFirstRender = false
  }

  const state = stateArr[cursor]
  const setter = setterArr[cursor]

  cursor++

  return [state, setter]
}

可以看出React需要保证多个hooks在component每次render的时候的执行顺序都保持一致,否则就会出现错误。这也是React hooks rule中必须在top level使用hooks的由来————条件,遍历等语句都有可能会改变hooks执行的顺序。

useEffect

基本结构

useEffect(callback,array)

1,callback:回调函数,用于处理副作用逻辑。

2,array(可选):一个数组,用于控制执行。

useEffect 会在每一次render之后执行,包括初始render和更新render。不阻塞渲染。

react中有两种常见的副作用,一种是需要清理的,一种是不需要清理的:

1、网络请求、DOM修改和日志记录等,这些是不需要清理的。useEffect会自动处理。

2、订阅和取消订阅,事件监听和取消事件监听,这种是需要清理的。

下面是useEffect清理机制:

1、useEffect在每次执行之前都会自动清理之前的effect。

2、effect中可以返回一个函数用于清理工作

//class.Component写法
class Greeting extends React.PureComponent{
    static contextType = ThemeContext;
    state={
        name: 'li',
        subname: 'sha',
        width: window.innerWidth,
    }
    componentDidMount(){
        document.title = this.state.name + ' ' + this.state.subname;
        window.addEventListener('resize', this.handleWidthChange);
    }
    componentDidUpdate(){
        document.title = this.state.name + ' ' + this.state.subname;
    }
    componentWillMount(){
        window.removeEventListener('resize', this.handleWidthChange);
    }
    handleWidthChange = () => {
        this.setState({width: window.innerWidth});
    }
    handleNameChange = (e)=>{
        this.setState({name: e.target.value});
    }
    handleSubNameChange = (e) => {
        this.setState({ subname: e.target.value });
    }
    render(){
        const theme = this.context;
        return(
            <div style={{ ...theme}}>
                <p>{this.state.name}</p>
                <input onChange={this.handleNameChange}/>
                <p>{this.state.subname}</p>
                <input onChange={this.handleSubNameChange} />
                <p>width:</p>
                <p>{this.state.width}</p>
            </div>
        );
    }
}

useEffect Hook的用法

在传入useEffect的函数(effect)中做了一些"side effect",在class.component中我们通常会在componentDidMount,componentDidUpdate中去做这些事情。另外在class.component中,需要在componentDidMount中订阅,在componentWillUnmount中取消订阅,这样将一件事拆成两件事做,不仅可读性低,还容易产生bug。hook中就会把相关联的事情放在一起处理。

const Greeting = (props) => {
    const name = useFormInput('li');
    const subname = useFormInput('lisha');
    const theme = useContext(ThemeContext);
    const width = useWindowWidth();
    
    useDocumentTitle(name.value + ' ' + subname.value);

    return (
        <div style={{...theme}}>
            <p>{name.value}</p>
            <input {...name}/>
            <p>{subname.value}</p>
            <input {...subname} /> 
            <p>width:</p>
            <p>{width}</p>
        </div>
    )
}
function useFormInput(initValue){
    const [value, setValue] = useState(initValue);
    
    function handleChange(e) {
        setValue(e.target.value)
    } 
    return {
        value,
        onChange: handleChange,
    };
}
function useDocumentTitle(title){
    useEffect(() => {
        document.title = title;
    }, [title]);
}
function useWindowWidth(){
    const [width, setWidth] = useState(window.innerWidth);
    useEffect(() => {
        const handleWidthChange = () => {
            setWidth(window.innerWidth);
        }
        window.addEventListener('resize', handleWidthChange);

        return () => {
            window.removeEventListener('resize', handleWidthChange);
        };
    }, [width]);
    return width;
}

每次render后都会执行effect,这样会不会对性能造成影响。其实effect是在页面渲染完成之后执行的,不会阻塞,而在effect中执行的操作往往不要求同步完成,除了少数如要获取宽度或高度,这种情况需要使用其他的hook(useLayoutEffect),此处不做详解。即使这样,React也提供了控制的方法,及useEffect的第二个参数————一个数组,如果数组中的值不发生变化的话就跳过effect的执行。

useContext

//Context.tsx
import React from 'react';

export const themes = {
    light: {
        color: '#ffffff',
        background: '#222222',
    },
    dark: {
        color: '#000000',
        background: '#eeeeee',
    },
};

export const ThemeContext = React.createContext(themes.light);
//Page.tsx
import { useState, useEffect, useContext} from "react";
import { ThemeContext, themes} from './Context';

const Page = (props) => {
    let [theme, setTheme] = useState('light');
    return (
        <>
        <ThemeContext.Provider value={themes[theme]}>
            <button onClick={(e) => { setTheme('dark') }}>dark</button>
            <button onClick={(e) => { setTheme('light') }}>light</button>
            <Greeting />
        </ThemeContext.Provider>
        </>
    );
};
export default Page;

class.Component写法

class Greeting extends React.PureComponent{
    static contextType = ThemeContext;
    state={
        name: 'li',
        subname: 'sha',
    }
    handleNameChange = (e)=>{
        this.setState({name: e.target.value});
    }
    handleSubNameChange = (e) => {
        this.setState({ subname: e.target.value });
    }
    render(){
        const theme = this.context;
        return(
            <div style={{ ...theme}}>
                <p>{this.state.name}</p>
                <input onChange={this.handleNameChange}/>
                <p>{this.state.subname}</p>
                <input onChange={this.handleSubNameChange} />
            </div>
        );
    }
}

useContext Hook写法

const Greeting = (props) => {
    const [name, setName] = useState('li');
    const [subname, setSubName] = useState('lisha');
    const theme = useContext(ThemeContext);
    function handleNameChange(e){
        setName(e.target.value)
    }  
    function handleSubNameChange(e) {
        setSubName(e.target.value)
    }  
    return (
        <div style={{...theme}}>
            <p>{name}</p>
            <input onChange={handleNameChange}/>
            <p>{subname}</p>
            <input onChange={handleSubNameChange} />
        </div>
    )
}

useWindowWidth和useDocumentTitle、useFormInput是custom hook,因custom hook复用的是stateful logic,而不是state本身。另外custom hook必须以use开头来命名,这样linter工具才能正确检测其是否符合规范。

注意、注意、注意:

1、不能在条件语句、循环语句中使用钩子,它必须在顶层。

2、自定义钩子函数必须使用use开头