One of the most common use of higher-order components is to separate AJAX logic from JSX presentation components. Since React Hooks is designed to replace higher-order components, naturally the first question is how you can make AJAX requests using hooks.

A Simple Weather App

We will build a simple weather app that comes in three parts,

  • An input box for location
  • Query Yahoo weather API with location string
  • Display the returned weather data
import React, { useState } from "react"

function Example() {
    const [location, setLocation] = useState("Cuptertino, CA")

    return (
        <div>
            <input type="input" value={location} onChange={evt => setLocation(evt.target.value)} />
        </div>
    )
}

export default Example

This is our app skeleton that use a location state linked to a controled input box.

Hello Custom Hooks

Custom Hook is a function that encapsulate state and effects. It can be used to isolate and reuse AJAX requests, similar to a higher-order component. But first let's sort out the Yahoo weather query. Yahoo weather API uses a psudo query language, YQL, encoded as URL string.

`https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22${location}%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys`

This is the string template to query by location.

We will create a custom component that wraps the Fetch API.

function useFetch(url, defaultData) {
    const [data, updateData] = useState(defaultData)

    useEffect(async () => {
        const resp = await fetch(url)
        const json = await resp.json()
        updateData(json)
    }, [url])

    return data
}

Note we created a data state variable to store the fetch result. useEffect is the lifecycle hook that will trigger the fetch call. It's important that we pass in url as the last parameter in useEffect. It ensures the fetch call is only trigged once when url changes.

Let's slot useFetch into the app,

import React, { useState, useEffect } from "react"

function useFetch(url, defaultData) {
    const [data, updateData] = useState(defaultData)

    useEffect(async () => {
        const resp = await fetch(url)
        const json = await resp.json()
        updateData(json)
    }, [url])

    return data
}

function Example() {
    const [location, setLocation] = useState("Cuptertino, CA")
    const query = `https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22${location}%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys`
    const result = useFetch(query, {})
 
    return (
        <div>
            <input type="input" value={location} onChange={evt => setLocation(evt.target.value)} />
            {JSON.stringify(result)}
        </div>
    )
}

export default Example

Here we introduced the AJAX call without polluting state, or nesting the render component. But we can make this cleaner by refactoring out the query logic.

Nested Custom Hook

Custom hooks are just functions – functions that carry state and effect logic. We can simply wrap the function to hide query details.

import React, { useState, useEffect } from "react"

function useFetch(url, defaultData) {
    const [data, updateData] = useState(defaultData)

    useEffect(async () => {
        const resp = await fetch(url)
        const json = await resp.json()
        updateData(json)
    }, [url])

    return data
}

function useFetchWeather(location) {
    const query = `https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22${location}%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys`
    return useFetch(query, {})
}


function Example() {
    const [location, setLocation] = useState("Cuptertino, CA")
    const result = useFetchWeather(location)

    return (
        <div>
            <input type="input" value={location} onChange={evt => setLocation(evt.target.value)} />
            {JSON.stringify(result)}
        </div>
    )
}

export default Example

Handle Null in Hooks

We are passing location string from the input box directly into the custom query hook. In the case where the input is empty, it would create an invalid query with a 404 response. So how about we just check for empty location string and shortcut the call.

const result = location ? useFetchWeather(location) : {}

This looks harmless. If you clear the input box, however, you will see this error in the dev console: react-dom.development.js:57 Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

This is because although hooks behave like regular functions, they have to remain static through the lifecycle of the host component. Hence no conditionals or loops around hooks. See the React Hooks rules.

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

Going back to our original problem, how do we handle empty location input strings? The answer is to let the hooks handle it.

function useFetchWeather(location) {
    const query = `https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22${location}%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys`
    return useFetch(location ? query : null, {})
}

In useFetchWeather hook wrapper, in the event of an empty location string, it would pass a null query string to useFetch.

function useFetch(url, defaultData) {
    const [data, updateData] = useState(defaultData)

    useEffect(async () => {
        if (!url) {
            updateData(defaultData)
            return
        }
        const resp = await fetch(url)
        const json = await resp.json()
        updateData(json)
    }, [url])

    return data
}

useFetch has also been updated to handle null urls.

Putting it all together:

import React, { useState, useEffect } from "react"

function useFetch(url, defaultData) {
    const [data, updateData] = useState(defaultData)

    useEffect(async () => {
        if (!url) {
            updateData(defaultData)
            return
        }
        const resp = await fetch(url)
        const json = await resp.json()
        updateData(json)
    }, [url])

    return data
}

function useFetchWeather(location) {
    const query = `https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22${location}%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys`
    return useFetch(location ? query : null, {})
}

function Example() {
    const [location, setLocation] = useState("Cuptertino, CA")
    const result = useFetchWeather(location)

    return (
        <div>
            <input type="input" value={location} onChange={evt => setLocation(evt.target.value)} />
            {JSON.stringify(result)}
        </div>
    )
}

export default Example

What's Next

This post demonstrated how to roll your own fetch calls with React useEffect hook. As of Oct 2018, React team is actively working on react-cache + Suspense, which would become the prefered path for making AJAX calls in React. I'll update this post once the stable release is out.