react-query

Performant and powerful data synchronization for React

2020-10-22 / Thomas Bassetto

Agenda

  1. Data fetching with React hooks
  2. Usage of react-query in the Marketplace
  3. Drawbacks and competition

Basic fetch with hooks

function App() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    const fetchData = async () => {
      const data = await fetchProduct();
      setData(data);
    };
    fetchData();
  }, []);

  return (
    <>
      <h2>Product</h2>
      {data && <p>{data.name}</p>}
    </>
  );
}

A loading UI would be great

With loading state

function App() {
  const [data, setData] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      const data = await fetchProduct();
      setData(data);
      setIsLoading(false);
    };
    fetchData();
  }, []);

  return (
    <>
      <h2>Product</h2>
      {(!isLoading && data) ? <p>{data.name}</p> : <p>Loading ...</p>}
    </>
  );
}

What if the request fails?

With error handling

function App() {
  const [data, setData] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    const fetchData = async () => {
      setError(null);
      setIsLoading(true);
      try {
        const data = await fetchProduct();
        setData(data);
      } catch (error) {
        setError(error);
      }
      setIsLoading(false);
    };
    fetchData();
  }, []);

  return (
    <>
      <h2>Product</h2>
      {error && <div>Something went wrong ...</div>}
      {(!isLoading && data) ? <p>{data.name}</p> : <p>Loading ...</p>}
    </>
  );
}

Our logic for what to show the user when is kind of convoluted

With status

function App() {
  const [status, setStatus] = React.useState("idle");
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    const fetchData = async () => {
      setError(null);
      setStatus("loading");
      try {
        const data = await fetchProduct();
        setData(data);
        setStatus("success");
      } catch (error) {
        setError(error);
        setStatus("failure");
      }
    };
    fetchData();
  }, []);
  
  // ...

With status (cont.)


  // ...

  return (
    <>
      <h2>Product</h2>
      {(() => {
        if (status === "idle" || status === "loading") {
          return <p>Loading ...</p>;
        }
        if (status === "failure") {
          return <p>Something went wrong: {error}</p>;
        }
        if (status === "success") {
          return <p>{data.name}</p>;
        }
      })()}
    </>
  );
}

Problem: each call to setSate can result in a re-render of our component

useReducer or ... 1 useState

const [state, setState] = React.useState({
  status: 'idle',
  data: undefined,
  error: undefined
});

React.useEffect(() => {
  const fetchData = async () => {
    setState({
      status: 'loading',
    });
    try {
      const data = await fetchProduct();
      setState({
        status: 'success',
        data: data,
      });
    } catch (error) {
      setState({
        status: 'error',
        error: error,
      });
    }
  };
  fetchData();
}, []);

Let's abstract the details to a custom hook

With a custom hook

function App() {
  const { status, data, error } = useProductApi();

  return (
    <>
      <h2>Product</h2>
      {(() => {
        if (status === "idle" || status === "loading") {
          return <p>Loading ...</p>;
        }
        if (status === "failure") {
          return <p>Something went wrong: {error}</p>;
        }
        if (status === "success") {
          return <p>{data.name}</p>;
        }
      })()}
    </>
  );
}
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Handle unmont

const useBetterProductApi = () => {
  // ... useState

  React.useEffect(() => {
    let mounted = true;
    const fetchData = async () => {
      // ... setState
      try {
        const data = await fetchProduct();
        if (!mounted) return;
        // ... setState
      } catch (error) {
        if (!mounted) return;
        // ... setState
      }
    };
    fetchData();
    return () => (mounted = false);
  }, []);

  return state;
};

Isn't it solved already?

react-query

function App() {
  const { status, data, error } = useQuery("produt", () => fetchProduct());

  return (
    <>
      <h2>Product</h2>
      {(() => {
        if (status === "idle" || status === "loading") {
          return <p>Loading ...</p>;
        }
        if (status === "error") {
          return <p>Something went wrong: {error}</p>;
        }
        if (status === "success") {
          return <p>{data.name}</p>;
        }
      })()}
    </>
  );
}

Backend agnostic

Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)

Use in Marketplace

Featured services

const { data } = useQuery('featuredServices', () => storeService.fetchPopularProducts(0, 3));
const popularProducts = data?.results ?? [];

Categories

const { data: categorization } = useQuery('categorization', () => storeService.fetchProductFilters(), {
  initialData: {
    categories: [],
    industries: [],
    productTypes: [],
    providers: [],
  },
  initialStale: true,
});

Countries

import { useQuery } from 'react-query';
import { settingsService } from 'src/libs/settingsService/settingsService';

export const useCountryData = () => {
  const { data: countryData, ...rest } = useQuery(
    'countryData',
    async () => {
      const data = await settingsService.getCountryData();
      data.sort((pre, next) => pre.name.localeCompare(next.name));
      return data;
    },
    {
      cacheTime: Infinity,
    },
  );
  return { countryData, ...rest };
};

Search input

const [inputText, setInputText] = useState('');
const [debouncedInputText] = useDebounce(inputText, 200);

const { data, status, refetch } = useQuery(['searchbox', debouncedInputText], search, {
  enabled: false, // do not make a request until we call refetch()
});

useEffect(() => {
  if (debouncedInputText !== '') refetch();
}, [debouncedInputText, refetch]);

Notes

  • Caching is fantastic
  • No need for global state (and boilerplate)

Pagination/Inf. scroll

const { data, status, isFetchingMore, fetchMore, canFetchMore } = useInfiniteQuery(
  ['products', queryParams],
  (_key, _q, page = 0) => {
    return storeService.getProducts(queryParams.query, queryParams.sortBy, page);
  },
  {
    getFetchMore: (lastData) => {
      if ((lastData.pageIndex + 1) * lastData.pageSize < lastData.total) {
        return lastData.pageIndex + 1;
      }
      return false;
    },
  },
);

{canFetchMore && (
  <button disabled={!!isFetchingMore} onClick={fetchMore}>
    {!isFetchingMore ? 'Load more' : 'Loading more...'}
  </button>
)}

Drawbacks

  • 3 retries by default
  • Fetch on window refocus by default
  • Weird API for requests that aren't automatic?!
const queryCache = new QueryCache({
  defaultConfig: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

<ReactQueryCacheProvider queryCache={queryCache}>
   {/* App */}
</ReactQueryCacheProvider>

Create/update/delete data

useMutation

  • Query Invalidation
  • Cache invalidation
  • Optimistic Updates
  • Rollbacks
  • Etc.

Competition

Questions?

Made with Slides.com