Skip to content

Commit

Permalink
#863 Add documentation for @tomic/react
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Mar 26, 2024
1 parent 099ce92 commit 10d53c7
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 7 deletions.
4 changes: 4 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
- [Resource](js-lib/resource.md)
- [Collection](js-lib/collection.md)
- [@tomic/react](usecases/react.md)
- [useStore](react/useStore.md)
- [useResource](react/useResource.md)
- [useValue & friends](react/useValue.md)
- [useCollection](react/useCollection.md)
- [@tomic/svelte](svelte.md)
- [JS CLI](js-cli.md)
- [Rust](rust-lib.md)
Expand Down
Empty file added docs/src/react/useCollection.md
Empty file.
121 changes: 121 additions & 0 deletions docs/src/react/useResource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# useResource

`useResource` is the primary way to fetch data with Atomic React.
It returns a [Resource](../js-lib/resource.md) object, that is still loading when initially returned.
When the data is loaded, the component will re-render with the new data allowing you to show some UI before the content is available, resulting in a more responsive user experience.

The hook also subscribes to changes meaning that the component will update whenever the data changes clientside and **even serverside**.
You essentially get real-time features for free!

```jsx
import { useResource } from '@tomic/react';

export const Component = () => {
const resource = useResource('https://my-atomic-server/my-resource');

// Optionally show a loading state
if (resource.loading) {
return <Loader />
}

return (
<p>{resource.title}</p>
)
}
```

## Typescript

Just like the [`store.getResource`](../js-lib/resource.md#typescript) method, `useResource` can be annotated with a subject of a certain class.

```typescript
import { useResource } from '@tomic/react';
import type { Author } from './ontologies/blogsite' // <-- Generated with @tomic/cli
// ...
const resource = useResource<Author>('https://my-atomic-server/moderndayshakespear69')
const age = Date.now() - resource.props.yearOfBirth
```

## Reference

### Parameters

- **subject**: `string` - The subject of the resource you want to fetch.
- **options**: `FetchOpts` - (Optional) Options for how the store should fetch the resource.

**FetchOpts**:

| Name | Type | Description |
| --- | --- | --- |
| allowIncomplete | `boolean` | ? |
| noWebSocket | `boolean` | (Optional) If true, uses HTTP to fetch resources instead of websockets |
| newResource | `Resource` | (Optional) If true, will not send a request to a server, it will simply create a new local resource.|

### Returns

[Resource](../js-lib/resource.md) - The fetched resource (might still be in a loading state).

## Views

A common way to build interfaces with Atomic React is to make a *view* component.
Views are a concept where the component is responsible for rendering a resource in a certain way to fit in the context of the view type.

The view selects a component based on the resource's class or renders a default view if there is no component for that class.
In this example, we have a `ResourceInline` view that renders a resource inline in some text.
For most resources, it will just render the name but for a Person or Product, it will render a special component.

```jsx
// views/inline/ResourceInline.tsx

import { useResource } from '@tomic/react';
import { shop } from '../../ontologies/shop'; // <-- Generated with @tomic/cli
import { PersonInline } from './PersonInline';
import { ProductInline } from './ProductInline';

interface ResourceInlineProps {
subject: string;
}

export interface ResourceInlineViewProps<T> {
resource: Resource<T>;
}

export const ResourceInline = ({ subject }: ResourceInlineProps): JSX.Element => {
const resource = useResource(subject);

const Comp = resource.matchClass({
[shop.classes.product]: ProductInline,
[shop.classes.person]: PersonInline,
}, Default);

return <Comp subject={subject} />
}

const Default = ({ subject }: ResourceInlineViewProps<unknown>) => {
const resource = useResource(subject);

return <span>{resource.title}</span>
}
```

The `PersonInline` view will render a person resource inline.
It could render a mention-like thing with the person's name, their profile picture and a link to their profile for example.

```jsx
// views/inline/PersonInline.tsx
import { useResource, Resource, type Server } from '@tomic/react';
import type { Person } from '../../ontologies/social' // <-- Generated with @tomic/cli
import type { ResourceInlineViewProps } from './ResourceInline';

export const PersonInline = ({ resource }: ResourceInlineViewProps<Person>) => {
const image = useResource<Server.File>(resource.props.image);

return (
<span className="person-inline">
<img src={image.props.downloadUrl} className="profile-image-inline" />
<span>{resource.title}</span>
</span>
)
}
```
47 changes: 47 additions & 0 deletions docs/src/react/useStore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# useStore

You can use `useStore` when you need direct access to the store in your components.

For example, on a login screen, you might want to set the agent on the store by calling `store.setAgent` after the user has entered their agent secret.

```jsx
import { Agent, useStore } from '@tomic/react';

export const Login = () => {
const store = useStore();
const [agentSecret, setAgentSecret] = useState('');

const login = () => {
try {
const newAgent = Agent.fromSecret(agentSecret);
store.setAgent(newAgent);
} catch(e) {
console.error(e);
// Show error.
}
};

return (
<label>
Agent Secret
<input
type="password"
placeholder="My agent secret"
value={agentSecret}
onChange={e => setAgentSecret(e.target.value)}
/>
</label>
<button onClick={login}>Login</button>
);
};
```

## Reference

### Paramaters

None.

### Returns

[Store](../js-lib/store.md) - The store object.
147 changes: 147 additions & 0 deletions docs/src/react/useValue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# useValue

The `useValue` hook is used to read and write values from a resource.
It looks and functions a lot like React's useState hook.

```jsx
import { useValue } from '@tomic/react';

const MyComponent = ({ subject }) => {
const resource = useResource(subject);
const [value, setValue] = useValue(resource, 'https://example.com/property');

return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
</div>
);
};
```

The `useValue` hook does not save the resource by default when the value is changed.
This can be configured by passing an options object as the third argument with `commit` set to true.

In practice, you will use typed versions of `useValue` more often.
These offer better typescript typing and validation on writes.

The following value hooks are available:

- `useString` for string, slug and markdown values.
- `useNumber` for float and integer values.
- `useBoolean` for boolean values.
- `useDate` for date and timestamp values.
- `useArray` for ResourceArray values.

## Reference

### Parameters

- **resource**: `Resource` - The resource object to read and write from.
- **property**: `string` - The subject of the property you want to read and write.
- **options**: `object` - (Optional) Options for how the value should be read and written.

**Options**:
| Name | Type | Description |
| --- | --- | --- |
| commit | `boolean` | If true, the resource will be saved when the value is changed. Default: `false` |
| validate | `boolean` | If true, the value will be validated against the properties datatype. Default: `true`|
| commitDebounce | `number` | The number of milliseconds to wait before saving the resource. Default: `100`|
| handleValidationError | `function` | A function that is called when the value is invalid. |

### Returns

Returns an array (tuple) with two items:
- **value**: type depends on the hook used - The value of the property.
- **setValue**: `function` - A function to set the value of the property.

## Some Examples

### Realtime Todo app

In this example, we create a basic to-do app that persists on the server and updates in real-time when anyone makes changes.
If you were to make this in vanilla react without any kind of persistence it would probably look almost the same.
The main difference is the use of the `useArray` and `useBoolean` hooks instead of `useState`.

```jsx
import { useArray, useBoolean, useResource } from '@tomic/react';
import { useState } from 'react';

export const TodoList = () => {
const store = useStore();
const checklist = useResource<Checklist>('https://my-server/checklist/1');

const [todos, setTodos] = useArray(checklist, todoApp.properties.todos, {
commit: true,
});

const [inputValue, setInputValue] = useState('');

const removeTodo = (subject: string) => {
setTodos(todos.filter(todo => todo !== subject));
};

const addTodo = async () => {
const newTodo = await store.newResource({
isA: todoApp.classes.todoItem,
parent: checklist.subject,
propVals: {
[core.properties.name]: inputValue,
[todoApp.properties.done]: false,
},
});

await newTodo.save();

setTodos([...todos, newTodo.subject]);
setInputValue('');
};

return (
<div>
<ul>
{todos.map(subject => (
<li key={subject}>
<Todo subject={subject} onDelete={removeTodo} />
</li>
))}
</ul>
<input
type='text'
placeholder='Add a new todo...'
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
</div>
);
};

interface TodoProps {
subject: string;
onDelete: (subject: string) => void;
}

const Todo = ({ subject, onDelete }: TodoProps) => {
const resource = useResource<Todo>(subject);
const [done, setDone] = useBoolean(resource, todoApp.properties.done, {
commit: true,
});

const deleteTodo = () => {
resource.destroy();
onDelete(subject);
};

return (
<span>
<input
type='checkbox'
checked={done}
onChange={e => setDone(e.target.checked)}
/>
{resource.title}
<button onClick={deleteTodo}>Delete</button>
</span>
);
};
```
Loading

0 comments on commit 10d53c7

Please sign in to comment.