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

[Doc] Add documentation for fetchJson #8712

Merged
merged 8 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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: 3 additions & 1 deletion docs/DataProviders.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ In this example, the `simpleRestProvider` accepts a second parameter to set auth

The `dataProvider` doesn't "speak" HTTP, so it doesn't have the notion of HTTP headers. If you need to pass custom headers to the API, the syntax depends on the Data Provider you use.

For instance, the `simpleRestProvider` function accepts an HTTP client function as second argument. By default, it uses react-admin's `fetchUtils.fetchJson()` function as HTTP client. It's similar to HTML5 `fetch()`, except it handles JSON decoding and HTTP error codes automatically.
For instance, the `simpleRestProvider` function accepts an HTTP client function as second argument. By default, it uses react-admin's [`fetchUtils.fetchJson()`](./fetchJson.md) function as HTTP client. It's similar to HTML5 `fetch()`, except it handles JSON decoding and HTTP error codes automatically.
slax57 marked this conversation as resolved.
Show resolved Hide resolved

That means that if you need to add custom headers to your requests, you can just *wrap* the `fetchJson()` call inside your own function:

Expand Down Expand Up @@ -218,6 +218,8 @@ const fetchJson = (url: string, options: fetchUtils.Options = {}) => {

Now all the requests to the REST API will contain the `X-Custom-Header: foobar` header.

**Tip:** Have a look at the [`fetchJson` documentation](./fetchJson.md) to learn more about its features.

**Warning**: If your API is on another domain as the JS code, you'll need to whitelist this header with an `Access-Control-Expose-Headers` [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) header.

```
Expand Down
130 changes: 130 additions & 0 deletions docs/fetchJson.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
layout: default
title: "fetchJson"
---

# `fetchJson`

React-admin includes a `fetchJson` utility function to make HTTP calls. It's a wrapper around the browser's `fetch` function, which adds the following features:
slax57 marked this conversation as resolved.
Show resolved Hide resolved

- It adds the `Content-Type='application/json'` header to all non GET requests
- It adds the `Authorization` header with optional parameters
- It makes it easier to add custom headers to all requests
- It handles the JSON decoding of the response
- It handles HTTP errors codes by throwing an `HttpError`

## Usage

You can use it to make HTTP calls directly, to build a custom [`dataProvider`](./DataProviderIntroduction.md), or pass it directly to any `dataProvider` that supports it, such as [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest).

```jsx
import { fetchUtils, Admin, Resource } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
slax57 marked this conversation as resolved.
Show resolved Hide resolved

const httpClient = async (url, options = {}) => {
const { status, headers, body, json } = fetchUtils.fetchJson(url, options);
console.log('fetchJson result', { status, headers, body, json });
return { status, headers, body, json };
}
const dataProvider = simpleRestProvider('http://path.to.my.api/', httpClient);

const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={PostList} />
</Admin>
);
```

**Tip:** `fetchJson` is included in the `fetchUtils` member of the `react-admin` package.
slax57 marked this conversation as resolved.
Show resolved Hide resolved

### Parameters
slax57 marked this conversation as resolved.
Show resolved Hide resolved

`fetchJson(url, options)` expects the following parameters:

- `url` **string** The URL to fetch
- `options` **Object** The options to pass to the fetch call. Defaults to `{}`.
- `options.user` **Object** The user object, used for the `Authorization` header
- `options.user.token` **string** The token to pass as the `Authorization` header
- `options.user.authenticated` **boolean** Whether the user is authenticated or not (the `Authorization` header will be set only if this is true)
- `options.headers` **Headers** The headers to pass to the fetch call

### Return Value

`fetchJson` returns an object with the following properties:

- `status` **number** The HTTP status code
- `headers` **Headers** The response headers
- `body` **string** The response body
- `json` **Object** The response body, parsed as JSON

## Adding Custom Headers

Here is an example of how to add custom headers to all requests:

```jsx
import { fetchUtils, Admin, Resource } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
slax57 marked this conversation as resolved.
Show resolved Hide resolved

const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
// add your own headers here
options.headers.set('X-Custom-Header', 'foobar');
return fetchUtils.fetchJson(url, options);
}
const dataProvider = simpleRestProvider('http://path.to.my.api/', httpClient);

const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={PostList} />
</Admin>
);
```

## TypeScript Support

For TypeScript users, here is a typed example of a custom `httpClient` that adds custom headers to all requests:

```ts
import { fetchUtils } from 'react-admin';

const httpClient = (url: string, options: fetchUtils.Options = {}) => {
const customHeaders = (options.headers ||
new Headers({
Accept: 'application/json',
})) as Headers;
// add your own headers here
customHeaders.set('X-Custom-Header', 'foobar');
options.headers = customHeaders;
return fetchUtils.fetchJson(url, options);
}
```

## Adding The `Authorization` Header

Here is an example of how to add the `Authorization` header to all requests, using a token stored in the browser's local storage:

```jsx
import { fetchUtils } from 'react-admin';

const httpClient = (url, options = {}) => {
const token = localStorage.getItem('token');
const user = { token: `Bearer ${token}`, authenticated: !!token };
return fetchUtils.fetchJson(url, {...options, user});
}
```

**Tip:** The `Authorization` header will only be added to the request if `user.authenticated` is `true`.

## Handling HTTP Errors

The `fetchJson` function rejects with an `HttpError` when the HTTP response status code is not in the 2xx range.

```jsx
import { fetchUtils } from 'react-admin';

fetchUtils.fetchJson('https://jsonplaceholder.typicode.com/posts/1')
.then(({ json }) => console.log('HTTP call succeeded. Return value:', json))
.catch(error => console.log('HTTP call failed. Error message:', error));
```
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<li {% if page.path == 'useDeleteMany.md' %} class="active" {% endif %}><a class="nav-link" href="./useDeleteMany.html"><code>useDeleteMany</code></a></li>
<li {% if page.path == 'useGetTree.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetTree.html"><code>useGetTree</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'withLifecycleCallbacks.md' %} class="active" {% endif %}><a class="nav-link" href="./withLifecycleCallbacks.html"><code>withLifecycleCallbacks</code></a></li>
<li {% if page.path == 'fetchJson.md' %} class="active" {% endif %}><a class="nav-link" href="./fetchJson.html"><code>fetchJson</code></a></li>
</ul>

<ul><div>Security</div>
Expand Down
111 changes: 111 additions & 0 deletions packages/ra-core/src/dataProvider/fetch.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as React from 'react';
import { fetchJson } from './fetch';

export default {
title: 'ra-core/dataProvider/fetch',
};

export const FetchJson = () => {
const [token, setToken] = React.useState('secret');
const [record, setRecord] = React.useState('');
const [headerName, setHeaderName] = React.useState('X-Custom-Header');
const [headerValue, setHeaderValue] = React.useState('foobar');

const user = { token: `Bearer ${token}`, authenticated: !!token };

const getHeaders = () => {
const headers = new Headers();
if (headerName) headers.set(headerName, headerValue);
return headers;
};

const doGet = () => {
fetchJson('https://jsonplaceholder.typicode.com/posts/1', {
user,
headers: getHeaders(),
}).then(({ status, headers, body, json }) => {
console.log('GET result', { status, headers, body, json });
setRecord(body);
});
};

const doPut = () => {
fetchJson('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PUT',
body: record,
user,
headers: getHeaders(),
}).then(({ status, headers, body, json }) => {
console.log('PUT result', { status, headers, body, json });
setRecord(body);
});
};

return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: 500,
padding: 20,
gap: 10,
}}
>
<p style={{ backgroundColor: '#ffb', textAlign: 'center' }}>
<b>Tip:</b> Open the DevTools network tab to see the HTTP
Headers
<br />
<b>Tip:</b> Open the DevTools console tab to see the returned
values
</p>
<div style={{ display: 'flex' }}>
<label htmlFor="token" style={{ marginRight: 10 }}>
Token:
</label>
<input
id="token"
type="text"
value={token}
onChange={e => setToken(e.target.value)}
style={{ flexGrow: 1 }}
title="Clear this field to simulate an unauthenticated user"
/>
</div>
<div style={{ display: 'flex' }}>
<label
htmlFor="header-name"
style={{ flexShrink: 0, marginRight: 10 }}
>
Custom header:
</label>
<input
id="header-name"
placeholder="header name"
type="text"
value={headerName}
onChange={e => setHeaderName(e.target.value)}
style={{ flexGrow: 1, marginRight: 10 }}
title="Clear this field to remove the header"
/>
<input
id="header-value"
placeholder="header value"
type="text"
value={headerValue}
onChange={e => setHeaderValue(e.target.value)}
style={{ flexGrow: 1, minWidth: 100 }}
/>
</div>
<button onClick={doGet}>Send GET request</button>
<textarea
value={record}
rows={10}
onChange={e => setRecord(e.target.value)}
placeholder="body"
/>
<button onClick={doPut} disabled={!record}>
Send PUT request
</button>
</div>
);
};
16 changes: 16 additions & 0 deletions packages/ra-core/src/dataProvider/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ export const createHeadersFromOptions = (options: Options): Headers => {
return requestHeaders;
};

/**
* Utility function to make HTTP calls. It's similar to HTML5 `fetch()`, except it handles JSON decoding and HTTP error codes automatically.
slax57 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param url the URL to call
* @param options the options to pass to the HTTP call
* @param options.user the user object, used for the Authorization header
* @param options.user.token the token to pass as the Authorization header
* @param options.user.authenticated whether the user is authenticated or not (the Authorization header will be set only if this is true)
* @param options.headers the headers to pass to the HTTP call
*
* @returns {Promise} the Promise for a response object containing the following properties:
* - status: the HTTP status code
* - headers: the HTTP headers
* - body: the response body
* - json: the response body parsed as JSON
*/
export const fetchJson = (url, options: Options = {}) => {
const requestHeaders = createHeadersFromOptions(options);

Expand Down