Fetching Data in React using Hooks

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 simpler 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.

Exploring published React components on Bit.dev

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!!

Learn More