Skip to main content

Storage

Until now, our app has been creating nodes and connections between those nodes in a graph, but after every page refresh our data is cleared.

Luckily, shapes help with storing, loading and validating data. So now that we are using shapes, we are ready to make the data in our graph persistent.

LINCD allows you to finely tune exactly which nodes are stored where. The default configuration of create-app comes with a configuration to automatically store all saved nodes in a json file.

See frontend/src/App.tsx:

//store all quads in a file on the backend named 'main'
let store = new FrontendFileStore('main');
Storage.setDefaultStore(store);

This sets the default store to a FrontendFileStore, which automatically syncs with a JSONLDFileStore that stores the data as a .json file on the local file system of the backend.

All we need to do to put that to work is to save() the nodes we want to store.

In this case, that's every person that is created.

So go to createNewPerson and after

newPerson.setValue(hasName, newName);

add:

newPerson.save();

Now, after you create a new person in the app, you will see the data of this person show up in /data/filestores/main.json

For example, it may look like this:

{
"@context": {
"foaf": "http://xmlns.com/foaf/0.1/",
"this": "http://localhost:4000/data/node-file-store/main/"
},
"@graph": [
{
"@id": "this:0",
"@type": "foaf:Person",
"foaf:name": "Rene"
}
]
}

This is using the JSON-LD format. Which is the recommended way to store Linked Data as json. It comes with some built in shortcuts, like @id for the URI of the node and @type for it's rdf:type. See the full spec here

Great! Our data is being stored.

However, it's not being loaded back into memory upon a page refresh.

To do that, we need to supply the current list of stored Person instances to the persons state when the component is first created.

React provides the useEffect hook for such use cases.

First, import the default store from App.tsx:

import {store} from '../App';

Then, within the Home component, after the creation of the different states, but before the return handle, add the following:

useEffect(() => {
store.init().then(() => {
setPersons([...Person.getLocalInstances()]);
});
}, []);

Also make sure to import useEffect from react:

import {useEffect} from 'react';

React refresher: useEffect allows you to execute code after each render, and in this case, with an empty array of dependences ([]) passed at the end, it will only run once, after the first render.

This code waits until the default store of our app is initialized. In the case of a FrontendFileStore, that means it has loaded all contents in the file back into the local graph.

Then, Person.getLocalInstances() gets all the nodes who have rdf:type foaf:Person and returns a Set of Person instances for those nodes.

In this case we've set up the persons state as an array, so this converts the set to an array, and then update the persons state.

Now when you reload the page you should see the same names appearing on the screen.

Hooray!

This concludes this tutorial.

You should now have a good first grasp of the fundamentals of Linked Data like NamedNodes and Ontologies and you know how to work with these in LINCD. This allows you to work right at the data level. And you've also learned how to work with shapes which makes the code a lot cleaner as they handle the data models for you.

With this, you now know the basics of how to start building Linked Data based applications with LINCD.

Congrats!


Final code

import {useEffect,useState} from 'react';
import {loadData} from 'lincd-foaf/lib/ontologies/foaf';
import {Person} from 'lincd-foaf/lib/shapes/Person';
import {store} from '../App';
loadData();

export default function Home() {
let [newName, setNewName] = useState<string>('');
let [persons, setPersons] = useState<Person[]>([]);
let [person1, setPerson1] = useState<Person>(null);
let [person2, setPerson2] = useState<Person>(null);
let [bool, setBool] = useState(false);
let forceUpdate = () => setBool(!bool);

useEffect(() => {
store.init().then(() => {
setPersons([...Person.getLocalInstances()]);
});
}, []);

let createNewPerson = function (e) {
//create a new named node for the new person
let newPerson = new Person();

//create a new edge from this new person to its own name, using the foaf.name property as the connecting edge
//to store a string in the graph, we use a Literal node.
newPerson.name = newName;

//save this node to permanent storage
newPerson.save();

//add the person to the array and update the state
setPersons(persons.concat(newPerson));
};

let onSubmitConnection = function (e) {
if (person1 && person2) {
person1.addKnows(person2);
forceUpdate();
}
};
return (
<div>
<h1>Persons</h1>
<form>
<input value={newName} onChange={(e) => setNewName(e.currentTarget.value)} type="text" placeholder="name" />
<input type="button" value="Add new person" onClick={createNewPerson} />
</form>
<hr />
<ul>
{persons.map((person) => {
return (
<li key={person.namedNode.uri}>
{person.name} has {person.knows.size} friends:{' '}
{person.knows.map((friend: Person) => friend.name).join(', ')}{' '}
</li>
);
})}
</ul>

{persons.length > 1 && (
<form>
<SelectPerson persons={persons} setPerson={setPerson1} value={person1}></SelectPerson>
&nbsp;has friend&nbsp;
<SelectPerson persons={persons} setPerson={setPerson2} value={person2}></SelectPerson>
<input type="button" value="Make connection" onClick={onSubmitConnection} />
</form>
)}
</div>
);
}

interface SelectPersonProps {
persons: Person[];
setPerson: (person: Person) => void;
value: Person;
}

const SelectPerson = function ({persons, setPerson, value}: SelectPersonProps) {
return (
<select value={value?.namedNode.uri} onChange={(v) => setPerson(Person.getFromURI(v.currentTarget.value))}>
<option value={null}>---</option>
{persons.map((person) => (
<option value={person.namedNode.uri} key={person.namedNode.uri}>
{person.name}
</option>
))}
</select>
);
};