Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/iconshot/superbia-untrue

Integrate Superbia and Untrue.
https://github.com/iconshot/superbia-untrue

superbia untrue

Last synced: 2 months ago
JSON representation

Integrate Superbia and Untrue.

Awesome Lists containing this project

README

        

# @superbia/untrue

Integrate Superbia and Untrue.

## Installation

```
npm i @superbia/untrue
```

## Get started

We will use the Untrue's Context API to integrate Superbia and Untrue. Two contexts are needed:

- `DocumentContext`: It will store all the documents returned by the Superbia requests and subscriptions, e.g., users, posts, comments, etc.
- `RequestContext` It will store all the requests we do via a Superbia client.

#### What is a Document?

A document is an object that has an ID and a typename.

```json
{
"user": {
"_typename": "User",
"_id": "123",
"name": "Jhon Doe"
}
}
```

In this case the ID is found at `_id`. This may change from app to app according to how your data has been structured. Common keys for ID are: `id`, `ID`, `_id`.

`_typename`, on the other hand, is automatically added by Superbia in the server side.

## DocumentContext

`DocumentContext` will intercept any data handled by the `client` and it will group the documents based on their ID and typename.

If we receive some client data like:

```json
{
"user": {
"_typename": "User",
"_id": "123",
"name": "Jhon Doe",
"lastPost": {
"_typename": "Post",
"_id": "456",
"text": "Hello world"
}
}
}
```

... the `DocumentContext` data will be:

```js
this.documents = {
User: {
123: {
_typename: "User",
_id: "123",
name: "Jhon Doe",
lastPost: "456",
},
},
Post: {
456: {
_typename: "Post",
_id: "456",
text: "Hello world",
},
},
};
```

Notice how for `lastPost` we only store the `id`. We do this to have a single source of truth for every document.

### Creation

`AppDocumentContext.js`

```js
import { DocumentContext } from "@superbia/untrue";

import { client } from "./client";

class AppDocumentContext extends DocumentContext {
// we'll have the documents in this.documents

onFollow = (userId) => {
const user = this.documents.User[userId]; // get the user

// update user

user.following = true;
user.followersCount++;

this.update(); // notify the Wrapper of changes
};

onUnfollow = (userId) => {
const user = this.documents.User[userId]; // get the user

// update user

user.following = false;
user.followersCount--;

this.update(); // notify the Wrapper of changes
};
}

export default new AppDocumentContext(client, {
id: "_id",
typename: "_typename",
}); // the client and the keys of documents
```

### Usage

`User.js`

```js
import { Node, Wrapper } from "untrue";

import AppDocumentContext from "./AppDocumentContext";

function User({ userId, name, following, onFollow, onUnfollow }) {
const onFollowUser = () => onFollow(userId);
const onUnfollowUser = () => onUnfollow(userId);

return [
new Node("span", name),
following
? new Node("button", { onclick: onUnfollowUser }, "unfollow")
: new Node("button", { onclick: onFollowUser }, "follow"),
];
}

export default Wrapper.wrapContext(User, AppDocumentContext, (props) => {
const { userId } = props; // we receive the userId as a prop

const documents = AppDocumentContext.getDocuments(); // all the documents

const { name, following } = documents.User[userId]; // the desired user document

const { onFollow, onUnfollow } = AppDocumentContext; // context handlers

return { name, following, onFollow, onUnfollow }; // data the component needs
});
```

## RequestContext

`RequestContext` will keep the state of the requests.

Every `request` has 4 properties:

- `loading`: `Boolean`. `true` if the request is loading.
- `done`: `Boolean`. `true` if the request has been completed and it has no error.
- `data`: `Object`. Result of the request. `null` if the request hasn't been completed yet or an error was found.
- `error`: `Error` object or `null`. It's an `Error` object if the request has been completed but there's an error in the request itself or in any endpoint.

Let's say we have an endpoint `userPosts` that returns an array of `Post` documents:

```json
{
"userPosts": [
{ "_typename": "Post", "_id": "123", "text": "Hello world" },
{ "_typename": "Post", "_id": "456", "text": "Lorem ipsum" }
]
}
```

The `requests` inside `RequestContext` would be:

```js
this.requests = {
someUniqueRequestKey: {
loading: false,
done: true,
error: null,
data: { userPosts: ["123", "456"] },
},
// more requests
};
```

Just as in `DocumentContext`, we won't store the entire documents but their IDs only.

### Creation

`AppRequestContext.js`

```js
import { RequestContext } from "@superbia/untrue";

import { client } from "./client";

class AppRequestContext extends RequestContext {
onSomeHandler = () => {
// we can update this.requests data directly from here
};
}

export default new AppRequestContext(client, {
id: "_id",
typename: "_typename",
}); // the client and the keys of documents
```

### Usage

`PostList.js`

```js
import { Component, Node, Wrapper } from "untrue";

import { RequestWrapper } from "@superbia/untrue";

import AppRequestContext from "./AppRequestContext";

import Post from "./Post";

class PostList extends Component {
constructor(props) {
super(props);

this.on("mount", this.handleMountRequest); // request on mount
}

handleMountRequest = () => {
const { requestKey, onRequest } = this.props;

// it will be requested as:
// client.request({ posts: null })

onRequest(requestKey, { posts: null });
};

render() {
const { loading, done, error, postIds } = this.props;

if (loading) {
return new Node("span", "Loading...");
}

if (error !== null) {
return new Node("span", error.message);
}

if (done) {
return postIds.map((postId) => new Node(Post, { postId }));
}

return null;
}
}

// RequestWrapper will add the `requestKey` prop

export default RequestWrapper.wrapRequester(
Wrapper.wrapContext(PostList, AppRequestContext, (props) => {
const { requestKey } = props;

const requests = AppRequestContext.getRequests(); // all the requests

// the desired request, it's undefined until onRequest is fired

const {
loading = false,
done = false,
error = null,
data = null,
} = requests?.[requestKey] ?? {};

const postIds = data !== null ? data.posts : [];

const { onRequest } = AppRequestContext; // context handler

return { loading, done, error, postIds, onRequest }; // data the component needs
})
);
```

`Post.js`

```js
import { Node, Wrapper } from "untrue";

import AppDocumentContext from "./AppDocumentContext";

function Post({ text }) {
return new Node("div", text);
}

export default Wrapper.wrapContext(Post, AppDocumentContext, (props) => {
const { postId } = props;

const documents = AppDocumentContext.getDocuments();

const { text } = documents.Post[postId];

return { text };
});
```

### Intercepting requests

As we know, documents are stored in `DocumentContext` and requests are stored in `RequestContext`.

Let's say you want to update a document when a specific endpoint is requested.

You can `intercept` a request to do so. You need to override the `intercept` method to return an object of interceptors.

`AppDocumentContext.js`:

```js
class AppDocumentContext extends DocumentContext {
onLike = (postId) => {
const post = this.documents.Post[postId];

post.liked = true;
post.likesCount++;

this.update();
};
}
```

`AppRequestContext.js`:

```js
import AppDocumentContext from "./AppDocumentContext";

class AppRequestContext extends RequestContext {
intercept() {
return {
likePost: {
load: (requestKey, endpoints) => {
// every time `likePost` is requested, this closure will be executed

const { postId } = endpoints.likePost;

AppDocumentContext.onLike(postId);
},
},
};
}
}
```

Then in the component, we call the handler:

```js
const onLike = () => {
onRequest(null, { likePost: { postId } }); // postId comes from props
};
```

### Paginated requests

To implement paginated requests the server must return a Pagination object.

```json
{
"userPosts": {
"_typename": "PostPagination",
"nodes": [
{ "_typename": "Post", "_id": "123", "text": "Hello world" },
{ "_typename": "Post", "_id": "456", "text": "Lorem ipsum" }
],
"hasNextPage": true,
"nextPageCursor": "789"
}
}
```

`RequestContext` will manage to store the data like:

```js
this.requests = {
someRequestKey: {
loading: false,
done: true,
error: null,
data: {
userPosts: {
loading: false,
error: null,
data: {
nodes: ["123", "456"],
hasNextPage: true,
nextPageCursor: "789",
},
},
},
},
};
```

Notice how we have two set of properties for `loading`, `error` and `data`. The first set will belong to the `onRequest` call while the second one will belong to the `onLoad` calls.

The rules of documents will be applied here, so every `node` will be stored as an ID only.

#### Usage

We will need two different components: one for the initial `onRequest` call and another one for the subsequent `onLoad` calls.

`PaginatedPostList.js`

```js
import { Component, Node, Wrapper } from "untrue";

import { RequestWrapper } from "@superbia/untrue";

import AppRequestContext from "./AppRequestContext";

import Content from "./Content";

class PaginatedPostList extends Component {
constructor(props) {
super(props);

this.on("mount", this.handleMountRequest); // request on mount
}

handleMountRequest = () => {
const { requestKey, onRequest } = this.props;

onRequest(requestKey, { posts: { limit: 2 } });
};

render() {
const { requestKey, loading, done, error } = this.props;

if (loading) {
return new Node("span", "Loading...");
}

if (error !== null) {
return new Node("span", error.message);
}

if (done) {
return new Node(Content, { requestKey }); // pass requestKey as prop
}

return null;
}
}

export default RequestWrapper.wrapRequester(
Wrapper.wrapContext(PaginatedPostList, AppRequestContext, (props) => {
const { requestKey } = props;

const requests = AppRequestContext.getRequests(); // all the requests

const {
loading = false,
done = false,
error = null,
} = requests?.[requestKey] ?? {}; // the desire request

const { onRequest } = AppRequestContext; // context handler

return { loading, done, error, onRequest }; // data the component needs
})
);
```

`Content.js`

```js
import { Node, Wrapper } from "untrue";

import AppRequestContext from "./AppRequestContext";

import Post from "./Post";

function Content({
requestKey,
loading,
error,
postIds,
hasNextPage,
nextPageCursor,
onLoad,
}) {
const onLoadNext = () => {
onLoad(requestKey, { posts: { limit: 2, cursor: nextPageCursor } });
};

return [
postIds.map((postId) => new Node(Post, { postId })),
hasNextPage
? new Node("button", { onclick: onLoadNext }, "load next page")
: null,
loading ? new Node("span", "Loading next page...") : null,
error !== null ? new Node("span", error.message) : null,
];
}

// here we don't need RequestWrapper because we receive the "requestKey" as a prop already

export default Wrapper.wrapContext(Content, AppRequestContext, (props) => {
const { requestKey } = props;

const requests = AppRequestContext.getRequests(); // all the requests

const {
loading,
error,
data: {
nodes: postIds, // renaming for convenience
hasNextPage,
nextPageCursor,
},
} = requests[requestKey].data.posts; // the desired request's data

const { onLoad } = AppRequestContext; // context handler

return { loading, error, postIds, hasNextPage, nextPageCursor, onLoad }; // data the component needs
});
```

## RequestWrapper

`RequestWrapper` exposes a method `wrapRequester`. This method will add a `requestKey` prop to the component.

```js
import { RequestWrapper } from "@superbia/untrue";

function Child({ requestKey }) {
// ...
}

const UsernameRequester = RequestWrapper.wrapRequester(Child, (props) => {
const { username } = props;

return username;
}); // requestKey will be the username prop

const ValueRequester = RequestWrapper.wrapRequester(Child, "profile"); // requestKey will be "profile"

const UniqueRequester = RequestWrapper.wrapRequester(Child); // requestKey will be a unique id
```