@nokkio/magic
All your data, none of the hassle.
When you define a schema using Nokkio's data abstraction, a variety of exports are added to the @nokkio/magic module in your project that allow you to easily access your data.
@nokkio/magic generates all the code you need to access your data, you don't have to write any server-side endpoints or fetch calls.
The code that is generated by @nokkio/magic is dependent on your project's schema. For the purposes of this documentation, we'll assume your project has the following schema.js file:
const List = defineModel('List', {
title: types.string(),
});
const Todo = defineModel('Todo', {
label: types.string(),
isComplete: types.bool(false),
});
const Tag = defineModel('Tag', {
name: types.string().max(50),
})
List.hasMany(Todo);
Todo.belongsToMany(Tag);
Collections
The primary method for fetching data in your Nokkio applications is hooks. Given our above example, @nokkio/magic will generate hooks that return collections or single records from your data.
Hooks that access a collection of items returns results in an array. For example, the useLists hook in our example can be used like this:
import { useLists } from '@nokkio/magic';
export default function App() {
const lists = useLists();
return (
<ul>
{lists.map((list) => (
<li>{list.name}</li>
))}
</ul>
);
}
Sorting and limiting
By default, the items are returned in ascending order of when they were created, and are limited to 100 results. You can change that via the limit and sort parameters. In this case, we are prefixing the sort field with a dash to signal that we want the results in descending order.
const lists = useLists({
limit: 10,
sort: '-createdAt',
});
Filtering
Similarly, we can use filter to limit the results based on specific values. For example, to get any Todo in our project that is incomplete:
const incompleteTodos = useTodos({
filter: {
isComplete: false,
},
});
Pagination
Collections contain convenience methods to easily manage pagination.
import { useLists } from '@nokkio/magic';
export default function App() {
const lists = useLists();
return (
<div>
<ul>
{lists.map((list) => (
<li>{list.name}</li>
))}
</ul>
<button
disabled={lists.hasPrev() === false}
onClick={() => lists.prev()}
>
Previous
</button>
<button
disabled={lists.hasNext() === false}
onClick={() => lists.next()}
>
Next
</button>
</div>
);
}
When the user clicks the either button, Nokkio will fetch the next or previous page or results and re-render your component.
Single entries
You can also get a single record by its identifier. This is commonly paired with Nokkio's router to get a single item based on the URL. For example, if our app has a /lists/[id].js file, we can use useList to get an instance of the List model.
import { useList } from '@nokkio/magic';
export default function ListPage({ id }) {
const list = useList(id);
return (
<div>
<h2>{list.name}</h2>
</div>
);
}
Relationships
If you know you'll be accessing related data, you can instruct Nokkio to eagerly load either the full records or counts of the relationahip efficiently in one request.
hasMany / belongsToMany
For example, if we wanted to return a list of Todo records for each list:
import { useLists } from '@nokkio/magic';
export default function App() {
const lists = useLists({
with: ['todos'],
});
return (
<div>
{lists.map((list) => (
<div>
<h2>{list.name}</h2>
<ul>
{list.todos.map((todo) => (
<li>{todo.label}</li>
))}
</ul>
</div>
))}
</div>
);
}
Note that list.todos is a full collection, and can be paginated just like any other collection.
Advanced hasMany
You can also apply filtering, sorting, and limits to the relational data returned. For example, to only load the most recent 5 incomplete todos for each list:
const lists = useLists({
with: {
todos: {
sort: '-createdAt',
limit: 5,
filter: {
isComplete: false,
},
},
},
});
belongsTo
with also works for records that belong to a parent record:
import { useTodos } from '@nokkio/magic';
export default function App() {
const todos = useTodos({
with: ['list'],
});
return (
<ul>
{todos.map((todo) => (
<li>
{todo.label} (from: {todo.list.name})
</li>
))}
</ul>
);
}
Counts
If you only need to display how many relational records exist, use withCounts:
function App() {
const lists = useLists({
withCounts: ['todos'],
});
return <div>
<ul>
{lists.map(list => (
<li>{list.name} ({list.todosCount} todos)</li>
))}
</ul>
</div>;
}
Advanced counts
You can generate multiple counts and/or counts with conditions. The following result will have a incompleteTodosCount and completedTodosCount field for each list.
const lists = useLists({
withCounts: {
todos: {
incompleteTodosCount: {
isComplete: false,
},
completedTodosCount: {
isComplete: true,
},
},
},
});
Live subscriptions
If you have multiple users accessing the same data at once, you may want to broadcast data changes out to all clients at once. With Nokkio, that's as easy as adding the live property to the magic hook:
import { useLists } from '@nokkio/magic';
export default function App() {
const lists = useLists({
withCounts: ['todos'],
live: true,
});
return (
<div>
<ul>
{lists.map((list) => (
<li>
{list.name} ({list.todosCount} todos)
</li>
))}
</ul>
</div>
);
}
All clients connected to the same page will now receive data updates in real time when one of the other clients updates any associated data.
Loading states
Nokkio's data hooks are ready-made for Suspense. To show loading states, move your data logic to its own component then wrap it in Suspense:
import { Suspense } from 'react';
import { useLists } from '@nokkio/magic';
function Lists() {
const lists = useLists();
return (
<ul>
{lists.map((list) => (
<li>
{list.name} ({list.todosCount} todos)
</li>
))}
</ul>
);
}
export default function App() {
return (
<div>
<h1>Lists</h1>
<Suspense fallback={<p>Loading...</p>}>
<Lists />
</Suspense>
</div>
);
}
Using models directly
So far, we've discussed abstractions like useLists and useForm that paper over the complexity of loading and updating data in your applications. However, there may be times when you want to access the data at a lower level. You can do so by accessing the models directly, just like the above abstractions do internally. For example, you can import the List model like so:
import { List } from '@nokkio/magic';
Let's explore the methods available when using Nokkio models directly.
find()
To fetch a collection of items, you can use the find method:
import { List, Todo } from '@nokkio/magic';
// Fetch the latest 10 lists, along with their todos.
const latestLists = await List.find({
sort: '-created',
limit: 10,
with: ['todos'],
});
// Fetch the latest completed todos.
const latestCompletedTodos = await Todo.find({
filter: { isComplete: true },
sort: '-updated',
limit: 10,
});
findById()
To fetch a item by its ID, you can use the findById method:
import { List } from '@nokkio/magic';
// Fetch the list with ID = abc123, along with its todos.
const list = await List.findById('abc123', {
with: ['todos'],
});
create()
To create a new item, use the create method:
import { List } from '@nokkio/magic';
// Create a new list, returns an object containing the
// new list's ID.
const { lastInsertId } = await List.create({
name: 'My new list',
});
createRelationship()
Nokkio also generates dynamic methods for hasMany relationships that allow you to easily create child records. In our example, any instance of List will have a createTodo method:
import { List } from '@nokkio/magic';
// First, fetch a list
const list = await List.findById('abc123');
// create a new Todo for that list
await list.createTodo({ text: 'Buy milk' });
update()
To update an instance of an item, call update:
import { Todo } from '@nokkio/magic';
// First, fetch a todo
const todo = await Todo.findById('abc123');
// Update the todo to set it as completed
await todo.update({ isComplete: true });
delete()
To delete an instance of an item, call delete:
import { Todo } from '@nokkio/magic';
// First, fetch a todo
const todo = await Todo.findById('abc123');
// Delete it
await todo.delete();
attach() / detach()
When belongsToMany relationships are used, you can attach a record to the other side of the relationship using attach:
import { Todo, Tag } from '@nokkio/magic';
// First, fetch a todo
const todo = await Todo.findById('abc123');
// Then, a tag
const tag = await Tag.findById('abc123');
// Attach the tag to the todo:
await tag.attach(todo);
// Works in either direction:
await todo.attach(tag);
To remove the connection, use detach:
await todo.detach(tag);