1.2.0 • Published 3 months ago

@superbia/untrue v1.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
3 months ago

@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.

{
  "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:

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

... the DocumentContext data will be:

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

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

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:

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

The requests inside RequestContext would be:

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

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

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

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:

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

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

    this.update();
  };
}

AppRequestContext.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:

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

Paginated requests

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

{
  "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:

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

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

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.

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
1.2.0

3 months ago

1.1.8

3 months ago

1.1.7

8 months ago

1.1.6

10 months ago

1.1.5

10 months ago

1.1.4

10 months ago