TypeScript and Custom React Hook Api Service

Viewed 3767

I'm trying to create an API Service for fetching data in all my components. I'm stuck with Typescript (i'm not really good with it)

I've created a custom hook:

useApiService.tsx

import React from 'react'
import axios from 'axios'

const useApiService = (url: string) => {
  const baseUrl = 'http://localhost:8009'
  const [response, setResponse] = React.useState(null)
  const [error, setError] = React.useState(null)
  React.useEffect(() => {
    const fetchData = async (): Promise<void> => {
      try {
        const res = await axios(`${baseUrl}${url}`)
        setResponse(res.data)
      } catch (error) {
        setError(error)
      }
    }
    fetchData()
  }, [url])
  return { response, error }
}

export default useApiService

and then use this hook in my components like so :

Header.tsx

import React from 'react'
import { Button, Form, FormControl, Navbar, NavItem } from 'react-bootstrap'
import Nav from 'react-bootstrap/Nav'
import { Link } from '@reach/router'
import useApiService from '../services/useApiService'

interface NavigationData {
  title: string
  ID: string
  post_excerpt: string
}

interface SiteData {
  name: string
}

const Header: React.FC = () => {
  const menuQuery: Array<NavigationData> = useApiService('/wp-json/wp-utils/menus')
  const siteNameQuery: SiteData = useApiService('/wp-json')

  const data = menuQuery.response
  const siteName = siteNameQuery.response

  if (data && siteName)
    return (
      <Navbar bg="primary" expand="lg" className="navbar-dark mb-4">
        <Link to={'/'}>
          <Navbar.Brand>{siteName.name}</Navbar.Brand>
        </Link>
        <Navbar.Toggle aria-controls="basic-navbar-nav" />
        <Navbar.Collapse id="basic-navbar-nav">
          <Nav className="mr-auto">
            {data.map((item: NavigationData) => (
              <NavItem key={item.ID}>
                <Link className="nav-link" to={item.post_excerpt}>
                  {item.title}
                </Link>
              </NavItem>
            ))}
          </Nav>
          <Form className={'my-2 my-lg-0'} inline>
            <FormControl type="text" placeholder="Recherche" className="mr-sm-2" />
            <Button variant="light" className="my-2 my-sm-0">
              Search
            </Button>
          </Form>
        </Navbar.Collapse>
      </Navbar>
    )
  else return null
}

export default Header

But I have typescript errors Type '{ response: null; error: null; }' is missing the following properties from type 'NavigationData[]': length, pop, push, concat, and 28 more.

I would like to inject the data interface or type from my component to my custom hook but I don't know if it's the good way to do this, and how to do this right.

Thanks for your advices

1 Answers

Because you initialize the state like this

// TS will infer type for value of response as null
const [response, setResponse] = React.useState(null) 
// TS will infer type for value of error as null
const [error, setError] = React.useState(null)

and return { response, error } from the custom hook useApiService, TS will infer the return type as {response: null; error: null}.

If you want to create useApiService as a more reusable hook consider making it a generic, which will take a type argument T and return type will be { response: T | null; error: Error | null }. For example,

function useApiService<T>(url: string): { response: T | null; error: Error | null } {
  const baseUrl = 'http://localhost:8009';
  const [response, setResponse] = React.useState<T | null>(null);
  const [error, setError] = React.useState<Error | null>(null);
  React.useEffect(() => {
    const fetchData = async (): Promise<void> => {
      try {
        const res = await axios(`${baseUrl}${url}`);
        setResponse(res.data);
      } catch (error) {
        setError(error);
      }
    };
    fetchData();
  }, [url]);
  return { response, error };
}

When you use the custom hook useApiService, now you just need to pass what response type you want as a type argument.

interface NavigationData {
  title: string
  ID: string
  post_excerpt: string
}

interface SiteData {
  name: string
}

const menuQuery = useApiService<NavigationData[]>('/wp-json/wp-utils/menus')
// Keep in mind that the return type of menuQuery is 
// { response: NavigationData[] | null; error: Error | null}

// you can destructure the response and error from the returned value 
// const { response, error } = useApiService<NavigationData[]>('/wp-json/wp-utils/menus');


// same applies to siteNameQuery you can either destructure the response and error
// or use siteNameQuery.response and siteNameQuery.error
const siteNameQuery = useApiService<SiteData>('/wp-json')

P.S.- You need to create the interfaces for the response types which matches the response from your API, otherwise you will loose type information.

Related