diff --git a/README.md b/README.md index 9ea66e2a..f4a87f32 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ As an abstraction, this tool allows for greater consistency and maintainability - [Loading and Error States](#loading-and-error-states) - [Lazy Fetching](#lazy-fetching) - [Response Resolution](#response-resolution) + - [Debouncing Requests](#debouncing-requests) - [TypeScript Integration](#typescript-integration) - [Mutations with `Mutate`](#mutations-with-mutate) - [`Mutate` Component API](#mutate-component-api) @@ -193,7 +194,10 @@ const MyAnimalsList = props => ( "OH NO!" ) : ( <> -

Here are all my {props.animal}s!

+

+ Here are all my {props.animal} + s! +

)} @@ -215,7 +219,10 @@ const MyAnimalsList = props => ( ) : (
You should only see this after things are loaded. -

Here are all my {props.animal}s!

+

+ Here are all my {props.animal} + s! +

) @@ -270,6 +277,62 @@ const myNestedData = props => ( ); ``` +### Debouncing Requests + +Some requests fire in response to a rapid succession of user events: things like autocomplete or resizing a window. For this reason, users sometimes need to wait until all the keystrokes are typed (until everything's _done_), before sending a request. + +Restful React exposes a `debounce` prop on `Get` that does exactly this. + +Here's an example: + +```jsx +const SearchThis = props => ( + + {data => ( +
+

Here's all the things I search

+ +
+ )} +
+); +``` + +Debounce also accepts a number, which tells `Get` how long to wait until doing the request. + +```diff +const SearchThis = props => ( +- ++ + {data => ( +
+

Here's all the things I search

+
    {data.map(thing =>
  • {thing}
  • )}
+
+ )} +
+); +``` + +It uses [lodash's debounce](https://lodash.com/docs/4.17.10#debounce) function under the hood, so you get all the benefits of it out of the box like so! + +```diff +const SearchThis = props => ( + + {data => ( +
+

Here's all the things I search

+
    {data.map(thing =>
  • {thing}
  • )}
+
+ )} +
+); +``` + ### TypeScript Integration One of the most poweful features of RESTful React, each component exported is strongly typed, empowering developers through self-documenting APIs. As for _returned_ data, simply tell your data prop _what_ you expect, and it'll be available to you throughout your usage of `children`. diff --git a/src/Get.test.tsx b/src/Get.test.tsx index 5a235fa4..eaa1532c 100644 --- a/src/Get.test.tsx +++ b/src/Get.test.tsx @@ -1,5 +1,6 @@ import "isomorphic-fetch"; import "jest-dom/extend-expect"; +import times from "lodash/times"; import nock from "nock"; import React from "react"; import { cleanup, render, wait } from "react-testing-library"; @@ -294,4 +295,66 @@ describe("Get", () => { expect(children.mock.calls[3][0]).toEqual({ id: 2 }); }); }); + + describe("with debounce", () => { + it("should call the API only 1 time", async () => { + let apiCalls = 0; + nock("https://my-awesome-api.fake") + .filteringPath(/test=[^&]*/g, "test=XXX") + .get("/?test=XXX") + .reply(200, () => ++apiCalls) + .persist(); + + const children = jest.fn(); + children.mockReturnValue(
); + + const { rerender } = render( + + + {children} + + , + ); + + times(10, i => + rerender( + + + {children} + + , + ), + ); + + await wait(() => expect(apiCalls).toEqual(1)); + }); + + it("should call the API only 10 times without debounce", async () => { + let apiCalls = 0; + nock("https://my-awesome-api.fake") + .filteringPath(/test=[^&]*/g, "test=XXX") + .get("/?test=XXX") + .reply(200, () => ++apiCalls) + .persist(); + + const children = jest.fn(); + children.mockReturnValue(
); + + const { rerender } = render( + + {children} + , + ); + + times(10, i => + rerender( + + {children} + , + ), + ); + + expect(apiCalls).toEqual(10); + }); + }); }); diff --git a/src/Get.tsx b/src/Get.tsx index e69a5657..6da6408c 100644 --- a/src/Get.tsx +++ b/src/Get.tsx @@ -1,3 +1,5 @@ +import { DebounceSettings } from "lodash"; +import debounce from "lodash/debounce"; import * as React from "react"; import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context"; import { processResponse } from "./util/processResponse"; @@ -84,6 +86,17 @@ export interface GetProps { * */ base?: string; + /** + * How long do we wait between subsequent requests? + * Uses [lodash's debounce](https://lodash.com/docs/4.17.10#debounce) under the hood. + */ + debounce?: + | { + wait?: number; + options: DebounceSettings; + } + | boolean + | number; } /** @@ -107,6 +120,18 @@ class ContextlessGet extends React.Component< GetProps, Readonly> > { + constructor(props: GetProps) { + super(props); + + if (typeof props.debounce === "object") { + this.fetch = debounce(this.fetch, props.debounce.wait, props.debounce.options); + } else if (typeof props.debounce === "number") { + this.fetch = debounce(this.fetch, props.debounce); + } else if (props.debounce) { + this.fetch = debounce(this.fetch); + } + } + public readonly state: Readonly> = { data: null, // Means we don't _yet_ have data. response: null,