npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@superbia/untrue

v2.1.2

Published

Integrate Superbia and Untrue.

Downloads

227

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.

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