Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit 092e0cb

Browse files
authored
useDataset and useThing hooks (#119)
* Add SWR * Add useDataset/useThing * Bump version, add exports for useThing/useDataset * get dataset from context if datasetIri is not defined * get Thing from context if thingIri is not defined
1 parent bbe6d36 commit 092e0cb

File tree

10 files changed

+397
-55
lines changed

10 files changed

+397
-55
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.11.0 ( September 14, 2020 )
2+
3+
### Added
4+
5+
- Add useThing and useDataset hooks
6+
17
## 0.9.8 ( September 9, 2020 )
28

39
### Added

package-lock.json

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@inrupt/solid-ui-react",
3-
"version": "0.10.1",
3+
"version": "0.11.0",
44
"description": "Set of UI libraries using @solid/core",
55
"main": "dist/index.js",
66
"types": "dist/src/index.d.ts",
@@ -88,6 +88,7 @@
8888
"@inrupt/solid-client": "^0.2.0",
8989
"@inrupt/solid-client-authn-browser": "^0.1.1",
9090
"@types/react-table": "^7.0.22",
91-
"react-table": "^7.5.0"
91+
"react-table": "^7.5.0",
92+
"swr": "^0.3.2"
9293
}
9394
}

src/context/datasetContext/index.test.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import * as React from "react";
2424
import { RenderResult, render, waitFor } from "@testing-library/react";
2525
import * as SolidFns from "@inrupt/solid-client";
26+
import useDataset from "../../hooks/useDataset";
2627
import DatasetContext, { DatasetProvider } from "./index";
2728

29+
jest.mock("../../hooks/useDataset");
30+
2831
let documentBody: RenderResult;
2932

3033
const mockUrl = "https://some-interesting-value.com";
@@ -119,6 +122,10 @@ function ExampleComponentWithDatasetUrl(): React.ReactElement {
119122

120123
describe("Testing DatasetContext", () => {
121124
it("matches snapshot with Dataset provided", () => {
125+
(useDataset as jest.Mock).mockReturnValue({
126+
dataset: undefined,
127+
error: undefined,
128+
});
122129
documentBody = render(
123130
<DatasetProvider dataset={mockDataSetWithResourceInfo}>
124131
<ExampleComponentWithDataset />
@@ -129,7 +136,10 @@ describe("Testing DatasetContext", () => {
129136
});
130137

131138
it("matches snapshot when fetching fails", async () => {
132-
jest.spyOn(SolidFns, "fetchLitDataset").mockRejectedValue(null);
139+
(useDataset as jest.Mock).mockReturnValue({
140+
dataset: undefined,
141+
error: "Error",
142+
});
133143

134144
documentBody = render(
135145
<DatasetProvider datasetUrl="https://some-broken-resource.com">
@@ -141,6 +151,10 @@ describe("Testing DatasetContext", () => {
141151
});
142152

143153
it("matches snapshot when fetching", async () => {
154+
(useDataset as jest.Mock).mockReturnValue({
155+
dataset: undefined,
156+
error: undefined,
157+
});
144158
documentBody = render(
145159
<DatasetProvider
146160
datasetUrl="https://some-broken-resource.com"
@@ -156,21 +170,25 @@ describe("Testing DatasetContext", () => {
156170
});
157171

158172
describe("Functional testing", () => {
159-
it("Should call fetchLitDataset", async () => {
160-
jest
161-
.spyOn(SolidFns, "fetchLitDataset")
162-
.mockResolvedValue(mockDataSetWithResourceInfo);
173+
it("Should call useDataset", async () => {
174+
(useDataset as jest.Mock).mockReturnValue({
175+
dataset: mockDataSetWithResourceInfo,
176+
error: undefined,
177+
});
163178

164179
render(
165180
<DatasetProvider datasetUrl={mockUrl}>
166181
<ExampleComponentWithDatasetUrl />
167182
</DatasetProvider>
168183
);
169-
expect(SolidFns.fetchLitDataset).toHaveBeenCalled();
184+
expect(useDataset).toHaveBeenCalled();
170185
});
171-
it("When fetchLitDataset fails, should call onError if passed", async () => {
186+
it("When useDataset return an error, should call onError if passed", async () => {
187+
(useDataset as jest.Mock).mockReturnValue({
188+
dataset: undefined,
189+
error: "Error",
190+
});
172191
const onError = jest.fn();
173-
(SolidFns.fetchLitDataset as jest.Mock).mockRejectedValue(null);
174192
render(
175193
<DatasetProvider
176194
onError={onError}
@@ -179,6 +197,6 @@ describe("Functional testing", () => {
179197
<ExampleComponentWithDatasetUrl />
180198
</DatasetProvider>
181199
);
182-
await waitFor(() => expect(onError).toHaveBeenCalled());
200+
await waitFor(() => expect(onError).toHaveBeenCalledWith("Error"));
183201
});
184202
});

src/context/datasetContext/index.tsx

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,10 @@
1919
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2020
*/
2121

22-
import React, {
23-
createContext,
24-
ReactElement,
25-
useState,
26-
useEffect,
27-
useCallback,
28-
useContext,
29-
} from "react";
30-
import {
31-
fetchLitDataset,
32-
LitDataset,
33-
WithResourceInfo,
34-
UrlString,
35-
} from "@inrupt/solid-client";
22+
import React, { createContext, ReactElement } from "react";
23+
import { LitDataset, WithResourceInfo, UrlString } from "@inrupt/solid-client";
3624

37-
import SessionContext from "../sessionContext";
25+
import useDataset from "../../hooks/useDataset";
3826

3927
export interface IDatasetContext {
4028
dataset: LitDataset | (LitDataset & WithResourceInfo) | undefined;
@@ -68,35 +56,16 @@ export const DatasetProvider = ({
6856
datasetUrl,
6957
loading,
7058
}: RequireDatasetOrDatasetUrl): ReactElement => {
71-
const { fetch } = useContext(SessionContext);
72-
const [dataset, setDataset] = useState<
73-
LitDataset | (LitDataset & WithResourceInfo) | undefined
74-
>(propDataset);
59+
const { dataset, error } = useDataset(datasetUrl);
7560

76-
const fetchDataset = useCallback(
77-
async (url: string) => {
78-
try {
79-
const resource = await fetchLitDataset(url, { fetch });
80-
setDataset(resource);
81-
} catch (error) {
82-
if (onError) {
83-
onError(error);
84-
}
85-
}
86-
},
87-
[onError, fetch]
88-
);
89-
90-
useEffect(() => {
91-
if (!dataset && datasetUrl) {
92-
// eslint-disable-next-line no-void
93-
void fetchDataset(datasetUrl);
94-
}
95-
}, [dataset, datasetUrl, fetchDataset]);
61+
if (error && onError) {
62+
onError(error);
63+
}
9664

65+
const datasetToUse = propDataset ?? dataset;
9766
return (
98-
<DatasetContext.Provider value={{ dataset }}>
99-
{dataset ? children : loading || <span>Fetching data...</span>}
67+
<DatasetContext.Provider value={{ dataset: datasetToUse }}>
68+
{datasetToUse ? children : loading || <span>Fetching data...</span>}
10069
</DatasetContext.Provider>
10170
);
10271
};

src/hooks/useDataset/index.test.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Copyright 2020 Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
import * as React from "react";
23+
import { renderHook } from "@testing-library/react-hooks";
24+
import { SWRConfig, cache } from "swr";
25+
import * as SolidFns from "@inrupt/solid-client";
26+
import { Session } from "@inrupt/solid-client-authn-browser";
27+
import DatasetContext from "../../context/datasetContext";
28+
import SessionContext from "../../context/sessionContext";
29+
import useDataset from ".";
30+
31+
describe("useDataset() hook", () => {
32+
const mockDatasetIri = "https://mock.url";
33+
const mockDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri);
34+
const mockContextDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri);
35+
const mockGetSolidDataset = jest
36+
.spyOn(SolidFns, "getSolidDataset")
37+
.mockResolvedValue(mockDataset);
38+
const mockFetch = jest.fn();
39+
const wrapper = ({ children }: { children: React.ReactNode }) => (
40+
<SessionContext.Provider
41+
value={{
42+
fetch: mockFetch,
43+
sessionRequestInProgress: true,
44+
session: {} as Session,
45+
}}
46+
>
47+
<DatasetContext.Provider value={{ dataset: mockContextDataset }}>
48+
<SWRConfig value={{ dedupingInterval: 0 }}>{children}</SWRConfig>
49+
</DatasetContext.Provider>
50+
</SessionContext.Provider>
51+
);
52+
53+
afterEach(() => {
54+
jest.clearAllMocks();
55+
cache.clear();
56+
});
57+
58+
it("should call getSolidDataset with given Iri", async () => {
59+
const { result, waitFor } = renderHook(() => useDataset(mockDatasetIri), {
60+
wrapper,
61+
});
62+
63+
expect(mockGetSolidDataset).toHaveBeenCalledTimes(1);
64+
expect(mockGetSolidDataset).toHaveBeenCalledWith(mockDatasetIri, {
65+
fetch: mockFetch,
66+
});
67+
await waitFor(() => expect(result.current.dataset).toBe(mockDataset));
68+
});
69+
70+
it("should call getSolidDataset with given options", async () => {
71+
const newMockFetch = jest.fn();
72+
const mockAdditionalOption = "additional option";
73+
const mockOptions = {
74+
fetch: newMockFetch,
75+
additionalOption: mockAdditionalOption,
76+
};
77+
78+
const { result, waitFor } = renderHook(
79+
() => useDataset(mockDatasetIri, mockOptions),
80+
{
81+
wrapper,
82+
}
83+
);
84+
85+
expect(mockGetSolidDataset).toHaveBeenCalledTimes(1);
86+
expect(mockGetSolidDataset).toHaveBeenCalledWith(mockDatasetIri, {
87+
fetch: newMockFetch,
88+
additionalOption: mockAdditionalOption,
89+
});
90+
await waitFor(() => expect(result.current.dataset).toBe(mockDataset));
91+
});
92+
93+
it("should return error if getSolidDataset call fails", async () => {
94+
mockGetSolidDataset.mockRejectedValue(new Error("async error"));
95+
96+
const { result, waitFor } = renderHook(() => useDataset(mockDatasetIri), {
97+
wrapper,
98+
});
99+
100+
expect(mockGetSolidDataset).toHaveBeenCalledTimes(1);
101+
expect(mockGetSolidDataset).toHaveBeenCalledWith(mockDatasetIri, {
102+
fetch: mockFetch,
103+
});
104+
await waitFor(() =>
105+
expect(result.current.error.message).toBe("async error")
106+
);
107+
});
108+
109+
it("should attempt to return dataset from context if uri is not defined", async () => {
110+
const { result, waitFor } = renderHook(() => useDataset(), {
111+
wrapper,
112+
});
113+
114+
expect(mockGetSolidDataset).toHaveBeenCalledTimes(0);
115+
116+
await waitFor(() =>
117+
expect(result.current.dataset).toBe(mockContextDataset)
118+
);
119+
});
120+
});

src/hooks/useDataset/index.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright 2020 Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
import { useContext } from "react";
23+
import useSWR from "swr";
24+
import {
25+
getSolidDataset,
26+
SolidDataset,
27+
WithResourceInfo,
28+
} from "@inrupt/solid-client";
29+
import SessionContext from "../../context/sessionContext";
30+
import DatasetContext from "../../context/datasetContext";
31+
32+
export default function useDataset(
33+
datasetIri?: string | null | undefined,
34+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
35+
options?: any
36+
): {
37+
dataset: SolidDataset | (SolidDataset & WithResourceInfo) | undefined;
38+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39+
error: any;
40+
} {
41+
const { fetch } = useContext(SessionContext);
42+
const { dataset: datasetFromContext } = useContext(DatasetContext);
43+
const { data, error } = useSWR(
44+
datasetIri ? [datasetIri, options, fetch] : null,
45+
() => {
46+
const requestOptions = {
47+
fetch,
48+
...options,
49+
};
50+
// useSWR will only call this fetcher if datasetUri is defined
51+
return getSolidDataset(datasetIri as string, requestOptions);
52+
}
53+
);
54+
55+
const dataset = datasetIri ? data : datasetFromContext;
56+
return { dataset, error };
57+
}

0 commit comments

Comments
 (0)