Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Merge pull request #32 from contiamo/debounce-request
Browse files Browse the repository at this point in the history
Add a debounce option to `Get`
  • Loading branch information
Tejas Kumar authored Sep 5, 2018
2 parents ccf91b0 + 49b231d commit 7e61a3c
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 2 deletions.
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -193,7 +194,10 @@ const MyAnimalsList = props => (
"OH NO!"
) : (
<>
<h1>Here are all my {props.animal}s!</h1>
<h1>
Here are all my {props.animal}
s!
</h1>
<ul>{animals.map(animal => <li>{animal}</li>)}</ul>
</>
)}
Expand All @@ -215,7 +219,10 @@ const MyAnimalsList = props => (
) : (
<div>
You should only see this after things are loaded.
<h1>Here are all my {props.animal}s!</h1>
<h1>
Here are all my {props.animal}
s!
</h1>
<ul>{animals.map(animal => <li>{animal}</li>)}</ul>
</div>
)
Expand Down Expand Up @@ -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 => (
<Get path={`/search?q=${props.query}`} debounce>
{data => (
<div>
<h1>Here's all the things I search</h1>
<ul>{data.map(thing => <li>{thing}</li>)}</ul>
</div>
)}
</Get>
);
```
Debounce also accepts a number, which tells `Get` how long to wait until doing the request.
```diff
const SearchThis = props => (
- <Get path={`/search?q=${props.query}`} debounce>
+ <Get path={`/search?q=${props.query}`} debounce={200 /*ms*/}>
{data => (
<div>
<h1>Here's all the things I search</h1>
<ul>{data.map(thing => <li>{thing}</li>)}</ul>
</div>
)}
</Get>
);
```
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 => (
<Get
path={`/search?q=${props.query}`}
- debounce={200}
+ debounce={{ wait: 200, options: { leading: true, maxWait: 300, trailing: false } }}
>
{data => (
<div>
<h1>Here's all the things I search</h1>
<ul>{data.map(thing => <li>{thing}</li>)}</ul>
</div>
)}
</Get>
);
```

### 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`.
Expand Down
63 changes: 63 additions & 0 deletions src/Get.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(<div />);

const { rerender } = render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="?test=1" debounce>
{children}
</Get>
</RestfulProvider>,
);

times(10, i =>
rerender(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path={`?test=${i + 1}`} debounce>
{children}
</Get>
</RestfulProvider>,
),
);

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(<div />);

const { rerender } = render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="?test=1">{children}</Get>
</RestfulProvider>,
);

times(10, i =>
rerender(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path={`?test=${i + 1}`}>{children}</Get>
</RestfulProvider>,
),
);

expect(apiCalls).toEqual(10);
});
});
});
25 changes: 25 additions & 0 deletions src/Get.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -84,6 +86,17 @@ export interface GetProps<TData, TError> {
*
*/
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;
}

/**
Expand All @@ -107,6 +120,18 @@ class ContextlessGet<TData, TError> extends React.Component<
GetProps<TData, TError>,
Readonly<GetState<TData, TError>>
> {
constructor(props: GetProps<TData, TError>) {
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<GetState<TData, TError>> = {
data: null, // Means we don't _yet_ have data.
response: null,
Expand Down

0 comments on commit 7e61a3c

Please sign in to comment.