Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use requests PR #159

Merged
merged 20 commits into from
Dec 17, 2019
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.3.0

- New React hook - `useRequests` (rctbusk [#159](https://github.com/amplitude/redux-query/pull/159))

## 3.2.1

- Generic Entities Typing Fix (petejohanson [#155](https://github.com/amplitude/redux-query/pull/155))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ A library for managing network state in Redux.
## React API

- [useRequest](https://amplitude.github.io/redux-query/docs/use-request)
- [useRequests](https://amplitude.github.io/redux-query/docs/use-requests)
- [useMutation](https://amplitude.github.io/redux-query/docs/use-mutation)
- [connectRequest](https://amplitude.github.io/redux-query/docs/connect-request)

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/simple.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Simple Example

This example is a very simple web app that has only one feature – you can view and update your username. The purpose of this example is to demonstrate how requests and mutations (including optimistic updates) work with redux-query.

[![Edit redux-query Basic Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/redux-query-hacker-news-example-jpoko?fontsize=14)
[![Edit redux-query Basic Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/redux-query-basic-example-8x4zo?fontsize=14)

**Note**: This example fakes a server with a custom mock [network interface](../network-interfaces). In a real app, you would want to use a network interface that actually communicates to a server via HTTP.

Expand Down
4 changes: 2 additions & 2 deletions docs/redux-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Similarly to how mutations are triggered by dispatching `mutateAsync` actions, y

You can also Promise-chain on dispatched `requestAsync` actions, but a Promise will only be returned if `redux-query` determines it will make a network request. For example, if the query config does not have `force` set to `true` and a previous request with the same query key previously succeeded, then a Promise will not be returned. So be sure to always check that the returned value from a dispatched `requestAsync` is a Promise before interacting with it.

**Note**: With redux-query-react, [`connectRequest`](connect-request) and [`useRequest`](use-request) automatically dispatch `requestAsync` actions when the associated component mounts (if the provided query config is valid). It will also dispatch `requestAsync` actions whenever the query config updates with a new query key.
**Note**: With redux-query-react, [`connectRequest`](connect-request), [`useRequest`](use-request), and [`useRequests`](use-requests) automatically dispatch `requestAsync` actions when the associated component mounts (if the provided query config is valid). It will also dispatch `requestAsync` actions whenever the query config updates with a new query key.

## cancelQuery

Expand All @@ -66,7 +66,7 @@ import { cancelQuery } from 'redux-query';
store.dispatch(cancelQuery('{"url":"/api/playlists"}'));
```

**Note**: With redux-query-react, [`connectRequest`](connect-request) and [`useRequest`](use-request) automatically dispatch `cancelQuery` actions when the associated component unmounts.
**Note**: With redux-query-react, [`connectRequest`](connect-request), [`useRequest`](use-request), and [`useRequests`](use-requests) automatically dispatch `cancelQuery` actions when the associated component unmounts.

## updateEntities

Expand Down
2 changes: 1 addition & 1 deletion docs/requests-vs-mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Mutations always trigger a network request.

Dispatch a [requestAsync](redux-actions#requestasync) action with your Redux store.

If you'd like to make a request right from a React component, install redux-query-react and use either the [useRequest](use-request) hooks or the [connectRequest](connect-request) HOC.
If you'd like to make a request right from a React component, install redux-query-react and use either the [useRequest](use-request) and [`useRequests`](use-requests) hooks or the [connectRequest](connect-request) HOC.

## How to make mutations

Expand Down
2 changes: 1 addition & 1 deletion docs/upgrade-guides/v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ If your app uses connectRequest:
2. Add redux-query-react as a dependency.
3. Follow the [getting started](getting-started#setup-react-integration) section for setting up the React integration.
4. If any of your existing connectRequest calls use the `withRef` configuration option, rename that to `forwardRef`.
5. Going forwards, consider using [useRequest](../use-request) and [useMutation]('../use-mutation) instead of connectRequest for new components.
5. Going forwards, consider using [useRequest](../use-request), [useRequests](../use-requests), and [useMutation]('../use-mutation) instead of connectRequest for new components.

And finally:

Expand Down
66 changes: 66 additions & 0 deletions docs/use-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
id: use-requests
title: useRequests
---

`useRequests` is one of the React hooks provided by redux-query-react. It's intended to be used for cases when you have a component that has network dependencies (i.e. things need to load from the server in order for this component to render properly). Its behavior is as follows:

1. When the associated component first renders, if there is a valid array of query configs provided, then requests will be made (by dispatching a [`requestAsync`](redux-actions#requestasync) action).
2. Whenever the query configs change and its query keys also change as a result, then new requests will be made. Also, if there are previously-issued requests that is still in-flight, then a [`cancelQuery`](redux-actions#cancelquery) action will be dispatched which will attempt to abort the active network requests.
3. When the associated component unmounts and there are requests in-flight, then [`cancelQuery`](redux-actions#cancelquery) actions will be dispatched which will attempt to abort the active network requests.

## API

`useRequests` takes a single parameter – an array of query configs. If you pass null, undefined, or an invalid array of query configs as the parameter to `useRequests`, the values will be ignored.

`useRequests` returns a tuple-like array, where the first value in the tuple is an object representing the state of the requests. This object will have "isFinished" and "isPending" keys. "isFinished" will be false until all queries in the query array are cmopleted. "isPending" will be true as long as any of the queries are still in flight. The second value in the tuple is a callback to re-issue all the requests, even if previous requests with the same query key have already been made and resulted in a successful server responses.

## Note

`useRequests` is meant to for cases in which the number of querys that are going to be dispatched is unknown or arbitrary. The point of this hook is for when a batch of queries need to be called and state of the individual queries is not important. What is important for the use case of this hook is the overall state of all of the input queries.

The below example takes an array of user requests. The list of users will not be rendered until every request in array of requests is complete.

## Example

```javascript
import * as React from 'react';
import { useSelector } from 'react-redux';
import { useRequests } from 'redux-query-react';

const getUsers = state => state.entities.users;

const userIds = ['ryan.busk@amplitude.com', 'ryan@amplitude.com' /* etc... */]; // arbitrary number of users

const requests = userIds.map(userId => {
return {
url: `/api/users/${userId}`,
update: {
users: (oldValue, newValue) => {
return (oldValue[user] = newValue);
},
},
};
}); // unknown number of requests

const NotificationsView = () => {
const users = useSelector(getUsers) || [];

const [{ isPending, isFinished }, refresh] = useRequests(requests);

if (!isFinished) {
return 'Loading…';
}

return (
<div>
<button onClick={refresh}>Refresh</button>
<ul>
{users.map(user => (
<User key={user.userId} />
))}
</ul>
</div>
);
};
```
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"packages": ["packages/*", "site"],
"version": "3.2.1",
"version": "3.3.0",
"npmClient": "yarn"
}
4 changes: 2 additions & 2 deletions packages/redux-query-interface-superagent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-query-interface-superagent",
"version": "3.2.1",
"version": "3.3.0",
"description": "The default interface for redux-query, powered by superagent",
"homepage": "https://github.com/amplitude/redux-query",
"main": "dist/commonjs/index.js",
Expand Down Expand Up @@ -49,7 +49,7 @@
"eslint": "^5.11.1",
"eslint-plugin-import": "^2.14.0",
"jest": "^24.8.0",
"redux-query": "^3.2.1",
"redux-query": "^3.3.0",
"rimraf": "^2.4.3",
"superagent-mock": "^3.7.0",
"terser-webpack-plugin": "^1.3.0",
Expand Down
1 change: 1 addition & 0 deletions packages/redux-query-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ redux-query-react is a library for directly integrating [redux-query](https://am
## API

- [useRequest](https://amplitude.github.io/redux-query/docs/use-request)
- [useRequests](https://amplitude.github.io/redux-query/docs/use-requests)
- [useMutation](https://amplitude.github.io/redux-query/docs/use-mutation)
- [connectRequest](https://amplitude.github.io/redux-query/docs/connect-request)

Expand Down
20 changes: 20 additions & 0 deletions packages/redux-query-react/flow-test/hooks/use-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @flow

import * as React from 'react';

import useRequest from '../../src/hooks/use-requests';

const Card = () => {
const [{ isPending }] = useRequest([
{
url: '/api',
},
{ url: '/test' },
]);

return <div>{isPending ? 'loading…' : 'loaded'}</div>;
};

export const App = () => {
return <Card />;
};
4 changes: 2 additions & 2 deletions packages/redux-query-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-query-react",
"version": "3.2.1",
"version": "3.3.0",
"description": "The React interface for integrating with redux-query",
"homepage": "https://github.com/amplitude/redux-query",
"main": "dist/commonjs/index.js",
Expand Down Expand Up @@ -68,7 +68,7 @@
"react-redux": "7.1.0",
"react-test-renderer": "^16.8.6",
"redux": "^4.0.1",
"redux-query": "^3.2.1",
"redux-query": "^3.3.0",
"rimraf": "^2.4.3",
"terser-webpack-plugin": "^1.3.0",
"webpack": "^4.19.0",
Expand Down
78 changes: 21 additions & 57 deletions packages/redux-query-react/src/components/connect-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { requestAsync, cancelQuery, getQueryKey } from 'redux-query';
import type { QueryConfig, QueryKey } from 'redux-query/types.js.flow';

import useConstCallback from '../hooks/use-const-callback';
import useMemoizedQueryConfigs from '../hooks/use-memoized-query-configs';

type MapPropsToConfigs<T> = (props: T) => QueryConfig | Array<QueryConfig>;

Expand Down Expand Up @@ -54,60 +55,6 @@ const diffQueryConfigs = (
return { cancelKeys, requestQueryConfigs };
};

/**
* This hook memoizes the list of query configs that are returned form the `mapPropsToConfigs`
* function. It also transforms the query configs to set `retry` to `true` and pass a
* synchronous callback to track pending state.
*
* `mapPropsToConfigs` may return null, undefined, a single query config,
* or a list of query configs. null and undefined values are ignored, and single query configs are
* normalized to be lists.
*
* Memoization is handled by comparing query keys. If the list changes in size, or any query config
* in the list's query key changes, an entirely new list of query configs is returned.
*/
const useMemoizedQueryConfigs = <Config>(
mapPropsToConfigs: MapPropsToConfigs<Config>,
props: Config,
callback: (queryKey: QueryKey) => void,
) => {
const queryConfigs = normalizeToArray(mapPropsToConfigs(props))
.map(
(queryConfig: QueryConfig): ?QueryConfig => {
const queryKey = getQueryKey(queryConfig);

if (queryKey) {
return {
...queryConfig,
retry: true,
unstable_preDispatchCallback: () => {
callback(queryKey);
},
};
}
},
)
.filter(Boolean);
const [memoizedQueryConfigs, setMemoizedQueryConfigs] = React.useState(queryConfigs);
const previousQueryKeys = React.useRef<Array<QueryKey>>(
queryConfigs.map(getQueryKey).filter(Boolean),
);

React.useEffect(() => {
const queryKeys = queryConfigs.map(getQueryKey).filter(Boolean);

if (
queryKeys.length !== previousQueryKeys.current.length ||
queryKeys.some((queryKey, i) => previousQueryKeys.current[i] !== queryKey)
) {
previousQueryKeys.current = queryKeys;
setMemoizedQueryConfigs(queryConfigs);
}
}, [queryConfigs]);

return memoizedQueryConfigs;
};

const useMultiRequest = <Config>(mapPropsToConfigs: MapPropsToConfigs<Config>, props: Config) => {
const reduxDispatch = useDispatch();

Expand Down Expand Up @@ -141,11 +88,28 @@ const useMultiRequest = <Config>(mapPropsToConfigs: MapPropsToConfigs<Config>, p
}
});

const finishedCallback = useConstCallback(() => {
(queryKey: QueryKey) => {
pendingRequests.current.delete(queryKey);
};
});

const transformQueryConfig = useConstCallback(
(queryConfig: ?QueryConfig): ?QueryConfig => {
return {
...queryConfig,
unstable_preDispatchCallback: finishedCallback,
retry: true,
};
},
);

// Query configs are memoized based on query key. As long as the query keys in the list don't
// change, the query config list won't change.
const queryConfigs = useMemoizedQueryConfigs(mapPropsToConfigs, props, (queryKey: QueryKey) => {
pendingRequests.current.delete(queryKey);
});
const queryConfigs = useMemoizedQueryConfigs(
normalizeToArray(mapPropsToConfigs(props)),
transformQueryConfig,
);

const forceRequest = React.useCallback(() => {
queryConfigs.forEach(requestReduxAction => {
Expand Down
60 changes: 60 additions & 0 deletions packages/redux-query-react/src/hooks/use-memoized-query-configs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @flow

import * as React from 'react';
import { getQueryKey } from 'redux-query';

import type { QueryConfig, QueryKey } from 'redux-query/types.js.flow';

const identity = x => x;

/**
* This hook memoizes the list of query configs that are returned form the `mapPropsToConfigs`
* function. It also transforms the query configs to set `retry` to `true` and pass a
* synchronous callback to track pending state.
*
* `mapPropsToConfigs` may return null, undefined, a single query config,
* or a list of query configs. null and undefined values are ignored, and single query configs are
* normalized to be lists.
*
* Memoization is handled by comparing query keys. If the list changes in size, or any query config
* in the list's query key changes, an entirely new list of query configs is returned.
*/

const useMemoizedQueryConfigs = (
providedQueryConfigs: ?Array<QueryConfig>,
transform: (?QueryConfig) => ?QueryConfig = identity,
) => {
const queryConfigs = providedQueryConfigs
? providedQueryConfigs
.map(
(queryConfig: ?QueryConfig): ?QueryConfig => {
const queryKey = getQueryKey(queryConfig);

if (queryKey) {
return transform(queryConfig);
}
},
)
.filter(Boolean)
: [];
const [memoizedQueryConfigs, setMemoizedQueryConfigs] = React.useState(queryConfigs);
const previousQueryKeys = React.useRef<Array<QueryKey>>(
queryConfigs.map(getQueryKey).filter(Boolean),
);

React.useEffect(() => {
const queryKeys = queryConfigs.map(getQueryKey).filter(Boolean);

if (
queryKeys.length !== previousQueryKeys.current.length ||
queryKeys.some((queryKey, i) => previousQueryKeys.current[i] !== queryKey)
) {
previousQueryKeys.current = queryKeys;
setMemoizedQueryConfigs(queryConfigs);
}
}, [queryConfigs]);

return memoizedQueryConfigs;
};

export default useMemoizedQueryConfigs;
Loading