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

Add a debounce option to Get #32

Merged
merged 5 commits into from
Sep 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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