万万没想到react请求数据花样如此之多

2020-05-02 10:01:59 浏览数 (1)

接触react一个星期,也慢慢熟悉了一些概念,比如HOC(高阶组件)、jsx、函数式组件、HOOK,感觉react也没有别人说的学习曲线陡峭,难上手等等,给我的感觉,如果你会Vue,上手React真的会非常快,不要被这些概念给吓到,这样的一些概念的出现,一定是有着他的道理的,无外乎包含但不限于以下两点理由:

  1. 为了代码复用,比如HOC,自定义HOOK
  2. 为了代码更加简洁,更加好理解,比如jsx,函数式组件。

那么,我们所理解的React的模式,其实归根结底就是UI=Render(State),这其实和Vue乃至整个前端的哲学并无任何冲突,相反,是一个统一。说来说去,说简单点,一个web应用,应该是状态驱动的,而状态=数据 逻辑所以,我们的UI=Render(Data Logic)

那么,Data从何而来,可以说99%的web应用的Data是从网络而已,俗称网络获取数据。

下面的代码段是一个很简单的显示列表数据模板,很简单,这里只用到了useState这个Hook,如果需要填充数据,很明显,使用setData给到数据就可以了,数据从何而来,这是一个问题,带到今天来看,要讲的是如何从网络获取数据。

代码语言:txt复制
import React, { useState } from 'react';

function App() {
  const [data, setData] = useState({ hits: [] });

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

export default App;

温馨提示,下面是的列车将一步一步提速,请系好安全带~~。

引入axios请求网络数据,将请求放入useEffect中
代码语言:txt复制
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>
  );
}

export default App;

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合,上述代码你应该不会满意吧,你可能仅仅需要网络请求代码只在componentDidMount的时候执行一次。如是

加了一个[]
代码语言:txt复制
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>
  );
}

export default App;

通过Object.is(value1, value2);成功挡住了componentDidUpdate每次无脑的唤醒。

然而,上面的代码会有一个告警

那是因为useEffect要求要么返回一个清理函数,要么啥都别返回,而上面返回的是一个Promise体,他将最终返回的是一个结果,这显然会受到一个告警,解决的办法如下。

代码语言:txt复制
import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

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

      setData(result.data);
    };

    fetchData();
  }, []);

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

export default App;
不过很快就会就想到,网络请求需要传递参数

所以,你加了一个query的useState,而且仅仅当query变化时触发重新获取网络数据,干得还不错。

代码语言:txt复制
...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    ...
  );
}

export default App;
后面,你发现网络请求有点慢,需要加loading状态,如是
代码语言:txt复制
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(url);

      setData(result.data);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

其实很简单,就加了一个loading的useState而已。所以,如果需要加什么错误状态,你应该也懂这个套路了吧。

请注意,要开车了,前面说到,更好复用才是推动技术变革的第一生产力,比如Docker,我瞎扯的。

假如其他业务需要用到你这个网络请求,如是,你写了一个自定义的Hook

代码语言:txt复制
const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
}
后面你知道了useReducer,你发现,loading,error这些状态应该交给他来做,而不是你通过useState来做,这样显得会更加清晰~~

因此,你引入了useReducer

代码语言:txt复制
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();
  }
};

const useDataApi = (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]);

  ...
};

像这种语法

代码语言:txt复制
return {
        ...state,
        isLoading: true,
        isError: false
      };

我们已经见怪不怪了,但是别小看这种,说大点,这可能是最简洁的函数式编程了,他返回的是一个全新的对象,函数式编程的好处?复用性无话可说,方便做备忘录,使用一个history数组记录每次变更的state就OK啦。anymore,自己YY吧。

最后,你可能会想,页面componentWillUnmount时,如果网络请求没回,是不是该”终止网络请求“
代码语言:txt复制
const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };

    fetchData();

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

  return [state, setUrl];
};

如是,你在useEffect中加了一个didCancel变量,并且返回一个闭包,其实就是一个函数啦,只不过他让你可以改他母体的变量而已,因此,在componentWillUnmount时候,这个变量被置位false了,如是dispatch的将不会触发。

是不是和移动客户端开发灰常像,页面的destory的时候,如果网络请求的presenter还持有页面的context,那么页面将释放不掉,造成内存泄漏不说,还会导致在页面执行destory之后,网络数据回来,走触发变更ui的逻辑,导致crash的发生,因为你不能对一个已经destory的页面进行变更ui的操作。

当然,这里,网络请求其实并没有真的被cancel掉,cancel掉的之后网络请求回来之后的逻辑。

0 人点赞