diff --git a/packages/react/test/Integration.test.tsx b/packages/react/test/Solid-Integration.test.tsx
similarity index 100%
rename from packages/react/test/Integration.test.tsx
rename to packages/react/test/Solid-Integration.test.tsx
diff --git a/packages/solid-react/.eslintrc b/packages/solid-react/.eslintrc
new file mode 100644
index 0000000..83c51a9
--- /dev/null
+++ b/packages/solid-react/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": ["../../.eslintrc"]
+}
\ No newline at end of file
diff --git a/packages/solid-react/.gitignore b/packages/solid-react/.gitignore
new file mode 100644
index 0000000..0c32b1f
--- /dev/null
+++ b/packages/solid-react/.gitignore
@@ -0,0 +1 @@
+test/test-server/data
\ No newline at end of file
diff --git a/packages/solid-react/LICENSE.txt b/packages/solid-react/LICENSE.txt
new file mode 100644
index 0000000..b87e67e
--- /dev/null
+++ b/packages/solid-react/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Jackson Morgan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/solid-react/README.md b/packages/solid-react/README.md
new file mode 100644
index 0000000..1458af6
--- /dev/null
+++ b/packages/solid-react/README.md
@@ -0,0 +1,138 @@
+# @ldo/solid-react
+
+`@ldo/solid-react` provides tool and hooks for easily building Solid applications using react.
+
+## Guide
+
+A full walkthrough for using the `@ldo/solid` library can be found in the [For Solid + React Guide](https://ldo.js.org/latest/guides/solid_react/)
+
+## Installation
+
+Navigate into your project's root folder and run the following command:
+```
+cd my_project/
+npx run @ldo/cli init
+```
+
+Now install the @ldo/solid library
+
+```
+npm i @ldo/solid @ldo/solid-react
+```
+
+
+
+Manual Installation
+
+
+If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently.
+
+```
+npm i @ldo/ldo @ldo/solid @ldo/solid-react
+```
+
+
+## Simple Example
+
+Below is a simple example of @ldo/solid-react in a real use-case. Assume that a ShapeType was previously generated and placed at `./.ldo/solidProfile.shapeTypess`.
+
+
+```typescript
+import type { FunctionComponent } from "react";
+import React, { useCallback } from "react";
+import {
+ BrowserSolidLdoProvider,
+ useResource,
+ useSolidAuth,
+ useSubject,
+} from "@ldo/solid-react";
+import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes";
+import { changeData, commitData } from "@ldo/solid";
+
+// The base component for the app
+const App: FunctionComponent = () => {
+ return (
+ /* The application should be surrounded with the BrowserSolidLdoProvider
+ this will set up all the underlying infrastructure for the application */
+
+
+
+ );
+};
+
+// A component that handles login
+const Login: FunctionComponent = () => {
+ // Get login information using the "useSolidAuth" hook
+ const { login, logout, session } = useSolidAuth();
+
+ const onLogin = useCallback(() => {
+ const issuer = prompt("What is your Solid IDP?");
+ // Call the "login" function to initiate login
+ if (issuer) login(issuer);
+ }, []);
+
+ // You can use session.isLoggedIn to check if the user is logged in
+ if (session.isLoggedIn) {
+ return (
+
+ {/* Get the user's webId from session.webId */}
+
Logged in as {session.webId}
+ {/* Use the logout function to log out */}
+
+
+
+ );
+ }
+ return ;
+};
+
+// Renders the name on the profile
+const Profile: FunctionComponent = () => {
+ const { session } = useSolidAuth();
+ // With useResource, you can automatically fetch a resource
+ const resource = useResource(session.webId);
+ // With useSubject, you can extract data from that resource
+ const profile = useSubject(SolidProfileShapeShapeType, session.webId);
+
+ const onNameChange = useCallback(async (e) => {
+ // Ensure that the
+ if (!profile || !resource) return;
+ // Change data lets you create a new object to make changes to
+ const cProfile = changeData(profile, resource);
+ // Change the name
+ cProfile.name = e.target.value;
+ // Commit the data back to the Pod
+ await commitData(cProfile);
+ }, []);
+
+ return ;
+};
+
+export default App;
+```
+
+## API Details
+
+Providers
+
+ - [BrowserSolidLdoProvider](https://ldo.js.org/latest/api/react/BrowserSolidLdoProvider/)
+ - [SolidLdoProvider](https://ldo.js.org/latest/api/react/SolidLdoProvider/)
+
+Hooks
+ - [useLdo](https://ldo.js.org/latest/api/react/useLdo/)
+ - [useResource](https://ldo.js.org/latest/api/react/useResource/)
+ - [useRootContainer](https://ldo.js.org/latest/api/react/useRootContainer/)
+ - [useSolidAuth](https://ldo.js.org/latest/api/react/useSolidAuth/)
+ - [useSubject](https://ldo.js.org/latest/api/react/useSubject/)
+ - [useMatchSubject](https://ldo.js.org/latest/api/react/useMatchSubject/)
+ - [useMatchObject](https://ldo.js.org/latest/api/react/useMatchSubject/)
+ - [useSubscribeToResource](https://ldo.js.org/latest/api/react/useMatchSubject/)
+
+## Sponsorship
+This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).
+
+[](https://nlnet.nl/)
+[](https://nlnet.nl/)
+
+## Liscense
+MIT
\ No newline at end of file
diff --git a/packages/solid-react/jest.config.js b/packages/solid-react/jest.config.js
new file mode 100644
index 0000000..4275a3f
--- /dev/null
+++ b/packages/solid-react/jest.config.js
@@ -0,0 +1,6 @@
+const sharedConfig = require("../../jest.config.js");
+module.exports = {
+ ...sharedConfig,
+ rootDir: "./",
+ testEnvironment: "jsdom",
+};
diff --git a/packages/solid-react/jest.setup.ts b/packages/solid-react/jest.setup.ts
new file mode 100644
index 0000000..22eed5f
--- /dev/null
+++ b/packages/solid-react/jest.setup.ts
@@ -0,0 +1,2 @@
+import "@inrupt/jest-jsdom-polyfills";
+globalThis.fetch = async () => new Response();
diff --git a/packages/solid-react/package.json b/packages/solid-react/package.json
new file mode 100644
index 0000000..b2b13a1
--- /dev/null
+++ b/packages/solid-react/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@ldo/solid-react",
+ "version": "1.0.0-alpha.1",
+ "description": "A React library for LDO and Solid",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json",
+ "watch": "tsc --watch",
+ "test": "npm run test:integration",
+ "test:watch": "jest --watch",
+ "prepublishOnly": "npm run test && npm run build",
+ "build:ldo": "ldo build --input src/shapes --output src/ldo",
+ "lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
+ "test:integration": "start-server-and-test start-test-server http://localhost:3002 start-integration-test",
+ "start-test-server": "ts-node ./test/test-server/runServer.ts",
+ "start-integration-test": "jest --coverage"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/o-development/ldobjects.git"
+ },
+ "author": "Jackson Morgan",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/o-development/ldobjects/issues"
+ },
+ "homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme",
+ "devDependencies": {
+ "@ldo/rdf-utils": "^1.0.0-alpha.1",
+ "@rdfjs/types": "^1.0.1",
+ "@testing-library/react": "^14.1.2",
+ "@types/jest": "^27.0.3",
+ "jest-environment-jsdom": "^27.0.0",
+ "start-server-and-test": "^2.0.3",
+ "ts-jest": "^27.1.2",
+ "ts-node": "^10.9.2"
+ },
+ "dependencies": {
+ "@inrupt/solid-client-authn-browser": "^2.0.0",
+ "@ldo/dataset": "^1.0.0-alpha.1",
+ "@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1",
+ "@ldo/ldo": "^1.0.0-alpha.1",
+ "@ldo/connected": "^1.0.0-alpha.1",
+ "@ldo/subscribable-dataset": "^1.0.0-alpha.1",
+ "@rdfjs/data-model": "^1.2.0",
+ "cross-fetch": "^3.1.6"
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "gitHead": "0287cd6371f06630763568dec5e41653f7b8583e"
+}
diff --git a/packages/solid-react/src/BrowserSolidLdoProvider.tsx b/packages/solid-react/src/BrowserSolidLdoProvider.tsx
new file mode 100644
index 0000000..6213890
--- /dev/null
+++ b/packages/solid-react/src/BrowserSolidLdoProvider.tsx
@@ -0,0 +1,97 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import type { FunctionComponent, PropsWithChildren } from "react";
+import type { LoginOptions, SessionInfo } from "./SolidAuthContext";
+import { SolidAuthContext } from "./SolidAuthContext";
+import {
+ getDefaultSession,
+ handleIncomingRedirect,
+ login as libraryLogin,
+ logout as libraryLogout,
+ fetch as libraryFetch,
+} from "@inrupt/solid-client-authn-browser";
+import { SolidLdoProvider } from "./SolidLdoProvider";
+
+const PRE_REDIRECT_URI = "PRE_REDIRECT_URI";
+
+export const BrowserSolidLdoProvider: FunctionComponent = ({
+ children,
+}) => {
+ const [session, setSession] = useState(getDefaultSession().info);
+ const [ranInitialAuthCheck, setRanInitialAuthCheck] = useState(false);
+
+ const runInitialAuthCheck = useCallback(async () => {
+ if (!window.localStorage.getItem(PRE_REDIRECT_URI)) {
+ window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href);
+ }
+
+ await handleIncomingRedirect({
+ restorePreviousSession: true,
+ });
+ // Set timout to ensure this happens after the redirect
+ setTimeout(() => {
+ setSession({ ...getDefaultSession().info });
+ window.history.replaceState(
+ {},
+ "",
+ window.localStorage.getItem(PRE_REDIRECT_URI),
+ );
+ window.localStorage.removeItem(PRE_REDIRECT_URI);
+
+ setRanInitialAuthCheck(true);
+ }, 0);
+ }, []);
+
+ const login = useCallback(async (issuer: string, options?: LoginOptions) => {
+ const cleanUrl = new URL(window.location.href);
+ cleanUrl.hash = "";
+ cleanUrl.search = "";
+ const fullOptions = {
+ redirectUrl: cleanUrl.toString(),
+ clientName: "Solid App",
+ oidcIssuer: issuer,
+ ...options,
+ };
+ window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href);
+ await libraryLogin(fullOptions);
+ setSession({ ...getDefaultSession().info });
+ }, []);
+
+ const logout = useCallback(async () => {
+ await libraryLogout();
+ setSession({ ...getDefaultSession().info });
+ }, []);
+
+ const signUp = useCallback(
+ async (issuer: string, options?: LoginOptions) => {
+ // The typings on @inrupt/solid-client-authn-core have not yet been updated
+ // TODO: remove this ts-ignore when they are updated.
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return login(issuer, { ...options, prompt: "create" });
+ },
+ [login],
+ );
+
+ useEffect(() => {
+ runInitialAuthCheck();
+ }, []);
+
+ const solidAuthFunctions = useMemo(
+ () => ({
+ runInitialAuthCheck,
+ login,
+ logout,
+ signUp,
+ session,
+ ranInitialAuthCheck,
+ fetch: libraryFetch,
+ }),
+ [login, logout, ranInitialAuthCheck, runInitialAuthCheck, session, signUp],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/solid-react/src/SolidAuthContext.ts b/packages/solid-react/src/SolidAuthContext.ts
new file mode 100644
index 0000000..4620b21
--- /dev/null
+++ b/packages/solid-react/src/SolidAuthContext.ts
@@ -0,0 +1,26 @@
+import type {
+ ISessionInfo,
+ ILoginInputOptions,
+} from "@inrupt/solid-client-authn-core";
+import { createContext, useContext } from "react";
+
+export type SessionInfo = ISessionInfo;
+export type LoginOptions = ILoginInputOptions;
+
+export interface SolidAuthFunctions {
+ login: (issuer: string, loginOptions?: LoginOptions) => Promise;
+ logout: () => Promise;
+ signUp: (issuer: string, loginOptions?: LoginOptions) => Promise;
+ fetch: typeof fetch;
+ session: SessionInfo;
+ ranInitialAuthCheck: boolean;
+}
+
+// There is no initial value for this context. It will be given in the provider
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+export const SolidAuthContext = createContext(undefined);
+
+export function useSolidAuth(): SolidAuthFunctions {
+ return useContext(SolidAuthContext);
+}
diff --git a/packages/solid-react/src/SolidLdoProvider.tsx b/packages/solid-react/src/SolidLdoProvider.tsx
new file mode 100644
index 0000000..a0c9664
--- /dev/null
+++ b/packages/solid-react/src/SolidLdoProvider.tsx
@@ -0,0 +1,55 @@
+import React, { createContext, useContext } from "react";
+import {
+ useMemo,
+ type FunctionComponent,
+ type PropsWithChildren,
+ useEffect,
+} from "react";
+import { useSolidAuth } from "./SolidAuthContext";
+import type { SolidLdoDataset } from "@ldo/solid";
+import { createSolidLdoDataset } from "@ldo/solid";
+import type { UseLdoMethods } from "./useLdoMethods";
+import { createUseLdoMethods } from "./useLdoMethods";
+
+export const SolidLdoReactContext =
+ // This will be set in the provider
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ createContext(undefined);
+
+export function useLdo(): UseLdoMethods {
+ return useContext(SolidLdoReactContext);
+}
+
+export interface SolidLdoProviderProps extends PropsWithChildren {}
+
+export const SolidLdoProvider: FunctionComponent = ({
+ children,
+}) => {
+ const { fetch } = useSolidAuth();
+
+ // Initialize storeDependencies before render
+ const solidLdoDataset: SolidLdoDataset = useMemo(() => {
+ const ldoDataset = createSolidLdoDataset({
+ fetch,
+ });
+ ldoDataset.setMaxListeners(1000);
+ return ldoDataset;
+ }, []);
+
+ // Keep context in sync with props
+ useEffect(() => {
+ solidLdoDataset.context.fetch = fetch;
+ }, [fetch]);
+
+ const value: UseLdoMethods = useMemo(
+ () => createUseLdoMethods(solidLdoDataset),
+ [solidLdoDataset],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx b/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx
new file mode 100644
index 0000000..8f25de1
--- /dev/null
+++ b/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx
@@ -0,0 +1,62 @@
+/* istanbul ignore file */
+import React, { useCallback, useMemo } from "react";
+import type { FunctionComponent, PropsWithChildren } from "react";
+import type { LoginOptions, SessionInfo } from "./SolidAuthContext";
+import { SolidAuthContext } from "./SolidAuthContext";
+import libraryFetch from "cross-fetch";
+import { SolidLdoProvider } from "./SolidLdoProvider";
+
+const DUMMY_SESSION: SessionInfo = {
+ isLoggedIn: false,
+ webId: undefined,
+ clientAppId: undefined,
+ sessionId: "no_session",
+ expirationDate: undefined,
+};
+
+export const UnauthenticatedSolidLdoProvider: FunctionComponent<
+ PropsWithChildren
+> = ({ children }) => {
+ const login = useCallback(
+ async (_issuer: string, _options?: LoginOptions) => {
+ throw new Error(
+ "login is not available for a UnauthenticatedSolidLdoProvider",
+ );
+ },
+ [],
+ );
+
+ const logout = useCallback(async () => {
+ throw new Error(
+ "logout is not available for a UnauthenticatedSolidLdoProvider",
+ );
+ }, []);
+
+ const signUp = useCallback(
+ async (_issuer: string, _options?: LoginOptions) => {
+ throw new Error(
+ "signUp is not available for a UnauthenticatedSolidLdoProvider",
+ );
+ },
+ [],
+ );
+
+ const solidAuthFunctions = useMemo(
+ () => ({
+ runInitialAuthCheck: () => {},
+ login,
+ logout,
+ signUp,
+ session: DUMMY_SESSION,
+ ranInitialAuthCheck: true,
+ fetch: libraryFetch,
+ }),
+ [login, logout, signUp],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/solid-react/src/index.ts b/packages/solid-react/src/index.ts
new file mode 100644
index 0000000..edbcc74
--- /dev/null
+++ b/packages/solid-react/src/index.ts
@@ -0,0 +1,12 @@
+export * from "./BrowserSolidLdoProvider";
+export * from "./UnauthenticatedSolidLdoProvider";
+export * from "./SolidAuthContext";
+
+export { useLdo } from "./SolidLdoProvider";
+
+// hooks
+export * from "./useResource";
+export * from "./useSubject";
+export * from "./useMatchSubject";
+export * from "./useMatchObject";
+export * from "./useRootContainer";
diff --git a/packages/solid-react/src/useLdoMethods.ts b/packages/solid-react/src/useLdoMethods.ts
new file mode 100644
index 0000000..fc95869
--- /dev/null
+++ b/packages/solid-react/src/useLdoMethods.ts
@@ -0,0 +1,84 @@
+import type { LdoBase, ShapeType } from "@ldo/ldo";
+import type { SubjectNode } from "@ldo/rdf-utils";
+import type {
+ Resource,
+ SolidLdoDataset,
+ SolidLdoTransactionDataset,
+} from "@ldo/solid";
+import { changeData, commitData } from "@ldo/solid";
+
+export interface UseLdoMethods {
+ dataset: SolidLdoDataset;
+ getResource: SolidLdoDataset["getResource"];
+ getSubject(
+ shapeType: ShapeType,
+ subject: string | SubjectNode,
+ ): Type;
+ createData(
+ shapeType: ShapeType,
+ subject: string | SubjectNode,
+ resource: Resource,
+ ...additionalResources: Resource[]
+ ): Type;
+ changeData(
+ input: Type,
+ resource: Resource,
+ ...additionalResources: Resource[]
+ ): Type;
+ commitData(
+ input: LdoBase,
+ ): ReturnType;
+}
+
+export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
+ return {
+ dataset: dataset,
+ /**
+ * Gets a resource
+ */
+ getResource: dataset.getResource.bind(dataset),
+ /**
+ * Returns a Linked Data Object for a subject
+ * @param shapeType The shape type for the data
+ * @param subject Subject Node
+ * @returns A Linked Data Object
+ */
+ getSubject(
+ shapeType: ShapeType,
+ subject: string | SubjectNode,
+ ): Type {
+ return dataset.usingType(shapeType).fromSubject(subject);
+ },
+ /**
+ * Begins tracking changes to eventually commit for a new subject
+ * @param shapeType The shape type that defines the created data
+ * @param subject The RDF subject for a Linked Data Object
+ * @param resources Any number of resources to which this data should be written
+ * @returns A Linked Data Object to modify and commit
+ */
+ createData(
+ shapeType: ShapeType,
+ subject: string | SubjectNode,
+ resource: Resource,
+ ...additionalResources: Resource[]
+ ): Type {
+ return dataset.createData(
+ shapeType,
+ subject,
+ resource,
+ ...additionalResources,
+ );
+ },
+ /**
+ * Begins tracking changes to eventually commit
+ * @param input A linked data object to track changes on
+ * @param resources
+ */
+ changeData: changeData,
+ /**
+ * Commits the transaction to the global dataset, syncing all subscribing
+ * components and Solid Pods
+ */
+ commitData: commitData,
+ };
+}
diff --git a/packages/solid-react/src/useMatchObject.ts b/packages/solid-react/src/useMatchObject.ts
new file mode 100644
index 0000000..c338646
--- /dev/null
+++ b/packages/solid-react/src/useMatchObject.ts
@@ -0,0 +1,21 @@
+import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo";
+import type { QuadMatch } from "@ldo/rdf-utils";
+import type { LdoBuilder } from "@ldo/ldo";
+import { useCallback } from "react";
+import { useTrackingProxy } from "./util/useTrackingProxy";
+
+export function useMatchObject(
+ shapeType: ShapeType,
+ subject?: QuadMatch[0] | string,
+ predicate?: QuadMatch[1] | string,
+ graph?: QuadMatch[3] | string,
+): LdSet {
+ const matchObject = useCallback(
+ (builder: LdoBuilder) => {
+ return builder.matchObject(subject, predicate, graph);
+ },
+ [subject, predicate, graph],
+ );
+
+ return useTrackingProxy(shapeType, matchObject);
+}
diff --git a/packages/solid-react/src/useMatchSubject.ts b/packages/solid-react/src/useMatchSubject.ts
new file mode 100644
index 0000000..494afc8
--- /dev/null
+++ b/packages/solid-react/src/useMatchSubject.ts
@@ -0,0 +1,21 @@
+import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo";
+import type { QuadMatch } from "@ldo/rdf-utils";
+import type { LdoBuilder } from "@ldo/ldo";
+import { useCallback } from "react";
+import { useTrackingProxy } from "./util/useTrackingProxy";
+
+export function useMatchSubject(
+ shapeType: ShapeType,
+ predicate?: QuadMatch[1] | string,
+ object?: QuadMatch[2] | string,
+ graph?: QuadMatch[3] | string,
+): LdSet {
+ const matchSubject = useCallback(
+ (builder: LdoBuilder) => {
+ return builder.matchSubject(predicate, object, graph);
+ },
+ [predicate, object, graph],
+ );
+
+ return useTrackingProxy(shapeType, matchSubject);
+}
diff --git a/packages/solid-react/src/useResource.ts b/packages/solid-react/src/useResource.ts
new file mode 100644
index 0000000..fb3fffb
--- /dev/null
+++ b/packages/solid-react/src/useResource.ts
@@ -0,0 +1,114 @@
+import { useMemo, useEffect, useRef, useState, useCallback } from "react";
+import type {
+ Container,
+ ContainerUri,
+ LeafUri,
+ Resource,
+ Leaf,
+} from "@ldo/solid";
+import { useLdo } from "./SolidLdoProvider";
+
+export interface UseResourceOptions {
+ suppressInitialRead?: boolean;
+ reloadOnMount?: boolean;
+ subscribe?: boolean;
+}
+
+export function useResource(
+ uri: ContainerUri,
+ options?: UseResourceOptions,
+): Container;
+export function useResource(uri: LeafUri, options?: UseResourceOptions): Leaf;
+export function useResource(
+ uri: string,
+ options?: UseResourceOptions,
+): Leaf | Container;
+export function useResource(
+ uri?: ContainerUri,
+ options?: UseResourceOptions,
+): Container | undefined;
+export function useResource(
+ uri?: LeafUri,
+ options?: UseResourceOptions,
+): Leaf | undefined;
+export function useResource(
+ uri?: string,
+ options?: UseResourceOptions,
+): Leaf | Container | undefined;
+export function useResource(
+ uri?: string,
+ options?: UseResourceOptions,
+): Leaf | Container | undefined {
+ const { getResource } = useLdo();
+ const subscriptionIdRef = useRef();
+
+ // Get the resource
+ const resource = useMemo(() => {
+ if (uri) {
+ const resource = getResource(uri);
+ // Run read operations if necissary
+ if (!options?.suppressInitialRead) {
+ if (options?.reloadOnMount) {
+ resource.read();
+ } else {
+ resource.readIfUnfetched();
+ }
+ }
+ return resource;
+ }
+ return undefined;
+ }, [getResource, uri]);
+ const [resourceRepresentation, setResourceRepresentation] =
+ useState(resource);
+ const pastResource = useRef<
+ { resource?: Resource; callback: () => void } | undefined
+ >();
+
+ useEffect(() => {
+ if (options?.subscribe) {
+ resource
+ ?.subscribeToNotifications()
+ .then((subscriptionId) => (subscriptionIdRef.current = subscriptionId));
+ } else if (subscriptionIdRef.current) {
+ resource?.unsubscribeFromNotifications(subscriptionIdRef.current);
+ }
+ return () => {
+ if (subscriptionIdRef.current)
+ resource?.unsubscribeFromNotifications(subscriptionIdRef.current);
+ };
+ }, [resource, options?.subscribe]);
+
+ // Callback function to force the react dom to reload.
+ const forceReload = useCallback(
+ // Wrap the resource in a proxy so it's techically a different object
+ () => {
+ if (resource) setResourceRepresentation(new Proxy(resource, {}));
+ },
+ [resource],
+ );
+
+ useEffect(() => {
+ // Remove listeners for the previous resource
+ if (pastResource.current?.resource) {
+ pastResource.current.resource.off(
+ "update",
+ pastResource.current.callback,
+ );
+ }
+ // Set a new past resource to the current resource
+ pastResource.current = { resource, callback: forceReload };
+ if (resource) {
+ // Add listener
+ resource.on("update", forceReload);
+ setResourceRepresentation(new Proxy(resource, {}));
+
+ // Unsubscribe on unmount
+ return () => {
+ resource.off("update", forceReload);
+ };
+ } else {
+ setResourceRepresentation(undefined);
+ }
+ }, [resource]);
+ return resourceRepresentation;
+}
diff --git a/packages/solid-react/src/useRootContainer.ts b/packages/solid-react/src/useRootContainer.ts
new file mode 100644
index 0000000..e32369a
--- /dev/null
+++ b/packages/solid-react/src/useRootContainer.ts
@@ -0,0 +1,31 @@
+import type { Container, ContainerUri } from "@ldo/solid";
+import { useEffect, useState } from "react";
+import type { UseResourceOptions } from "./useResource";
+import { useResource } from "./useResource";
+import { useLdo } from "./SolidLdoProvider";
+
+export function useRootContainerFor(
+ uri?: string,
+ options?: UseResourceOptions,
+): Container | undefined {
+ const { getResource } = useLdo();
+
+ const [rootContainerUri, setRootContainerUri] = useState<
+ ContainerUri | undefined
+ >(undefined);
+
+ useEffect(() => {
+ if (uri) {
+ const givenResource = getResource(uri);
+ givenResource.getRootContainer().then((result) => {
+ if (!result.isError) {
+ setRootContainerUri(result.uri);
+ }
+ });
+ } else {
+ setRootContainerUri(undefined);
+ }
+ }, [uri]);
+
+ return useResource(rootContainerUri, options);
+}
diff --git a/packages/solid-react/src/useSubject.ts b/packages/solid-react/src/useSubject.ts
new file mode 100644
index 0000000..c728c6f
--- /dev/null
+++ b/packages/solid-react/src/useSubject.ts
@@ -0,0 +1,30 @@
+import type { SubjectNode } from "@ldo/rdf-utils";
+import type { ShapeType } from "@ldo/ldo";
+import type { LdoBuilder } from "@ldo/ldo";
+import type { LdoBase } from "@ldo/ldo";
+import { useCallback } from "react";
+
+import { useTrackingProxy } from "./util/useTrackingProxy";
+
+export function useSubject(
+ shapeType: ShapeType,
+ subject: string | SubjectNode,
+): Type;
+export function useSubject(
+ shapeType: ShapeType,
+ subject?: string | SubjectNode,
+): Type | undefined;
+export function useSubject(
+ shapeType: ShapeType,
+ subject?: string | SubjectNode,
+): Type | undefined {
+ const fromSubject = useCallback(
+ (builder: LdoBuilder) => {
+ if (!subject) return;
+ return builder.fromSubject(subject);
+ },
+ [subject],
+ );
+
+ return useTrackingProxy(shapeType, fromSubject);
+}
diff --git a/packages/solid-react/src/useSubscribeToResource.ts b/packages/solid-react/src/useSubscribeToResource.ts
new file mode 100644
index 0000000..d6dc8b0
--- /dev/null
+++ b/packages/solid-react/src/useSubscribeToResource.ts
@@ -0,0 +1,52 @@
+import { useLdo } from "./SolidLdoProvider";
+import { useEffect, useRef } from "react";
+
+export function useSubscribeToResource(...uris: string[]): void {
+ const { dataset } = useLdo();
+ const currentlySubscribed = useRef>({});
+ useEffect(() => {
+ const resources = uris.map((uri) => dataset.getResource(uri));
+ const previousSubscriptions = { ...currentlySubscribed.current };
+ Promise.all(
+ resources.map(async (resource) => {
+ if (!previousSubscriptions[resource.uri]) {
+ // Prevent multiple triggers from created subscriptions while waiting
+ // for connection
+ currentlySubscribed.current[resource.uri] = "AWAITING";
+ // Read and subscribe
+ await resource.readIfUnfetched();
+ currentlySubscribed.current[resource.uri] =
+ await resource.subscribeToNotifications();
+ } else {
+ delete previousSubscriptions[resource.uri];
+ }
+ }),
+ ).then(async () => {
+ // Unsubscribe from all remaining previous subscriptions
+ await Promise.all(
+ Object.entries(previousSubscriptions).map(
+ async ([resourceUri, subscriptionId]) => {
+ // Unsubscribe
+ delete currentlySubscribed.current[resourceUri];
+ const resource = dataset.getResource(resourceUri);
+ await resource.unsubscribeFromNotifications(subscriptionId);
+ },
+ ),
+ );
+ });
+ }, [uris]);
+
+ // Cleanup Subscriptions
+ useEffect(() => {
+ return () => {
+ Promise.all(
+ Object.entries(currentlySubscribed.current).map(
+ async ([resourceUri, subscriptionId]) => {
+ const resource = dataset.getResource(resourceUri);
+ await resource.unsubscribeFromNotifications(subscriptionId);
+ },
+ ),
+ );
+ };
+ }, []);
+}
diff --git a/packages/solid-react/src/util/TrackingProxyContext.ts b/packages/solid-react/src/util/TrackingProxyContext.ts
new file mode 100644
index 0000000..3bbd84a
--- /dev/null
+++ b/packages/solid-react/src/util/TrackingProxyContext.ts
@@ -0,0 +1,61 @@
+import type {
+ ProxyContextOptions,
+ SubjectProxy,
+ SetProxy,
+} from "@ldo/jsonld-dataset-proxy";
+import { ProxyContext } from "@ldo/jsonld-dataset-proxy";
+import type { QuadMatch } from "@ldo/rdf-utils";
+import type { SubscribableDataset } from "@ldo/subscribable-dataset";
+import type { BlankNode, NamedNode, Quad } from "@rdfjs/types";
+import { createTrackingSubjectProxy } from "./TrackingSubjectProxy";
+import { createTrackingSetProxy } from "./TrackingSetProxy";
+
+/**
+ * @internal
+ * Options to be passed to the tracking proxy
+ */
+export interface TrackingProxyContextOptions extends ProxyContextOptions {
+ dataset: SubscribableDataset;
+}
+
+/**
+ * @internal
+ * This proxy exists to ensure react components rerender at the right time. It
+ * keeps track of every key accessed in a Linked Data Object and only when the
+ * dataset is updated with that key does it rerender the react component.
+ */
+export class TrackingProxyContext extends ProxyContext {
+ private listener: () => void;
+ private subscribableDataset: SubscribableDataset;
+
+ constructor(options: TrackingProxyContextOptions, listener: () => void) {
+ super(options);
+ this.subscribableDataset = options.dataset;
+ this.listener = listener;
+ }
+
+ // Adds the listener to the subscribable dataset while ensuring deduping of the listener
+ public addListener(eventName: QuadMatch) {
+ const listeners = this.subscribableDataset.listeners(eventName);
+ if (!listeners.includes(this.listener)) {
+ this.subscribableDataset.on(eventName, this.listener);
+ }
+ }
+
+ protected createNewSubjectProxy(node: NamedNode | BlankNode): SubjectProxy {
+ return createTrackingSubjectProxy(this, node);
+ }
+
+ protected createNewSetProxy(
+ quadMatch: QuadMatch,
+ isSubjectOriented?: boolean,
+ isLangStringSet?: boolean,
+ ): SetProxy {
+ return createTrackingSetProxy(
+ this,
+ quadMatch,
+ isSubjectOriented,
+ isLangStringSet,
+ );
+ }
+}
diff --git a/packages/solid-react/src/util/TrackingSetProxy.ts b/packages/solid-react/src/util/TrackingSetProxy.ts
new file mode 100644
index 0000000..9c34347
--- /dev/null
+++ b/packages/solid-react/src/util/TrackingSetProxy.ts
@@ -0,0 +1,56 @@
+import { createNewSetProxy, type SetProxy } from "@ldo/jsonld-dataset-proxy";
+import type { TrackingProxyContext } from "./TrackingProxyContext";
+import type { QuadMatch } from "@ldo/rdf-utils";
+
+export function createTrackingSetProxy(
+ proxyContext: TrackingProxyContext,
+ quadMatch: QuadMatch,
+ isSubjectOriented?: boolean,
+ isLangStringSet?: boolean,
+): SetProxy {
+ const baseSetProxy = createNewSetProxy(
+ quadMatch,
+ isSubjectOriented ?? false,
+ proxyContext,
+ isLangStringSet,
+ );
+
+ return new Proxy(baseSetProxy, {
+ get: (target: SetProxy, key: string | symbol, receiver) => {
+ if (trackingMethods.has(key)) {
+ proxyContext.addListener(quadMatch);
+ } else if (disallowedMethods.has(key)) {
+ console.warn(
+ "You've attempted to modify a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
+ );
+ }
+ return Reflect.get(target, key, receiver);
+ },
+ });
+}
+
+const trackingMethods = new Set([
+ "has",
+ "size",
+ "entries",
+ "keys",
+ "values",
+ Symbol.iterator,
+ "every",
+ "every",
+ "some",
+ "forEach",
+ "map",
+ "reduce",
+ "toArray",
+ "toJSON",
+ "difference",
+ "intersection",
+ "isDisjointFrom",
+ "isSubsetOf",
+ "isSupersetOf",
+ "symmetricDifference",
+ "union",
+]);
+
+const disallowedMethods = new Set(["add", "clear", "delete"]);
diff --git a/packages/solid-react/src/util/TrackingSubjectProxy.ts b/packages/solid-react/src/util/TrackingSubjectProxy.ts
new file mode 100644
index 0000000..54fa5fe
--- /dev/null
+++ b/packages/solid-react/src/util/TrackingSubjectProxy.ts
@@ -0,0 +1,43 @@
+import type { SubjectProxyTarget } from "@ldo/jsonld-dataset-proxy";
+import {
+ createSubjectHandler,
+ type SubjectProxy,
+} from "@ldo/jsonld-dataset-proxy";
+import type { BlankNode, NamedNode } from "@rdfjs/types";
+import type { TrackingProxyContext } from "./TrackingProxyContext";
+import { namedNode } from "@rdfjs/data-model";
+
+export function createTrackingSubjectProxy(
+ proxyContext: TrackingProxyContext,
+ node: NamedNode | BlankNode,
+): SubjectProxy {
+ const baseHandler = createSubjectHandler(proxyContext);
+ const oldGetFunction = baseHandler.get;
+ const newGetFunction: ProxyHandler["get"] = (
+ target: SubjectProxyTarget,
+ key: string | symbol,
+ receiver,
+ ) => {
+ const subject = target["@id"];
+ const rdfTypes = proxyContext.getRdfType(subject);
+ if (typeof key === "symbol") {
+ // Do Nothing
+ } else if (key === "@id") {
+ proxyContext.addListener([subject, null, null, null]);
+ } else if (!proxyContext.contextUtil.isSet(key, rdfTypes)) {
+ const predicate = namedNode(
+ proxyContext.contextUtil.keyToIri(key, rdfTypes),
+ );
+ proxyContext.addListener([subject, predicate, null, null]);
+ }
+ return oldGetFunction && oldGetFunction(target, key, receiver);
+ };
+ baseHandler.get = newGetFunction;
+ baseHandler.set = () => {
+ console.warn(
+ "You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
+ );
+ return true;
+ };
+ return new Proxy({ "@id": node }, baseHandler) as unknown as SubjectProxy;
+}
diff --git a/packages/solid-react/src/util/useTrackingProxy.ts b/packages/solid-react/src/util/useTrackingProxy.ts
new file mode 100644
index 0000000..05fa452
--- /dev/null
+++ b/packages/solid-react/src/util/useTrackingProxy.ts
@@ -0,0 +1,55 @@
+import {
+ ContextUtil,
+ JsonldDatasetProxyBuilder,
+} from "@ldo/jsonld-dataset-proxy";
+import { LdoBuilder } from "@ldo/ldo";
+import type { LdoBase, ShapeType } from "@ldo/ldo";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { TrackingProxyContext } from "./TrackingProxyContext";
+import { defaultGraph } from "@rdfjs/data-model";
+import { useLdo } from "../SolidLdoProvider";
+
+export function useTrackingProxy(
+ shapeType: ShapeType,
+ createLdo: (builder: LdoBuilder) => ReturnType,
+): ReturnType {
+ const { dataset } = useLdo();
+
+ const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
+ const forceUpdate = useCallback(
+ () => setForceUpdateCounter((val) => val + 1),
+ [],
+ );
+
+ // The main linked data object
+ const linkedDataObject = useMemo(() => {
+ // Remove all current subscriptions
+ dataset.removeListenerFromAllEvents(forceUpdate);
+
+ // Rebuild the LdoBuilder from scratch to inject TrackingProxyContext
+ const contextUtil = new ContextUtil(shapeType.context);
+ const proxyContext = new TrackingProxyContext(
+ {
+ dataset,
+ contextUtil,
+ writeGraphs: [defaultGraph()],
+ languageOrdering: ["none", "en", "other"],
+ },
+ forceUpdate,
+ );
+ const builder = new LdoBuilder(
+ new JsonldDatasetProxyBuilder(proxyContext),
+ shapeType,
+ );
+ return createLdo(builder);
+ }, [shapeType, dataset, forceUpdateCounter, forceUpdate, createLdo]);
+
+ useEffect(() => {
+ // Unregister force update listener upon unmount
+ return () => {
+ dataset.removeListenerFromAllEvents(forceUpdate);
+ };
+ }, [shapeType]);
+
+ return linkedDataObject;
+}
diff --git a/packages/solid-react/test/.ldo/post.context.ts b/packages/solid-react/test/.ldo/post.context.ts
new file mode 100644
index 0000000..5cb3a91
--- /dev/null
+++ b/packages/solid-react/test/.ldo/post.context.ts
@@ -0,0 +1,32 @@
+import { ContextDefinition } from "jsonld";
+
+/**
+ * =============================================================================
+ * postContext: JSONLD Context for post
+ * =============================================================================
+ */
+export const postContext: ContextDefinition = {
+ type: {
+ "@id": "@type",
+ },
+ SocialMediaPosting: "http://schema.org/SocialMediaPosting",
+ CreativeWork: "http://schema.org/CreativeWork",
+ Thing: "http://schema.org/Thing",
+ articleBody: {
+ "@id": "http://schema.org/articleBody",
+ "@type": "http://www.w3.org/2001/XMLSchema#string",
+ },
+ uploadDate: {
+ "@id": "http://schema.org/uploadDate",
+ "@type": "http://www.w3.org/2001/XMLSchema#date",
+ },
+ image: {
+ "@id": "http://schema.org/image",
+ "@type": "@id",
+ },
+ publisher: {
+ "@id": "http://schema.org/publisher",
+ "@type": "@id",
+ "@container": "@set",
+ },
+};
diff --git a/packages/solid-react/test/.ldo/post.schema.ts b/packages/solid-react/test/.ldo/post.schema.ts
new file mode 100644
index 0000000..39e8b63
--- /dev/null
+++ b/packages/solid-react/test/.ldo/post.schema.ts
@@ -0,0 +1,155 @@
+import { Schema } from "shexj";
+
+/**
+ * =============================================================================
+ * postSchema: ShexJ Schema for post
+ * =============================================================================
+ */
+export const postSchema: Schema = {
+ type: "Schema",
+ shapes: [
+ {
+ id: "https://example.com/PostSh",
+ type: "ShapeDecl",
+ shapeExpr: {
+ type: "Shape",
+ expression: {
+ type: "EachOf",
+ expressions: [
+ {
+ type: "TripleConstraint",
+ predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
+ valueExpr: {
+ type: "NodeConstraint",
+ values: [
+ "http://schema.org/SocialMediaPosting",
+ "http://schema.org/CreativeWork",
+ "http://schema.org/Thing",
+ ],
+ },
+ },
+ {
+ type: "TripleConstraint",
+ predicate: "http://schema.org/articleBody",
+ valueExpr: {
+ type: "NodeConstraint",
+ datatype: "http://www.w3.org/2001/XMLSchema#string",
+ },
+ min: 0,
+ max: 1,
+ annotations: [
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#label",
+ object: {
+ value: "articleBody",
+ },
+ },
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
+ object: {
+ value: "The actual body of the article. ",
+ },
+ },
+ ],
+ },
+ {
+ type: "TripleConstraint",
+ predicate: "http://schema.org/uploadDate",
+ valueExpr: {
+ type: "NodeConstraint",
+ datatype: "http://www.w3.org/2001/XMLSchema#date",
+ },
+ annotations: [
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#label",
+ object: {
+ value: "uploadDate",
+ },
+ },
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
+ object: {
+ value:
+ "Date when this media object was uploaded to this site.",
+ },
+ },
+ ],
+ },
+ {
+ type: "TripleConstraint",
+ predicate: "http://schema.org/image",
+ valueExpr: {
+ type: "NodeConstraint",
+ nodeKind: "iri",
+ },
+ min: 0,
+ max: 1,
+ annotations: [
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#label",
+ object: {
+ value: "image",
+ },
+ },
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
+ object: {
+ value:
+ "A media object that encodes this CreativeWork. This property is a synonym for encoding.",
+ },
+ },
+ ],
+ },
+ {
+ type: "TripleConstraint",
+ predicate: "http://schema.org/publisher",
+ valueExpr: {
+ type: "NodeConstraint",
+ nodeKind: "iri",
+ },
+ annotations: [
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#label",
+ object: {
+ value: "publisher",
+ },
+ },
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
+ object: {
+ value: "The publisher of the creative work.",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ annotations: [
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#label",
+ object: {
+ value: "SocialMediaPost",
+ },
+ },
+ {
+ type: "Annotation",
+ predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
+ object: {
+ value:
+ "A post to a social media platform, including blog posts, tweets, Facebook posts, etc.",
+ },
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/packages/solid-react/test/.ldo/post.shapeTypes.ts b/packages/solid-react/test/.ldo/post.shapeTypes.ts
new file mode 100644
index 0000000..4c50683
--- /dev/null
+++ b/packages/solid-react/test/.ldo/post.shapeTypes.ts
@@ -0,0 +1,19 @@
+import { ShapeType } from "@ldo/ldo";
+import { postSchema } from "./post.schema";
+import { postContext } from "./post.context";
+import { PostSh } from "./post.typings";
+
+/**
+ * =============================================================================
+ * LDO ShapeTypes post
+ * =============================================================================
+ */
+
+/**
+ * PostSh ShapeType
+ */
+export const PostShShapeType: ShapeType = {
+ schema: postSchema,
+ shape: "https://example.com/PostSh",
+ context: postContext,
+};
diff --git a/packages/solid-react/test/.ldo/post.typings.ts b/packages/solid-react/test/.ldo/post.typings.ts
new file mode 100644
index 0000000..bf9ac17
--- /dev/null
+++ b/packages/solid-react/test/.ldo/post.typings.ts
@@ -0,0 +1,45 @@
+import { LdSet, LdoJsonldContext } from "@ldo/ldo";
+
+/**
+ * =============================================================================
+ * Typescript Typings for post
+ * =============================================================================
+ */
+
+/**
+ * PostSh Type
+ */
+export interface PostSh {
+ "@id"?: string;
+ "@context"?: LdoJsonldContext;
+ type:
+ | {
+ "@id": "SocialMediaPosting";
+ }
+ | {
+ "@id": "CreativeWork";
+ }
+ | {
+ "@id": "Thing";
+ };
+ /**
+ * The actual body of the article.
+ */
+ articleBody?: string;
+ /**
+ * Date when this media object was uploaded to this site.
+ */
+ uploadDate: string;
+ /**
+ * A media object that encodes this CreativeWork. This property is a synonym for encoding.
+ */
+ image?: {
+ "@id": string;
+ };
+ /**
+ * The publisher of the creative work.
+ */
+ publisher: LdSet<{
+ "@id": string;
+ }>;
+}
diff --git a/packages/solid-react/test/Solid-Integration.test.tsx b/packages/solid-react/test/Solid-Integration.test.tsx
new file mode 100644
index 0000000..5dd3f1c
--- /dev/null
+++ b/packages/solid-react/test/Solid-Integration.test.tsx
@@ -0,0 +1,645 @@
+import React, { useCallback, useEffect, useState } from "react";
+import type { FunctionComponent } from "react";
+import { render, screen, fireEvent, act } from "@testing-library/react";
+import {
+ SAMPLE_BINARY_URI,
+ SAMPLE_DATA_URI,
+ SERVER_DOMAIN,
+ setUpServer,
+} from "./setUpServer";
+import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider";
+import { useResource } from "../src/useResource";
+import { useRootContainerFor } from "../src/useRootContainer";
+import { useLdo } from "../src/SolidLdoProvider";
+import { PostShShapeType } from "./.ldo/post.shapeTypes";
+import type { PostSh } from "./.ldo/post.typings";
+import { useSubject } from "../src/useSubject";
+import { useMatchSubject } from "../src/useMatchSubject";
+import { useMatchObject } from "../src/useMatchObject";
+import { useSubscribeToResource } from "../src/useSubscribeToResource";
+
+// Use an increased timeout, since the CSS server takes too much setup time.
+jest.setTimeout(40_000);
+
+describe("Integration Tests", () => {
+ setUpServer();
+
+ /**
+ * ===========================================================================
+ * useResource
+ * ===========================================================================
+ */
+ describe("useResource", () => {
+ it("Fetches a resource and indicates it is loading while doing so", async () => {
+ const UseResourceTest: FunctionComponent = () => {
+ const resource = useResource(SAMPLE_DATA_URI);
+ if (resource?.isLoading()) return
Loading
;
+ return
{resource.status.type}
;
+ };
+ render(
+
+
+ ,
+ );
+ await screen.findByText("Loading");
+ const resourceStatus = await screen.findByRole("status");
+ expect(resourceStatus.innerHTML).toBe("dataReadSuccess");
+ });
+
+ it("returns undefined when no uri is provided, then rerenders when one is", async () => {
+ const UseResourceUndefinedTest: FunctionComponent = () => {
+ const [uri, setUri] = useState(undefined);
+ const resource = useResource(uri, { suppressInitialRead: true });
+ if (!resource)
+ return (
+
;
+ };
+ render(
+
+
+ ,
+ );
+
+ const article = await screen.findByRole("value");
+ expect(article.innerHTML).toBe("undefined");
+ });
+
+ it("returns an id if an id key is provided", async () => {
+ const UseSubjectTest: FunctionComponent = () => {
+ const resource = useResource(SAMPLE_DATA_URI);
+ const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
+ if (resource.isLoading() || !post) return
loading
;
+
+ return
{post["@id"]}
;
+ };
+ render(
+
+
+ ,
+ );
+
+ const article = await screen.findByRole("value");
+ expect(article.innerHTML).toBe(`${SAMPLE_DATA_URI}#Post1`);
+ });
+
+ it("does not set a value if a value is attempted to be set", async () => {
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
+ const UseSubjectTest: FunctionComponent = () => {
+ const resource = useResource(SAMPLE_DATA_URI);
+ const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
+ if (resource.isLoading() || !post) return
loading
;
+
+ return (
+
+
{post.articleBody}
+
+
+ );
+ };
+ render(
+
+
+ ,
+ );
+
+ const article = await screen.findByRole("value");
+ expect(article.innerHTML).toBe(`test`);
+ fireEvent.click(screen.getByText("Attempt Change"));
+ expect(article.innerHTML).not.toBe("bad");
+ expect(warn).toHaveBeenCalledWith(
+ "You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
+ );
+ expect(warn).toHaveBeenCalledWith(
+ "You've attempted to modify a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
+ );
+ warn.mockReset();
+ });
+
+ it("rerenders when asked to subscribe to a resource", async () => {
+ const NotificationTest: FunctionComponent = () => {
+ const [isSubscribed, setIsSubscribed] = useState(true);
+ const resource = useResource(SAMPLE_DATA_URI, {
+ subscribe: isSubscribed,
+ });
+ const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
+
+ const addPublisher = useCallback(async () => {
+ await fetch(SAMPLE_DATA_URI, {
+ method: "PATCH",
+ body: `INSERT DATA { <${SAMPLE_DATA_URI}#Post1> . }`,
+ headers: {
+ "Content-Type": "application/sparql-update",
+ },
+ });
+ }, []);
+
+ if (resource.isLoading() || !post) return