内容简介:Class components are verbose and cumbersome. In many cases, we are forced to duplicate our logic in different lifecycle methods to implement our ‘effect logic’.Class components do not offer an elegant solution to sharing logic between components (HOC and f
Making network requests, memoizing, and handling errors using React hooks.
Why Use Hooks?
Class components are verbose and cumbersome. In many cases, we are forced to duplicate our logic in different lifecycle methods to implement our ‘effect logic’.
Class components do not offer an elegant solution to sharing logic between components (HOC and friends are not an elegant solution) — React Hooks, on the other hand, give us the ability to build custom hooks, a much more elegant solution.
The list goes on and on. In a nutshell, I can say function components with hooks are much more “in the spirit of React”. They make sharing and reusing components much simpler and easier.
As someone who uses cloud component hubs (e.g, Bit.dev ) to publish and document components for my team and the open-source community, I can say that without a doubt, function components are more suitable for sharing and reusing.
Data Fetching with a Class Component
When working with regular class components in React we make use of lifecycle methods to fetch data from a server and display it with no problems.
Let’s see a simple example:
class App extends Component { this.state = { data: [] } componentDidMount() { fetch("/api/data").then( res => this.setState({...this.state, data: res.data}) ) } render() { return ( <> {this.state.data.map( d => <div>{d}</div>)} </> ) } }
Once the component is mounted, it will fetch data and render it. Note we didn’t place the fetch logic in the constructor but instead, delegated it to the componentDidMount
hook. Network requests may take some time — it's better not to hold up your component from mounting.
We resolve the Promise returned by the fetch(...)
call and set the data
state to the response data. This, in turn, will re-render the component (to display the new data in the component’s state).
From a Class Comp to a Function Component
Let’s say we want to change our class component to a function component. How will we implement that so that the former behavior remains the same?
useState and useEffect
useState
is a hook used to maintain local states in function components.
useEffect
is used to execute functions after a component gets rendered (to “perform side effects”). useEffect
can be limited to cases where a selected set of values change. These values are referred to as ‘dependencies’.
useEffects
does the job of componentDidMount
, componentDidUpdate
, componentWillUpdate
combined.
These two hooks essentially give us all the utilities we’ve previously got from class states and lifecycle methods.
So, let’s refactor the App from a class component to a function component.
function App() { const [state, setState] = useState([]) useEffect(() => { fetch("/api/data").then( res => setState(res.data) ) }) return ( <> {state.map( d => <div>{d}</div>)} </> ) }
The useState
manages a local array state, state
.
The useEffect
will make a network request on component render. When that fetch resolves, it will set the response from the server to the local state using the setState
function. This, in turn, will cause the component to render so as to update the DOM with the data.
Preventing Endless Callbacks using Dependencies
We have a problem. useEffect
runs when a component mounts and updates. In the above code, the useEffect
will run when the App mounts, when the setState
is called (after the fetch has been resolved) but that’s not all — useEffect
will get triggered again as a result of the component being rendered. As you’ve probably figured out yourself, this will resolve in endless callbacks.
As mentioned earlier, useEffect
has a second param, the ‘dependencies’. These dependencies specify on which cases useEffect
should respond to a component being updated.
The dependencies are set as an array. The array will contain variables to check against if they have changed since the last render. If any of them change, useEffect
will run, if not useEffect
will not run.
useEffect(()=> { ... }, [dep1, dep2])
An empty dependency array makes sure useEffect
run only once when the component is mounted.
function App() { const [state, setState] = useState([]) useEffect(() => { fetch("/api/data").then( res => setState(res.data) ) }, []) return ( <> {state.map( d => <div>{d}</div>)} </> ) }
Now, this functional component implementation is the same as our initial regular class implementation. Both will run on a mount to fetch data and then nothing on subsequent updates.
Memoizing using Dependencies
Let’s see a case where we can use dependencies to memoize useEffect
.
Let’s say we have a component that fetches data from a query.
function App() { const [state, setState] = useState([]) const [query, setQuery] = useState() useEffect(() => { fetch("/api/data?q=" + query).then( res => setState(res.data) ) }, [query]) function searchQuery(evt) { const value = evt.target.value setQuery(value) } return ( <> {state.map( d => <div>{d}</div>)}<input type="text" placeholder="Type your query" onEnter={searchQuery} /> </> ) }
We have a query state to hold the search param that will be sent to the API.
We memoized the useEffect
by passing the query state to the dependency array. This will make the useEffect
load data for a query on an update/re-render, only when the query has changed.
Without this memoization, the useEffect
will constantly load data from the endpoint even when the query has not changed which will cause unnecessary re-renders in the component.
So, we have a basic implementation of how we can fetch data in functional React components using hooks: useState
and useEffect
.
The useState
is used to maintain the data response from the server in the component.
The useEffect
hook is what we used to fetch data from the server('cos it is a side-effect) and also gives us lifecycle hooks only available to regular class components so we can fetch/update data on mounts and on updates.
Error handling
Nothing comes without errors. We set up data fetching using hooks in the last section, awesome. But what happens if the fetch request returns with some errors? How does the App component respond?
We need to handle errors in the component’s data fetching.
Error Handling in a Class Component
Let’s see how we can do it in a class component:
class App extends Component { constructor() { this.state = { data: [], hasError: false } } componentDidMount() { fetch("/api/data").then( res => this.setState({...this.state, data: res.data}) ).catch(err => { this.setState({ hasError: true }) }) } render() { return ( <> {this.state.hasError ? <div>Error occured fetching data</div> : (this.state.data.map( d => <div>{d}</div>))} </> ) } }
Now, we added a hasError
to the local state with a default value of false (yes, it should be false, because, at the initialization of the component, no data fetching has occurred yet).
In the render method, we used a ternary operator to check for the hasError
flag in the component’s state. Also, we added a catch promise to the fetch call, to set the hasError
state to true when the data fetching fails.
Error Handling in a Function Component
Let’s see the functional equivalent:
function App() { const [state, setState] = useState([]) const [hasError, setHasError] = useState(false) useEffect(() => { fetch("/api/data").then( res => setState(res.data) ).catch(err => setHasError(true)) }, []) return ( <> {hasError? <div>Error occured.</div> : (state.map( d => <div>{d}</div>))} </> ) }
Adding the ‘ Loading...'
Indicator
Loading in a Class Component
Let’s see the implementation in a class component:
class App extends Component { constructor() { this.state = { data: [], hasError: false, loading: false } } componentDidMount() { this.setState({loading: true}) fetch("/api/data").then( res => { this.setLoading({ loading: false}) this.setState({...this.state, data: res.data}) } ).catch(err => { this.setState({loading: false}) this.setState({ hasError: true }) }) } render() { return ( <> { this.state.loading ? <div>loading...</div> : this.state.hasError ? <div>Error occured fetching data</div> : (this.state.data.map( d => <div>{d}</div>))} </> ) } }
We declare a state to hold the loading
flag. Then, in the componentDidMount
it sets the loading flag to true, this will cause the component to re-render to display the "loading...".
Loading in a Function Component
Let’s see the functional implementation:
function App() { const [state, setState] = useState([]) const [hasError, setHasError] = useState(false) const {loading, setLoading} = useState(false) useEffect(() => { setLoading(true) fetch("/api/data").then( res => { setState(res.data); setLoading(false)} ).catch(err => { setHasError(true)) setLoading(false)}) }, []) return ( <> { loading ? <div>Loading...</div> : hasError ? <div>Error occured.</div> : (state.map( d => <div>{d}</div>)) } </> ) }
This will work the same way as the previous class component.
We added another state using the useState.
This state will hold the loading flag.
It is initially set to false
, so when the App mounts, the useEffect
will set it to true (and a “loading…” will appear). Then, after the data is fetched or an error occurs, the loading state is set to false, so the “Loading…” disappears, replaced by whatever result the Promise has returned.
Packaging all in a Node module
Let’s bind all that we have done into a Node module. We are going to make a custom hook that will be used to fetch data from an endpoint in functional components.
function useFetch(url, opts) { const [response, setResponse] = useState(null) const [loading, setLoading] = useState(false) const [hasError, setHasError] = useState(false) useEffect(() => { setLoading(true) fetch(url, opts) .then((res) => { setResponse(res.data) setLoading(false) }) .catch(() => { setHasError(true) setLoading(false) }) }, [ url ]) return [ response, loading, hasError ] }
We have it, useFetch
is a custom hook to be used in functional components for data fetching. We combined every topic we treated into one single custom hook. useFetch memoizes against the URL where the data will be fetched from, by passing the url
param to the dependency array. useEffcect will always run when a new URL is passed.
We can use the custom hook in our function components.
function App() { const [response, loading, hasError] = useFetch("api/data") return ( <> {loading ? <div>Loading...</div> : (hasError ? <div>Error occured.</div> : (response.map(data => <div>{data}</div>)))} </> ) }
Simple.
Conclusion
We have seen how to use useState and useEffect hooks to fetch and maintain data from an API endpoint in functional components.
Don't forget to pen down your suggestions, comments, notes, corrections below or you can DM or email them.
Thanks!!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。