Skip to content

Commit 88c777c

Browse files
authored
Merge pull request #8712 from marmelab/doc-fetchUtils
[Doc] Add documentation for `fetchJson`
2 parents 93b0cb0 + 353a01d commit 88c777c

File tree

5 files changed

+263
-1
lines changed

5 files changed

+263
-1
lines changed

docs/DataProviders.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ In this example, the `simpleRestProvider` accepts a second parameter to set auth
176176

177177
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.
178178

179-
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.
179+
For instance, the `simpleRestProvider` function accepts an HTTP client function as its second argument. By default, it uses react-admin's [`fetchUtils.fetchJson()`](./fetchJson.md) function as the HTTP client. It's similar to the HTML5 `fetch()`, except it handles JSON decoding and HTTP error codes automatically.
180180

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

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

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

221+
**Tip:** Have a look at the [`fetchJson` documentation](./fetchJson.md) to learn more about its features.
222+
221223
**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.
222224

223225
```

docs/fetchJson.md

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
layout: default
3+
title: "fetchJson"
4+
---
5+
6+
# `fetchJson`
7+
8+
React-admin includes a `fetchJson` utility function to make HTTP calls. It's a wrapper around the browser's `fetch` function, that adds the following features:
9+
10+
- It adds the `Content-Type='application/json'` header to all non GET requests
11+
- It adds the `Authorization` header with optional parameters
12+
- It makes it easier to add custom headers to all requests
13+
- It handles the JSON decoding of the response
14+
- It handles HTTP errors codes by throwing an `HttpError`
15+
16+
## Usage
17+
18+
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).
19+
20+
```jsx
21+
import { fetchUtils, Admin, Resource } from 'react-admin';
22+
import simpleRestProvider from 'ra-data-simple-rest';
23+
import { PostList } from './posts';
24+
25+
const httpClient = async (url, options = {}) => {
26+
const { status, headers, body, json } = fetchUtils.fetchJson(url, options);
27+
console.log('fetchJson result', { status, headers, body, json });
28+
return { status, headers, body, json };
29+
}
30+
const dataProvider = simpleRestProvider('http://path.to.my.api/', httpClient);
31+
32+
const App = () => (
33+
<Admin dataProvider={dataProvider}>
34+
<Resource name="posts" list={PostList} />
35+
</Admin>
36+
);
37+
```
38+
39+
**Tip:** `fetchJson` is included in the `fetchUtils` object exported by the `react-admin` package.
40+
41+
## Parameters
42+
43+
`fetchJson(url, options)` expects the following parameters:
44+
45+
- `url` **string** The URL to fetch
46+
- `options` **Object** The options to pass to the fetch call. Defaults to `{}`.
47+
- `options.user` **Object** The user object, used for the `Authorization` header
48+
- `options.user.token` **string** The token to pass as the `Authorization` header
49+
- `options.user.authenticated` **boolean** Whether the user is authenticated or not (the `Authorization` header will be set only if this is true)
50+
- `options.headers` **Headers** The headers to pass to the fetch call
51+
52+
## Return Value
53+
54+
`fetchJson` returns an object with the following properties:
55+
56+
- `status` **number** The HTTP status code
57+
- `headers` **Headers** The response headers
58+
- `body` **string** The response body
59+
- `json` **Object** The response body, parsed as JSON
60+
61+
## Adding Custom Headers
62+
63+
Here is an example of how to add custom headers to all requests:
64+
65+
```jsx
66+
import { fetchUtils, Admin, Resource } from 'react-admin';
67+
import simpleRestProvider from 'ra-data-simple-rest';
68+
import { PostList } from './posts';
69+
70+
const httpClient = (url, options = {}) => {
71+
if (!options.headers) {
72+
options.headers = new Headers({ Accept: 'application/json' });
73+
}
74+
// add your own headers here
75+
options.headers.set('X-Custom-Header', 'foobar');
76+
return fetchUtils.fetchJson(url, options);
77+
}
78+
const dataProvider = simpleRestProvider('http://path.to.my.api/', httpClient);
79+
80+
const App = () => (
81+
<Admin dataProvider={dataProvider}>
82+
<Resource name="posts" list={PostList} />
83+
</Admin>
84+
);
85+
```
86+
87+
## TypeScript Support
88+
89+
For TypeScript users, here is a typed example of a custom `httpClient` that adds custom headers to all requests:
90+
91+
```ts
92+
import { fetchUtils } from 'react-admin';
93+
94+
const httpClient = (url: string, options: fetchUtils.Options = {}) => {
95+
const customHeaders = (options.headers ||
96+
new Headers({
97+
Accept: 'application/json',
98+
})) as Headers;
99+
// add your own headers here
100+
customHeaders.set('X-Custom-Header', 'foobar');
101+
options.headers = customHeaders;
102+
return fetchUtils.fetchJson(url, options);
103+
}
104+
```
105+
106+
## Adding The `Authorization` Header
107+
108+
Here is an example of how to add the `Authorization` header to all requests, using a token stored in the browser's local storage:
109+
110+
```jsx
111+
import { fetchUtils } from 'react-admin';
112+
113+
const httpClient = (url, options = {}) => {
114+
const token = localStorage.getItem('token');
115+
const user = { token: `Bearer ${token}`, authenticated: !!token };
116+
return fetchUtils.fetchJson(url, {...options, user});
117+
}
118+
```
119+
120+
**Tip:** The `Authorization` header will only be added to the request if `user.authenticated` is `true`.
121+
122+
## Handling HTTP Errors
123+
124+
The `fetchJson` function rejects with an `HttpError` when the HTTP response status code is not in the 2xx range.
125+
126+
```jsx
127+
import { fetchUtils } from 'react-admin';
128+
129+
fetchUtils.fetchJson('https://jsonplaceholder.typicode.com/posts/1')
130+
.then(({ json }) => console.log('HTTP call succeeded. Return value:', json))
131+
.catch(error => console.log('HTTP call failed. Error message:', error));
132+
```

docs/navigation.html

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<li {% if page.path == 'useDeleteMany.md' %} class="active" {% endif %}><a class="nav-link" href="./useDeleteMany.html"><code>useDeleteMany</code></a></li>
3737
<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>
3838
<li {% if page.path == 'withLifecycleCallbacks.md' %} class="active" {% endif %}><a class="nav-link" href="./withLifecycleCallbacks.html"><code>withLifecycleCallbacks</code></a></li>
39+
<li {% if page.path == 'fetchJson.md' %} class="active" {% endif %}><a class="nav-link" href="./fetchJson.html"><code>fetchJson</code></a></li>
3940
</ul>
4041

4142
<ul><div>Security</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as React from 'react';
2+
import { fetchJson } from './fetch';
3+
4+
export default {
5+
title: 'ra-core/dataProvider/fetch',
6+
};
7+
8+
export const FetchJson = () => {
9+
const [token, setToken] = React.useState('secret');
10+
const [record, setRecord] = React.useState('');
11+
const [headerName, setHeaderName] = React.useState('X-Custom-Header');
12+
const [headerValue, setHeaderValue] = React.useState('foobar');
13+
14+
const user = { token: `Bearer ${token}`, authenticated: !!token };
15+
16+
const getHeaders = () => {
17+
const headers = new Headers();
18+
if (headerName) headers.set(headerName, headerValue);
19+
return headers;
20+
};
21+
22+
const doGet = () => {
23+
fetchJson('https://jsonplaceholder.typicode.com/posts/1', {
24+
user,
25+
headers: getHeaders(),
26+
}).then(({ status, headers, body, json }) => {
27+
console.log('GET result', { status, headers, body, json });
28+
setRecord(body);
29+
});
30+
};
31+
32+
const doPut = () => {
33+
fetchJson('https://jsonplaceholder.typicode.com/posts/1', {
34+
method: 'PUT',
35+
body: record,
36+
user,
37+
headers: getHeaders(),
38+
}).then(({ status, headers, body, json }) => {
39+
console.log('PUT result', { status, headers, body, json });
40+
setRecord(body);
41+
});
42+
};
43+
44+
return (
45+
<div
46+
style={{
47+
display: 'flex',
48+
flexDirection: 'column',
49+
width: 500,
50+
padding: 20,
51+
gap: 10,
52+
}}
53+
>
54+
<p style={{ backgroundColor: '#ffb', textAlign: 'center' }}>
55+
<b>Tip:</b> Open the DevTools network tab to see the HTTP
56+
Headers
57+
<br />
58+
<b>Tip:</b> Open the DevTools console tab to see the returned
59+
values
60+
</p>
61+
<div style={{ display: 'flex' }}>
62+
<label htmlFor="token" style={{ marginRight: 10 }}>
63+
Token:
64+
</label>
65+
<input
66+
id="token"
67+
type="text"
68+
value={token}
69+
onChange={e => setToken(e.target.value)}
70+
style={{ flexGrow: 1 }}
71+
title="Clear this field to simulate an unauthenticated user"
72+
/>
73+
</div>
74+
<div style={{ display: 'flex' }}>
75+
<label
76+
htmlFor="header-name"
77+
style={{ flexShrink: 0, marginRight: 10 }}
78+
>
79+
Custom header:
80+
</label>
81+
<input
82+
id="header-name"
83+
placeholder="header name"
84+
type="text"
85+
value={headerName}
86+
onChange={e => setHeaderName(e.target.value)}
87+
style={{ flexGrow: 1, marginRight: 10 }}
88+
title="Clear this field to remove the header"
89+
/>
90+
<input
91+
id="header-value"
92+
placeholder="header value"
93+
type="text"
94+
value={headerValue}
95+
onChange={e => setHeaderValue(e.target.value)}
96+
style={{ flexGrow: 1, minWidth: 100 }}
97+
/>
98+
</div>
99+
<button onClick={doGet}>Send GET request</button>
100+
<textarea
101+
value={record}
102+
rows={10}
103+
onChange={e => setRecord(e.target.value)}
104+
placeholder="body"
105+
/>
106+
<button onClick={doPut} disabled={!record}>
107+
Send PUT request
108+
</button>
109+
</div>
110+
);
111+
};

packages/ra-core/src/dataProvider/fetch.ts

+16
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ export const createHeadersFromOptions = (options: Options): Headers => {
2727
return requestHeaders;
2828
};
2929

30+
/**
31+
* Utility function to make HTTP calls. It's similar to the HTML5 `fetch()`, except it handles JSON decoding and HTTP error codes automatically.
32+
*
33+
* @param url the URL to call
34+
* @param options the options to pass to the HTTP call
35+
* @param options.user the user object, used for the Authorization header
36+
* @param options.user.token the token to pass as the Authorization header
37+
* @param options.user.authenticated whether the user is authenticated or not (the Authorization header will be set only if this is true)
38+
* @param options.headers the headers to pass to the HTTP call
39+
*
40+
* @returns {Promise} the Promise for a response object containing the following properties:
41+
* - status: the HTTP status code
42+
* - headers: the HTTP headers
43+
* - body: the response body
44+
* - json: the response body parsed as JSON
45+
*/
3046
export const fetchJson = (url, options: Options = {}) => {
3147
const requestHeaders = createHeadersFromOptions(options);
3248

0 commit comments

Comments
 (0)