useEffect-数据获取

4,032 阅读6分钟

习惯于在class组件中执行数据获取的逻辑,当转换到函数组件使用hook来实现数据获取时,我们有点懵了,于是需要搞清楚两个问题:

  • 如何实现useEffect模拟componentDidMount的生命周期?

也就是说在组件被加载后,能够去请求页面的数据。

  • 如何正确使用useEffect来获取数据?

因为useEffect不仅在组件加载后会执行,在组件更新后也可能会执行,这样会导致一些问题存在。

数据状态

一般来说,当state或者props发生变化时,组件将会重新渲染。这里,将数据存放在一个状态中。

const App = () => {
	const [data, setData] = useState([]);

  return (
  	<div>
    	<ul>
    		{data.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
    	</ul>
    </div>
  )
}

组件挂载后获取数据

这里用到useEffect,在useEffect中执行网络请求的操作。保证在组件挂载后,能够去获取网络数据。

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

      setData(data.hits || []);
    }

    fetchData();
  });

  return (
  	<div>
    	<ul>
    		{data.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
    	</ul>
    </div>
  )
}

这时候,打开开发者工具,就会发现,一直在不断地做接口请求。这是因为useEffect的第二个数组参数。这里,我们需要记住useEffect和它的第二个参数之间的关系:

  • useEffect在组件加载后,将会执行一次;
  • 当useEffect的第二个参数(数组),没有传入,则在组件每次更新之后,useEffect都会执行一次;
  • 当useEffect的第二个参数是一个空数组,useEffect在组件更新后,不会执行;
  • 当useEffect的第二个参数不是一个空数组,useEffect在组件更新后,是否会执行,取决于该数组中的的某个元素时候是否发生了变化。如果没有一个元素发生了变化,则useEffect不会执行。

所以,在这里,我们需要给useEffect的第二个参数传入一个空数组:

useEffect(() => {}, []);

更新数据

在一个包含搜索功能的页面中,用户可通过搜索行为,来更新页面的数据。对于开发者来说,需要拿到用户键入的搜索内容,然后接口请求数据,更新页面。根据我们在class组件内的行为,我们会这么做:

  • 定义一个接口请求的函数;
  • 在组件挂载后,通过useEffect来执行接口请求,获取页面数据;
  • 为点击搜索的行为绑定接口请求的函数。
const App = () => {
  const [data, setData] = useState([]);
  const [query, setQuery] = useState('');

  function searchData() {
    async function fetchData() {
      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
      const { data = {} } = result || {};

      setData(data.hits || []);
    }

    fetchData();
  }

  useEffect(() => {
    searchData();
  }, []);

  return (
    <div>
      <input value={query} onChange={(e) => {
        setQuery(e.target.value);
      }} />
      <button onClick={searchData}>搜索</button>
      <ul>
        {data.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </div>
  )
}

确实目前已经能够完成诉求。而整个实现过程,还是没有脱离class组件中的思维方式,通过useEffect来模式componentDidMount,同时定义行为逻辑。我们还是没有完全实现useEffect的价值,函数式组件,理应是没有副作用的,React提供useEffect来为函数组件添加副作用,在这个例子中,搜索行为触发接口请求生成了副作用,我们并没有放在useEffect,而是直接定义在函数组件中,这违背了hook作者美好的初衷,也使得组件不那么优雅。

通过useEffect来更新数据,这里还是用到了他的第二个数组参数:

const searchUrl = 'https://hn.algolia.com/api/v1/search';

const App = () => {
  const [data, setData] = useState([]);
  const [query, setQuery] = useState('');
  const [url, setUrl] = useState(`${searchUrl}?query=`);

  useEffect(() => {
    async function fetchData() {
      const result = await axios(url);
      const { data = {} } = result || {};

      setData(data.hits || []);
    }

    fetchData();
  }, [url]);

  return (
    <div>
      <input value={query} onChange={(e) => {
        setQuery(e.target.value);
      }} />
      <button onClick={() => {
        setUrl(`${searchUrl}?query=${query}`)
      }}>搜索</button>
      <ul>
        {data.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </div>
  )
}

该例子中,接口请求依赖于接口url,当url发生变化时,则触发useEffect的执行。这样的编写方式,将所有的副作用都放在了useEffect中,组件本身只用维持组件内部的状态:data、query、url。实现了在组件加载时接口请求,在组件的url状态发生了后,更新页面。相比于上一种方式,更加优雅,更加具有函数式组件的特色。

自定义hook,抽离逻辑

实际的开发中,在接口请求时,需要一个loading状态,当接口请求发生了问题时,需要有一个错误状态,一般来说,可以这么写:

const searchUrl = 'https://hn.algolia.com/api/v1/search';

const App = () => {
  const [data, setData] = useState([]);
  const [query, setQuery] = useState('');
  const [url, setUrl] = useState(`${searchUrl}?query=`);
  const [isLoading, setLoading] = useState(false);
  const [isError, setError] = useState(false);

  useEffect(() => {
    setLoading(true);
    async function fetchData() {
      const result = await axios(url);
      const { data = {}, error } = result || {};
      
      if (error) {
      	setError(true)
      } else {
        setData(data.hits || []);
      }     
      setLoading(false);
    }

    fetchData();
  }, [url]);

  return (
    <div>
      <input value={query} onChange={(e) => {
        setQuery(e.target.value);
      }} />
      <button onClick={() => {
        setUrl(`${searchUrl}?query=${query}`)
      }}>搜索</button>
			{isError && <p>ERROR</p>}
			{isLoading ? (<p>loading....</p>) : (
        <ul>
          {data.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul> 
      )}
    </div>
  )
}

在这个页面中,维持了data、query、url、isLoading、isError这个五个状态,并且所有的逻辑都写在了这个组件中,组件主要的旋律是渲染内容,这些庞大的逻辑使得组件显得臃肿,而且类似该页面的的逻辑,在别的页面中也有,我们完全可以将这些关于接口请求的逻辑抽离出来,为函数组件减重,不仅使得代码清爽,同时能够复用逻辑。通过自定义hook,如下:

const searchUrl = 'https://hn.algolia.com/api/v1/search';
// 自定义hook
const useNetWork = (initData = [], initUrl) => {
  const [data, setData] = useState(initData);
  const [url, setUrl] = useState(initUrl);
  const [isLoading, setLoading] = useState(false);
  const [isError, setError] = useState(false);

  useEffect(() => {
    setLoading(true);
    async function fetchData() {
      const result = await axios(url);
      const { data = {}, error } = result || {};

      if (error) {
        setError(true)
      } else {
        setData(data.hits || []);
      }
      setLoading(false);
    }

    fetchData();
  }, [url]);

  return [{data, isLoading, isError}, setUrl];
}

// 函数式组件
const App = () => {
  const [query, setQuery] = useState('');
  const [{ data, isLoading, isError }, setUrl] = useNetWork([], searchUrl);

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

使用useReducer

到上面一小节为止,已经很好地使用了useEffect hook和自定义hook来为函数式组件添加副作用,并且抽离逻辑,减轻组件体重,实现组件诉求。在整个整个过程中,自定义hook提供的data、isLoading、isError这几个状态来相互作用、相互影响,拧在一起,决定了函数组件的展示逻辑。其实,这四个状态的值是可预测的,并且是仅仅绑定在一起的,类似于redux,可通过dispatch行为,更改这些状态,实现数据流管理。react提供了useReducer,将这几个状态集合在一起,进行管理。

const searchUrl = 'https://hn.algolia.com/api/v1/search';

function dataReducer(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();
  }
}

// 自定义hook
const useNetWork = (initData = [], initUrl) => {
  const [url, setUrl] = useState(initUrl);
  const [state, dispatch] = useReducer(dataReducer, {
    isLoading: false,
    isError: false,
    data: initData,
  });

  useEffect(() => {
    dispatch({ type: 'FETCH_INIT' });
    async function fetchData() {
      try {
        const result = await axios(url);

        const { data = {} } = result || {};
        
        dispatch({ type: 'FETCH_SUCCESS', payload: data.hits || [] });
        
      } catch (error) {
         dispatch({ type: 'FETCH_FAILURE' });
      }
    }

    fetchData();
  }, [url]);

  return [{data, isLoading, isError}, setUrl];
}

// 函数式组件
const App = () => {
  const [query, setQuery] = useState('');
  const [{ data, isLoading, isError }, setUrl] = useNetWork([], searchUrl);

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

通过useReducer,将状态的管理抽离出来,自定义hoo中,主要逻辑处理,发布状态,reducer对状态进行更新。

在useEffect中取消数据的获取

在日常的使用中,接口还在进行网络请求时,组件已经处于卸载状态,但是当接口返回后,仍然对组件的状态进行更改,我们可以通过useEffect的请求函数,来避免这种情况:

// 自定义hook
const useNetWork = (initData = [], initUrl) => {
  const [url, setUrl] = useState(initUrl);
  const [state, dispatch] = useReducer(dataReducer, {
    isLoading: false,
    isError: false,
    data: initData,
  });

  useEffect(() => {
    let canCancel = false;
    dispatch({ type: 'FETCH_INIT' });
    async function fetchData() {
      try {
        const result = await axios(url);

        const { data = {} } = result || {};
     
        if (!cancel) {
        	dispatch({ type: 'FETCH_SUCCESS', payload: data.hits || [] });
        }
        
      } catch (error) {
        if (!cancel) {
        	dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    }

    fetchData();
    
    return () => {
    	canCancel = true;
    }
  }, [url]);

  return [{data, isLoading, isError}, setUrl];
}

在useEffect中定义一个canCancel变量,并且返回更改canCancel为true的清理函数,在组件卸载后,canCancel标志为true,在接口返回后,无法组件的状态进行更改。

在我们的实操中,可以看到,当每次url发生变化后,清理函数就会执行,旧的url的对应的canCancel就会为true,旧的url对应的接口返回,就不会更新组件的状态。每一轮useEffect对应的数据都是那一轮的,不会影响到下一轮,所以,当上一轮useEffect的canCancel变为true,并不会影响到下一轮,因为下一轮useEffect有一个新的canCancel。关于每一轮useEffect的数据问题,可以到下一篇文章再做一个详细讲解,这是一个很有意思的问题。

参考文章