react hooks 使用指南

795 阅读5分钟

Hook 是 React 16.8 的新增特性,它的动机这里就不做过多的介绍了。下面我们主要说一下几个主要hooks的用法,以及遇到的一些,用起来不是很顺畅的地方。

一、useEffect

你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。useEffect就是一个Effect Hook 给函数组件增加了操作副作用的能力。

 
 主要从一下两个问题中描述:
  1. useEffect是否与class组件的生命周期完全一致?
  2. 如何正确地在useEffect里请求数据?

1、useEffect与生命周期函数异同

例如,React 更新 DOM 后会设置一个页面标题

class组件

class counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

hooks函数组件

import React,{useState,useEffect} from 'react'
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

通过模拟,很明显我们发现,大多数情况下,我们会需要在mount和update中执行副作用,此时hooks的写法相对于class组件要简单明了的多。

不过,看到这个例子,大家也不要认为useEffect和生命周期函数可以完全相等。在hooks内部使用了javascript的闭包机制,useEffect会捕获props和state,在回调函数中拿到的也是初始的props和state.

useEffect的capture value 特性

即上面说的useEffect中props和state不会改变。

不过大家可能又会疑惑,为什么说props和state不会变,但是每次点击按钮后,显示的次数都不一致呢?!

这事因为,当我们点击按钮更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。当setCount的时候,react就会带着不同的值,更新DOM。不用着急,看下面这个代码,你就明白了

// During first render
function Counter() {
  // ...
  useEffect(
    // Effect function from first render
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...
}

// After a click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from second render
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...
}

由此可知,count在不变的effect中产生变化,主要是因为每一次的渲染effect都不相同。

useEffect的清理

上述,我们知道了useEffect实现了class组件的mount和update方法。那组件卸载呢?

useEffect 返回一个函数(可选),这就是它可选的清除机制

useEffect(()=>{
    //...
    return ()=>{
        //清除操作
    }
})

这里需要注意的是,class组件的componentWillUnmount只会在组件卸载的时候执行。useEffect返回的函数会在每次渲染后执行。

useEffect 将产生副作用的操作,都整合在一起,在代码量上简写了很多,也是一大优势。

注意: 一个常见的问题,当组件卸载后,依然会在effect中设置state。

useEffect(()=>{
    let isUnmount = false
    //...
    if(!isUnmount){
        setValue(value)
    }
    return ()=>{
        isUnmount = true
        //清除操作
    }
})

useEffect的依赖项

useEffect还有第一个可选参数,即可以传入函数的依赖项。

我们先看一下在componentWillReciveProps中,我们会判断props和nextProps是都一致 来判断是否需要更新,那么在useEffct是怎么解决的呢?!

import React,{useState,useEffect} from 'react'
function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('xixi');
  useEffect(() => {
    document.title = `You clicked ${name} times`;
  });

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

此时,每次点击按钮,name并未改变,都会调用useEffect,重新给document.title赋值。很显然这并不是我们想要的。所以,我们可以给useEffect穿第二个参数,给定该effect的依赖项

  useEffect(() => {
    document.title = `You clicked ${name} times`;
  },[name]);

只有当name更新了,才会执行该effect,反之,则会跳过该effect了。是不是简单清晰又明了呢~

当第二个参数为空数组时,该effect只会执行一次,即模拟了componentDidUnmount效果。

useEffect(()=>{
   //... 
},[])

需要强调的是,关于依赖项不要企图欺瞒react

举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[]。

  useEffect(() => {
     const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  },[]);

上述,useEffect只会执行一次,由于useEffect的capture value 特性,count的值不会更新。所以我们应该如实的给出count这个依赖项。

  useEffect(() => {
     const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  },[count]);

但是这样会导致,定时器在每次count更新后清除和重新设定, 这里我们需要了解下,如何移除依赖? 1、我们需要的每次count+1,那么我们可以采用setCount回调的方式

  useEffect(() => {
     const id = setInterval(() => {
      setCount(c=>c + 1);
    }, 1000);
    return () => clearInterval(id);
  },[]);

上述有一个弊端。现在我们每次固定递增1,当递增的数会发生变化时,我们依然会遇到之前的问题。所以,需要探索其他的解决办法。

2、采用useReducer

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [dispatch]);

effect里不用再关心count的更新,更新的逻辑交给reducer

我现在的项目,虽然用的hooks,但是,依然用redux状态管理,同理,此处也可以将更新操作交给redux。

此处还需要强调一点,依赖项我们不能忘记,但出现以下依赖过多的情况时,需要注意拆分useEffect,避免不必要的更新计算。

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
  //...
  return () => clearInterval(id);
}, [a,b,c,d,e,f,g,...]);

2.在useEffect中请求数据

先看一个简单的例子

import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  });
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

上述代码, 在运行时,我们发现会陷入循环。因为该effect将会在页面加载以及更新时执行,而在该effect存在setData,会导致组件更新,继续执行该effect,从而陷入循环。

如果我们只需要在页面加载时,拉取数据,需要给定useEffect第二个参数为[],

useEffect(async () => {
const result = await axios(
  'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
},[]);

更改后,我们会发现,控制台会出现警告⚠️,"Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => ...) are not supported, but you can call an async function inside an effect.. " 由此可知useEffect无法返回一个AsyncFunction对象。 改写如下

useEffect( () => {
    const fetchData = async()=>{
        const result = await axios(
            'https://hn.algolia.com/api/v1/search?query=redux',
        );
        setData(result.data);
    }
    fetchData()
},[]);

如何手动触发更新数据?

表单查询中,查询条件发生变化时,useEffect中的处理:

useEffect很明显需要依赖query查询条件

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, [query]);
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}
export default App;

查看上诉代码会发现,每次input的值发生改变时,useEffect都会执行一次请求数据,有时候,这样的行为并不是我们所需要的。我们需要在【输入完成】后 再请求数据。

这里可以有两种做法,通常我们项目中都会封装Input组件,会提供输入完成函数并返回当前值,这里不做过多说明。另外 我们可以在input失去焦点时请求数据。注意。这时 useEffect 依赖项 有 input 是否失去焦点,但同时 我们也需要判断输入的值与上次是否相同

const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, [query]);
  
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
        onBlur={()=>{
            setSearch(query)
        }}
      />
     <!--...-->
    </Fragment>
  );

如上所写,可以实现我们的需求,但是有一点不舒服的是,search和query存储的值都是一样的,有点迷惑。所以我们可以在search中设置为请求的url,这样看起来正常点。不过,倔强的你,还是可以像上面这样的。

const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, [url]);
  
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
        onBlur={()=>{
            setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }}
      />
     <!--...-->
    </Fragment>
  );

编写自定义获取数据钩子

拉取数据时,一般需要如下操作

  1. 设置loading状态,
  2. 请求数据
  3. 错误处理

以下只是简单的demo,具体根据业务调整

const usefetchData = (initUrl,initData) => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(initUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMsg, setErrorMsg] = useState('');
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      const {ok,result} = await axios(url);
      if(!ok){
       setErrorMsg(data || data.message)
       setIsLoading(false);
      }
      setData(result.data);  
      setIsLoading(false);
    };
    fetchData();
  }, [url]);
  return [{ data, isLoading, errorMsg }, setUrl];
}

function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, errorMsg }, doFetch] = usefetchData(
    'https://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );

  return (
    <Fragment>
      <form onSubmit={event => {
        doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);
        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
      {errorMsg && (<div>{errorMsg}</div>)}
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

另外,数据可以存在useReducer中,同理redux类似。

const usefetchData = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
      try {
        const result = await axios(url);
        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };
    fetchData();
  }, [url]);
  ...
};

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_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();
  }
};

二、useState

const {value,setValue} = useState('')
const {loading,setLoading} = useState(false)
const {isError,setError} = useState(false)
...

如果觉得需要的变量过多,可以采用对象的形式

const {obj,setObj} = useState({
    value:'',
    loading:false,
    isError:false,
})

useState和class组件中的setState类似,都是【异步】的,这点真的要注意了,很容易被忽略。

(例子不恰当,只是为了说明问题

function Counter() {
  const [count, setCount] = useState(0);
  const dealCount = ()=>{
    setCount(count + 1)
  }
  const save = ()=>{
      console.log(count) //--- 有可能值并未更新
      //TODO
  }
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={dealCount}>
        Click me
      </button>
      <button onClick={save}>
        保存
      </button>
    </div>
  );
}

useState 依赖调用顺序,注意不要在条件判断等改变调用顺序的代码块中使用useState
具体参考: overreacted.io/why-do-hook…

三、useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中

  1. useRef --- DOM引用
function Preview() {
  const canvasRef = useRef(null)
  const completeCanvasRef = useRef(null)
  return (
    <div>
        <canvas ref={canvasRef} />
        <canvas ref={completeCanvasRef} />
    </div>
  );
}

2.ref 对象可以确保在整个生命周期中值不变,且【同步】更新,是因为 ref 的返回值始终只有一个实例

可以解决一些 由于useState异步 导致未更新的问题 (例子不恰当,只是为了说明问题

function Counter() {
  const count = useRef();
  const dealCount = ()=>{
    count.current += 1
  }
  
  const save = ()=>{
    console.log(count.current) //--- 更新后的值
    //TODO
  }
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={dealCount}>
        Click me
      </button>
      <button onClick={save}>
        保存
      </button>
    </div>
  );
}

四、useMemo 与 useCallback

useCallback和useMemo的参数跟useEffect一致,他们之间最大的区别有是useEffect会用于处理副作用,而前两个hooks不能。

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。

useMemo:

function Counter() {
  const [name,setName] = useState('you');
  const [count,setCount] = useState(0);
  const dealCount = () =>{
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
        sum += i;
    }
    return sum;
  }
  
  return (
    <div>
      <p>{name} clicked {dealCount()} times</p>
      <button onClick={() => setCount(count + 1)}>click me</button>
      <input value={name} onChange={event => setName(event.target.value)}/>
    </div>
  );
}

当input 发生改变时,此时组件重绘,每次都会调用dealCount重新计算,但是 dealCount只依赖与count的改变,上述代码可以改写为:

function Counter() {
  const [name,setName] = useState('you');
  const [count,setCount] = useState(0);
  const dealCount = useMemo(() =>{
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
        sum += i;
    }
    return sum;
  },[count])
  
  return (
    <div>
      <p>{name} clicked {dealCount} times</p>
      <button onClick={() => setCount(count + 1)}>click me</button>
      <input value={name} onChange={event => setName(event.target.value)}/>
    </div>
  );
}

useCallback: 涉及到组件通讯时,useCallback会派上用场

function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const dealCount = useCallback(() => {
        //TODO
        return count
    }, [count]);
    
    return <div>
        <h4>{count}</h4>
        <Child dealCount={dealCount}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}

function Child({ dealCount }) {
    useEffect(() => {
        dealCount();
    }, [dealCount]);
}

如果不采用useCallback,input发生变化时,每次都会调用dealCount重新计算, useCallback(fn, inputs) 等价于 useMemo(() => fn, inputs)。

参考文章:

overreacted.io/a-complete-…

www.robinwieruch.de/react-hooks…

关于react hooks先介绍这么多。希望大家指正。