Tutorial: Building a React front-end app with RDF Data, powered by Link and Solid

6/17/2021 - Joep Meindertma

EDIT: This tutorial is outdated. We no longer support the link-lib library. We recommend checking out Atomic Data and @tomic/react instead.

This tutorial will help you build a linked data front-end application, compatible with Tim Berners-Lee's Solid initiative. Knowledge of React and Typescript / Javascript is assumed, as is a little knowledge of RDF. For an introduction to RDF, read this.

Working with RDF (linked data) can be a bit different from using more conventional JSON REST APIs. We need tooling to deal with some of the complexities that arise when working with linked data:

  • The client (the browser app) does not know in advance what kind of data it will get back from a server. Link will simply fetch a URL using content negotiation, and get some RDF serialized response.
  • Deciding which view to render is often done by checking routes, but this is not possible in a linked data app. Instead, we can fetch the URL and check which rdf:type is being set (e.g. Person or Blogpost).
  • Many strings are URLs, which can be a bother to type and might cause typo-related-bugs. To deal with this, we set URLs programmatically using @ontologies/core.
  • Ordered data in RDF is a bit more complicated than JSON Arrays, so we'll use tooling to abstract these to arrays.

In this tutorial we're going to use react and the link-lib + link-redux (in short: Link) libraries to create a simple front-end app to render data from a Solid Pod. You can see the final result on CodeSandbox

Understanding Link: Store, Views and Topologies

The Link libraries have an interesting, but unconventional way of rendering data. The first concept to understand, is the LinkedRenderStore (the Store). The Store is responsible for most of the heavy lifting: it contains the stored data, keeps rendered data in sync, parses changes, and stores registered Views. Views are (in this case react) components that render one or more rdf:Classes in one or more Topologies. Topologies are contexts in the DOM, such as Page or Card or Detail.

Let's say you want to view your public Solid Pod folder, such as https://joep.inrupt.net/public/, as a full page. First, you pass the subject (the URL of the resource) to a Link Resource component. This will let the LRS check if that resource is already present in the Store. If not, it will be fetched. Once it has been fetched and parsed, the resource will be added to the Store. Then, the Resource component will be notified of new data, and then the Store will try to find the most suitable View. This is done by checking the rdf:type of the Resource and the current Topology. In this case, that type is an ldp:Container, and the topology is fullPage. If there is a fullPage View registered in the Store for ldp:Container, it will render that View. If there is not such a View, it might try a more generic view.

Setting up the basics

First, we'll need to set up a react project. You can use our boilerplate, or you can set it up manually.

Easy mode: Using the link-solid-boilerplate

You can get started right away using the link-solid-boilerplate on CodeSandbox. Alternatively, you can use the Template feature on Github to fork it, and follow the readme to get it running locally. Got it running? Skip the next section and go to Loading Data.

Hard mode: Without using the boilerplate

(skip this if you're using the boilerplate)

  1. Make sure node and npm are installed.
  2. Start a new react project: npx create-react-app my-app --template typescript.
  3. Add the type definitions: yarn add @types/node @types/react @types/react-dom @types/jest .
  4. Add link-lib and link-redux as dependencies: yarn add link-lib link-redux @rdfdev/delta @rdfdev/actions @rdfdev/iri @rdfdev/collections
  5. Add their peerDepdencies: yarn add @ontologies/as @ontologies/core @ontologies/schema @ontologies/shacl @ontologies/xsd http-status-codes n-quads-parser
  6. Set-up the Linked Render Store.
  7. Got stuck? Check the boilerplate for inspiration and use the link-redux wiki.
  8. Continue with the rest of this tutorial.

Loading data

As a data source, we could use multiple back-ends. Here's a list of tested and open source options:

  • A Solid pod hosted on Inrupt.net
  • A DexPod, hosted on dexpods.eu.
  • Atomic-Server, which also serializes data to RDF formats.
  • Any other public RDF server that supports n-triples and has permissive cors headers.

But for this tutorial, we'll stick with the Inrupt.net pod. We'll try to render https://joep.inrupt.net/public/, which is an ldp:Container. That Container, as its name suggests, contains a bunch of resources, similar to a folder.

Registering a View

Create the following file views/Container.tsx with a functional React component. All we need is a single property

import { Resource, useProperty } from "link-redux";
import React from 'react';
import ldp from '../../ontology/ldp'

const Container = () => {
  // This hook allows you to get any property / value combination from the resource.
  // The ldp.contains essentially contains a URL for the predicate.
  const contains = useProperty(ldp.contains);

  return (
    <>
      {contains.map(member => (
        // This renders a nested Resource.
        // Each subject will be fetcher by Link
        <Resource
          key={member.value}
          subject={member}
        />
      ))}
    </>
  );
};

// The Type attribute dictates for which RDF Classes
Container.type = ldp.Container;

export default Container;

Next, go to views/index.ts and add the newly created Container component to the getViews list:

const getViews = (): RegistrableComponent<any>[] => [
  // here!
  Container,
  ErrorResource,
  LoadingResource,
  // ....
]

This will make sure that when the app initializes, the Container component will be registered to the store. That's it! The component should now render. However, you won't see much. We also want to render the members from above. But these should not be rendered on a full page. They need a different topology.

So let's create one for the current context. We'll call this one topologies/BrowserList.ts, as it's a list in the file browser.

import { TopologyProvider } from 'link-redux'

import { appNS } from '../helpers/app'

// This is the same as `new NamedNode("myappurl/browserlist")`.
// Export this, you'll use it later!
export const browserListTopology = appNS('browserList');

// The link-redux TopologyProvider deals with most of the complexity
class BrowserList extends TopologyProvider {
  constructor(props: any) {
    super(props);

    // Create a NamedNode for your Topology, and bind it to this.topology
    this.topology = browserListTopology;
    // This determines the HTML element type (optional)
    this.elementType = 'ul';
    // The CSS Classname (optional)
    this.className = 'BrowserList';
  }
}

export default BrowserList;

Great! Now we can use this BrowserList topology in our Container component.

import { Resource, useProperty } from "link-redux";
import React from 'react';

import ldp from '../../ontology/ldp'
import BrowserList from '../../topologies/BrowserList'

const Container = () => {
  const contains = useProperty(ldp.contains);

  return (
    // We've wrapped our new Resources in our BrowserList topology
    <BrowserList>
      {contains.map(member => (
        <Resource
          key={member.value}
          subject={member}
        />
      ))}
    </BrowserList>
  );
};

// The Type attribute dictates for which RDF Classes
Container.type = ldp.Container;

export default Container;

Now we need to register a new View for our members and our BrowserList toplogy. We'll create a highly generic view, for all sorts of things. Let's call it ThingBrowserList. It's good practice to use the ConceptTopology naming convention.

import dcterms from '@ontologies/dcterms'
import { Literal, NamedNode } from "@ontologies/core";
import foaf from '@ontologies/foaf'
import rdfs from '@ontologies/rdfs'
import schema from '@ontologies/schema'
import { FC, Property, useProperty } from "link-redux";
import React from 'react';

import BrowserListItem from '../../components/BrowserListItem'
import { browserListTopology } from '../../topologies/BrowserList'
import { filename } from '@rdfdev/iri';

export interface Props {
  name: Literal;
}

// We can use multiple predicates.
// Link will select the first one that matches.
// This is especially useful in generic kinds of views, such as this one.
const namePredicates = [
  schema.name,
  rdfs.label,
  dcterms.title,
  foaf.name,
]

const ThingBrowserList: FC<Props> = ({
  subject,
}) => {
  const [name] = useProperty(namePredicates)
  const [modified] = useProperty(dcterms.modified);
  // When using a value from usePropety, you'll often want to use the internal raw string value using `.value`
  const displayName = name?.value || filename(subject as NamedNode);

  return (
    <BrowserListItem
      name={displayName}
      title={subject.value}
      to={subject}
    >
      {modified.value}
    </BrowserListItem>
  );
};

ThingBrowserList.type = rdfs.Resource;
// This is where we let this View know that it should be rendered in the BrowserList topology!
ThingBrowserList.topology = browserListTopology;

export default ThingBrowserList;

And for the final step, we'll register this View in views/index.ts like before:

const getViews = (): RegistrableComponent<any>[] => [
  Container,
  ThingBrowserList,
  // ....
]

Now, open the app and enter your public Solid Pod URL (e.g. https://joep.inrupt.net/public/).

Got stuck?