React Hook - 对比Class组件理解学习

React Hook

版本React 16.8及以上

使用规则:

  • 只能在函数最外层调用Hook。不要在循环、条件判断或者子函数中调用。(React依靠hook的调用顺序来确定哪个state对应哪个hook。因此在写代码的过程中,要保证hook的调用顺序在每次渲染中都是相同的。确保它能正常运行)
  • 只能在 React 的函数组件中调用 Hook。 在自定义 Hook 中调用其他 Hook.不要在其他 JavaScript 函数中调用。

useState:

const [xxx, setxxx] = useState(initialState)

Class组件写法

import React, { Component } from 'react';

class Example extends Component{
    constructor(){
        super();
        this.state = {
            count: 0
        }
    }

    render() {
        return (
            <div>
                <h2>You clicked { this.state.count } times</h2>
                <button onClick={() => {this.setState({count: this.state.count + 1})}}>Click me</button>
            </div>
        );
    }
}

export default Example; 

Hook写法

import React, { useState } from 'react';

function Example() {
    const [count, setCount] = useState(0);

    return(
        <div>
            <h2>You clicked { count } times</h2>
            <button onClick={() => {setCount(count + 1)}}>Click me</button>
        </div>
    );
}

export default Example;

知识点:

  • useState
    只有唯一的参数,用于给state初始化赋值。两个返回值,一个是state,一个是用于更新state的函数(可以理解为每个state有了自己的this.setState.).
    通常我们使用数组解构赋值来获取这两个返回值, 例如: const [count, setCount] = useState(0);
  • useState需成对出现,需要多少state和对应的更新state函数,就声明多少次。
  • React依靠hook的调用顺序来确定哪个state对应哪个useState。因此在写代码的过程中,要保证hook的调用顺序在每次渲染中都是相同的。确保它能正常运行
  • useState不会自动合并更新对象,需要用函数式的方式来达到效果
setCount(prevCount => {
  // 也可以使用 Object.assign
  return {...prevCount, ...updatedValues};
});
  • state初始化参数既可以是值,也可以是一个函数(返回初始的state)。此函数只在初始渲染时被调用。
const [count, setCount] = useState(() => {
  const initialCount = someComputation(props);
  return initialCount;
});

useEffect

无需清除的effect
比如发送网络请求,手动变更 DOM,记录日志、

需要清除的effect
例如订阅外部数据源,监听器,定时器

无需清除的effect
Class组件写法

import React, {Component} from 'react';

class Example extends Component{
    constructor(props){
        super(props);
        this.state = {
            count: 0
        }
    }

    componentDidMount(){
        console.log(`You clicked ${this.state.count} times`);
    }

    componentDidUpdate(){
        console.log(`You clicked ${this.state.count} times`);
    }

    render(){
        return(
            <div>
                <h2>You clicked {this.state.count} times</h2>
                <button onClick={() => {this.setState({ count: this.state.count + 1})}}>Click me</button>
            </div>
        );
    }
}

export default Example;

Hook

import React, { useState, useEffect } from 'react';

function Example() {
    const [count, setCount] = useState(0);

    useEffect(()=>{
       console.log(`You clicked ${count} times`);
    });

    return(
        <div>
            <h2>You clicked {count} times</h2>
            <button onClick={() => {setCount(count + 1)}}>Click me</button>
        </div>
    );
}

export default Example;

需清除的effect
Class组件写法

import React, {Component} from 'react';

class Example extends Component{
    constructor(props){
        super(props);
        this.state = {
            innerWidth: 0,
            innerHeight: 0
        };
        this.handleWindowSizeChange = this.handleWindowSizeChange.bind(this);
    }

    componentDidMount(){
        window.addEventListener('resize', this.handleWindowSizeChange);
    }

    componentWillUnmount(){
        window.removeEventListener('resize', this.handleWindowSizeChange);
    }

    handleWindowSizeChange(){
        this.setState({
            innerWidth: window.innerWidth,
            innerHeight: window.innerHeight
        })
    }

    render(){
        return(
            <div>
                <h2>
                    window innerWidth: {this.state.innerWidth}<br/>
                    window innerHeight: {this.state.innerHeight}
                </h2>
            </div>
        );
    }
}

export default Example;

Hook

import React, { useState, useEffect } from 'react';

function Example() {
    const [windowSize, setWindowSize] = useState({innerWidth: 0, innerHeight: 0});

    useEffect(()=>{
        window.addEventListener('resize', handleWindowSizeChange);
        return () => window.removeEventListener('resize', handleWindowSizeChange);
    }, []); //空数组作为第二个参数,表明执行只运行一次的effect。等同于告诉React我的effect不依赖于props或state中的任何值(仅在组件挂载和卸载时执行)

    function handleWindowSizeChange() {
        setWindowSize(
            {
                innerWidth: window.innerWidth,
                innerHeight: window.innerHeight
            }
        )
    }

    return(
        <div>
            <h2>
                window innerWidth: {windowSize.innerWidth}<br/>
                window innerHeight: {windowSize.innerHeight}
            </h2>
        </div>
    );
}

export default Example;

知识点:

  • useEffect可以看作是componentDidMount, componentDidUpdate 和
    componentWillUnmount三个生命周期函数的组合
  • 不添加第二个参数:相当于ComponentDidMount + ComponentDidUpdate; 会一直执行匿名函数里的操作。
  • 空数组作为第二个参数,表明执行只运行一次的effect。等同于告诉React我的effect不依赖于props或state中的任何值(仅在组件挂载和卸载时执行)
  • 推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps
    规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useEffect(()=>{
   console.log(`You clicked ${count} times`);
});
  • 第二个参数里传入state: 仅在count更新时更新
useEffect(()=>{
   console.log(`You clicked ${count} times`);
}, [count]);

useContext

用于父子组件传值及跨越多个组件传值, 可以避免多次中间元素传递 props

Context 在class组件用法

import { createContext } from 'react';

export const ParentContext = createContext();
import React, { Component } from 'react';
import { ParentContext } from './CommonContext'
import Child from "./Child";

class Parent extends Component{
    constructor(){
        super();
        this.state = {
            count: 0
        }
    }

    render() {
        return (
            <div>
                <h2>You clicked { this.state.count } times</h2>
                <button onClick={() => {this.setState({count: this.state.count + 1})}}>Click me</button>
                <ParentContext.Provider value={this.state.count}>
                    <Child/>
                </ParentContext.Provider>
            </div>
        );
    }
}

export default Parent;
import React, { Component } from 'react';
import { ParentContext } from './CommonContext';

class Child extends Component{
    static contextType =  ParentContext;

    render() {
        return (
            <div>
                <h2>{this.context}</h2>
            </div>
        );
    }
}

export default Child;

注意: 不要把createContext放在父级组件里面,否则会发生
Uncaught ReferenceError: Cannot access ‘ParentContext’ before initialization
原因是:造成了循环依赖。Index引用 Parent组件, Parent组件引用了Child组件。Child引用了 Parent组件中创建的’ParentContext’.
正确做法: 是单独创建一个文件保存Context. Parent和Child都从该文件中引用context

Context 在Hooks用法

import React, {useState, createContext} from 'react';
import Child from "./Child";

export const ParentContext = createContext();

function Parent() {
    const [count, setCount] = useState(0);

    return(
      <div>
          <h2>You click {count} times</h2>
          <button onClick={() => {setCount(count + 1)}}>Click me</button>
          <ParentContext.Provider value={count}>
              <Child/>
          </ParentContext.Provider>
      </div>
    );
}

export default Parent;
import React, {useContext} from 'react';
import {ParentContext} from './Parent';

function Child() {
    let count = useContext(ParentContext);

    return(
        <h2>{count}</h2>
    )
}

export default Child;

知识点:

  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext
    或者
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context
    的变化。你仍然需要在上层组件树中使用 来为下层组件提供 context。

useReducer

import React, { useReducer } from 'react';

function useReducerDemo() {
    const [state, dispatch] = useReducer((state, action)=>{
        switch (action) {
            case 'add':
                return state + 1;
            case 'sub':
                return state - 1;
            default:
                return state;
        }
    }, 0);

    return(
        <div>
            <p>current count is {state}</p>
            <button onClick={() => {dispatch('add')}}>Add</button>
            <button onClick={() => {dispatch('sub')}}>Subtract</button>
        </div>
    )
}

export default useReducerDemo;

知识点:

const [state, dispatch] = useReducer(reducer, initialArg, init);

第一个参数reducer可以是匿名函数,也可以是普通函数。
第二个参数initialArg 设置state的初始值
第三个参数init 惰性初始化。引用组件时再传入初始值。

惰性初始化

ReactDOM.render(<Counter initialCount={0} />, document.getElementById('root'));
import React, { useReducer } from 'react';

function init(initialCount) {
    return {count: initialCount};
}

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        case 'reset':
            return init(action.payload);
        default:
            throw new Error();
    }
}

function Counter({initialCount}) {
    const [state, dispatch] = useReducer(reducer, initialCount, init);
    return (
        <>
            Count: {state.count}
            <button
                onClick={() => dispatch({type: 'reset', payload: initialCount})}>

                Reset
            </button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
    );
}

export default Counter;

useReducer(实现类似Redux中Reducer的功能) + useContext(状态全局化)实现Redux

TodoList example – useReducer + useContext

import React from 'react';
import {TodoReducer} from "./TodoReducer";
import TodoList from "./TodoList";

function HookDemo() {
    return(
        <TodoReducer>
            <TodoList/>
        </TodoReducer>
    )
}

export default HookDemo;
import React, { useContext } from 'react';
import {TodoContext, ADD_ITEM} from './TodoReducer';

let inputVal;

function TodoList() {
    const {state, dispatch} = useContext(TodoContext);

    return(
        <div>
                <input type="text" id='todo' ref={el => inputVal = el}/>
                <button onClick={() => {dispatch({type: ADD_ITEM, payload: {id: state.nextId + 1, newItem: {id: state.nextId + 1, value: inputVal.value}}})}}>add Todo</button>
                <ul>
                    {
                        state.itemList.map(item => (
                            <li key={item.id}>{item.value}</li>
                        ))
                    }
                </ul>
        </div>
    )
}

export default TodoList;
import React, {createContext, useReducer} from 'react';

export const ADD_ITEM = 'add_item';
export const TodoContext = createContext([]);

const reducer = (state, action) => {
    switch (action.type) {
        case ADD_ITEM:
            return {
                ...state,
                nextId: action.payload.id,
                itemList: [...state.itemList, action.payload.newItem]
            };
        default:
            return state;
    }
};

const initialState = {
    nextId: 0,
    itemList: []
};

export const TodoReducer = props => {
    const[state, dispatch] = useReducer(reducer, initialState);

    return(
        <TodoContext.Provider value={{state, dispatch}}>
            {props.children}
        </TodoContext.Provider>
    );
};

TodoList example – React + Redux

import React, {Component} from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import TodoList from './TodoList';
import todo from './reducer';

const store = createStore(todo);

class ReduxDemo extends Component{
    render() {
        return (
            <Provider store={store}>
                <TodoList/>
            </Provider>
        );
    }

}

export default ReduxDemo;
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {addNewItem} from "./actions";

class TodoList extends Component{
    constructor(){
        super();
        this.inputVal = null;
    }

    handleBtnClick(){
        this.props.AddItem(this.inputVal.value);
    }

    render() {
        return(
            <div>
                <input type="text" ref={el => this.inputVal = el}/>
                <button onClick={this.handleBtnClick.bind(this)}>Add Item</button>
                <ul>
                    {
                        this.props.todoList.list.map(item => (
                            <li>{item}</li>
                        ))
                    }
                </ul>
            </div>
        );
    }
}

const mapStateToProps = state => ({
    todoList: state
});

const mapDispatchToProps = dispatch => {
    return{
        AddItem: (inputVal) =>{
            dispatch(addNewItem(inputVal));
        }
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
export const ADD_ITEM = 'add_item';

export const addNewItem = (inputVal) => ({
    type: ADD_ITEM,
    payload: inputVal
});
import {ADD_ITEM} from "./actions";

const initialState = {
    list: [
        'add item here'
    ]
};

const todo =  (state = initialState, action) => {
    let newList = state.list;
    switch (action.type) {
        case ADD_ITEM:
            newList.push(action.payload);
            return{
                ...state,
                list: newList
            };
        default:
            return state;
    }
};

export default todo;

useRef

const refContainer = useRef(initialValue);

Hooks写法

import React, {useRef} from 'react';

function UseRef() {
    let inputRef = useRef( null);
    const handleClick = () => {
        alert(`input value is ${inputRef.current.value}`);
    };
  return(
      <div>
          <input type="text" ref={inputRef}/>
          <button onClick={handleClick}>Get Input Value</button>
      </div>
  )
}

export default UseRef;

Class组件 createRef法 (适用于版本React 16.3以上)

import React, { Component } from 'react';

class RefDemo extends Component{
    constructor(props){
        super(props);
        this.inputRef = React.createRef();
    }

    handleClick = () => {
        alert(`input value is ${this.inputRef.current.value}`);
    };

    render() {
        return(
            <div>
                <input type="text" ref={this.inputRef}/>
                <button onClick={this.handleClick}>Get Input Value</button>
            </div>
        );
    }
}

export default RefDemo;

Class组件 回调ref法(适用于版本React 16.3以下)

import React, { Component } from 'react';

class RefDemo extends Component{
    constructor(props){
        super(props);
        this.inputRef = null;
    }

    handleClick = () => {
        alert(`input value is ${this.inputRef.value}`);
    };

    render() {
        return(
            <div>
                <input type="text" ref={el => {this.inputRef = el}}/>
                <button onClick={this.handleClick}>Get Input Value</button>
            </div>
        );
    }
}

export default RefDemo;

ESlint插件
npm install eslint-plugin-react-hooks --save-dev

// 你的 ESLint 配置{
“plugins”: [
// …
“react-hooks”
],
“rules”: {
// …
“react-hooks/rules-of-hooks”: “error”, // 检查 Hook 的规则
“react-hooks/exhaustive-deps”: “warn” // 检查 effect 的依赖
}}

Hook 使用了 JavaScript 的闭包机制?

自定义Hooking
以useSomething开头。useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。记住是必须以use开头。不遵循这条约定的话,由于无法判断某个函数是否包含对其内部Hook的调用。React将无法自动检查的你的Hook是否违反了Hook的规则

在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

知识点:

  • 用于向子组件传递回调时,需要依赖对比引用的变化。以此防止不必要的渲染(例如shouldComponentUpdate)
  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
    为什么使用useCallback或useMemo,因为在react function
    component中,定义的同样的object都不是引用相等的 {} === {} // false.(即使是同样的属性,同样的值);

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

参数一:函数
参数二:依赖项数组

有依赖项数组:仅会在某个依赖项改变时才重新计算memoizedValue
无依赖项数组:每次渲染的时都会计算新的值

注:传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

useCallback and useMemo 例子

import React from 'react';

const CountButton = React.memo(function CountButton({onClick, count}) {
    return <button onClick={onClick}>{count}</button>
});// 因为被count1 ,count2重用。如果不用memo, 每次点击都会导致CountButton re-render

function DualCounter() {
    const [count1, setCount1] = React.useState(0);
    const increment1 = React.useCallback(() => setCount1(c => c + 1), []);
    const [count2, setCount2] = React.useState(0);
    const increment2 = React.useCallback(() => setCount2(c => c + 1), []);
    return (
        <>
            <CountButton count={count1} onClick={increment1} />
            <CountButton count={count2} onClick={increment2} />
        </>
    )
}

export default DualCounter;

以上这个例子来自文章“When to useMemo and useCallback”
英文链接:https://kentcdodds.com/blog/usememo-and-usecallback
译文链接:https://jancat.github.io/post/2019/translation-usememo-and-usecallback/

useImperativeHandle

相较于直接把ref暴露给父组件,useImperativeHandle可以定义对ref的操作来限制父组件的对ref的使用

useImperativeHandle(ref, createHandle, [deps])

参数一: 需要操作的ref
参数二:对ref操作的函数
参数三:依赖项,当依赖项变化的时候调用

import React, { forwardRef, useRef, useImperativeHandle } from 'react';

function Parent() {
    const inputRef= useRef();

    const handleChildRef = () => {
        console.log(inputRef.current); //这时取到的ref.current里面就只有focus属性了
        inputRef.current.focus();
    };

    return(
        <>
            <InputEle ref={inputRef} />
            <button onClick={handleChildRef}>click to focus input</button>
        </>
    )
}

const InputEle = forwardRef((props, ref) => {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current.focus();
        }
    }));

    return <input ref={inputRef} placeholder="put whatever you want"/>;
});

export default Parent;

useLayoutEffect

官方推荐使用useEffect就没有深入学习了。

useDebugValue

useDebugValue(value)

可用于显示自定义的hooks label 在React DevTools

以上code都可以到我的github上面下载
https://github.com/CassyWu/reactHooks

你可能感兴趣的:(前端)