tutorial // Feb 04, 2022

How to Fetch and Render Data in Joystick Components

Adding the data option to Joystick components to fetch data on the server and render it in components on the server and the client.

How to Fetch and Render Data in Joystick Components

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you run this, we need to install one additional dependency, node-fetch:

Terminal

cd app && npm i node-fetch

Once this is installed, from the same app directory you just cd'd into, you can start up the app:

Terminal

joystick start

After this, your app should be running and we're ready to get started.

Wiring up an API endpoint using getters

The first thing we need to do is get access to some data that we'll render in our component. While we could just render some static (or hard-coded) data, it'd be better to pull some data from a third-party API so we can see the power and flexibility of this technique.

/api/index.js

import fetch from 'node-fetch';
import { URL, URLSearchParams } from 'url';

export default {
  getters: {
    posts: {
      get: (input = {}) => {
        const url = new URL('https://jsonplaceholder.typicode.com/posts');

        if (input?.id) {
          const searchParams = new URLSearchParams(input);
          url.search = searchParams.toString();
        }

        return fetch(url).then((response) => response.json());
      },
    },
  },
  setters: {},
};

In a Joystick application, "getters" allow us to define API endpoints for "getting" data. Behind the scenes, getters are turned into plain HTTP REST API endpoints in your app (e.g., http://localhost:2600/api/_getters/posts).

Above, we're defining a new getter called posts which will get a list of posts from the JSON Placeholder API—a free REST API that provides test data for testing and prototyping.

Getters are one of two types of API endpoints in a Joystick app with the other being setters (these "set" data in our application—the "create, update, and delete" part of CRUD). In a Joystick app, getters and setters are defined together on a single object exported from the /api/index.js file we see above (referred to as your API's "schema" in Joystick).

This object is then imported into /index.server.js and passed as part of the options to the node.app() function—as api—from the @joystick.js/node package. This tells Joystick to automatically load all of the getters and setters defined in the file we see above when it starts up the server side of our app.

For this tutorial, we're defining a single getter posts which returns data from the JSON Placeholder API. To make it work, we add a new property posts to the object assigned to getters which itself is assigned an object.

That object contains a property get which is assigned to a function that's responsible for "getting" our data and returning it to the HTTP request that called the getter. Inside of that function, we begin by create an instance of a URL object via the new URL() constructor (notice we've imported this up top from the url package—this is built-in to Node.js and we do not need to install it separately).

To that constructor, we pass the URL that we want to create the object for. In this case, we want to use the /posts endpoint from the JSON Placeholder API located at https://jsonplaceholder.typicode.com/posts.

Next, we make a check to see if our getter was passed any input variables when it was called (how this works will make more sense later, but think of this like being passed as a POST body to an HTTP request). If we have an id value defined on our input (the ID of a post on the JSON Placeholder API like 1 or 5), we want to create a new instance of the URLSearchParams class, passing in our input object. Here, each property on the object will be turned into a query parameter. For example, an input value of...

{ id: 5 }

will be turned into...

?id=5

To make that value useful, we set the .search property of the url object we created above to the value of searchParams cast as a string value (using the .toString() function).

Finally, with our complete url object, we call to the fetch() function we imported from the node-fetch package up top, passing the url object (fetch understands how to interpret this object). Because we expect fetch() to return us a JavaScript Promise, on the end, we call to .then() to say "after we get a response then do this."

The "this" that we're doing is taking the response object and converting it into a JSON format with the .json() method. What we expect to return from this chain of methods is an array of objects representing posts from the JSON Placeholder API.

With this in place, now we're ready to get our data wired up. To do that, we're going to need a route where we can render the component that we're going to create. Real quick, let's jump over to the /index.server.js file and set up that route.

Wiring up a route for our component

If we open up the /index.server.js file at the root of our app, we'll see that the joystick create app function we called earlier created a file that automatically imports and runs node.app() for us along with some example routes.

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/posts": (req, res) => {
      res.render("ui/pages/posts/index.js");
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

By default, a root route at / and a catch-all or 404 route at * (meaning, everything that doesn't match a route above this one) are pre-defined for us. Here, we've added an additional route /posts. To that route, we've assigned a function to handle the inbound request taking in the req and res objects. Though it may not look like it, behind the scenes, Joystick turns this into a plain Express.js route, similar to us writing app.get('/posts', (req, res) => { ... }).

Inside of that function, we make a call to a special function added by Joystick to the res object called .render(). This function, like the name implies, is designed to render a Joystick component in response to a request. To it, we pass the path to a component in our app that we want it to render, along with an object of options (if necessary, which it isn't here so we've omitted it).

When this route is matched in a browser, Joystick will go and get this component and server-side render it into HTML for us and send that HTML back to the browser. Internally, res.render() is aware of the data option on Joystick components. If it sees this on a component, it "scoops up" the call and fetches the data as part of the server-side rendering process.

This is how we're going to call to the posts getter we defined above. Our goal being to make it so that when our page loads, we get back server-side rendered HTML with out data already loaded in it.

Next, we need to actually create the component at the path we're passing to res.render() above.

Wiring up a Joystick component with data from the API

To start, first, we need to add the file we assumed will exist at /ui/pages/posts/index.js:

/ui/pages/posts/index.js

import ui from '@joystick.js/ui';

const Posts = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Posts;

Here, we're just adding a skeleton component using the ui.component() function imported from the @joystick.js/ui package (automatically installed for us by joystick create).

In the HTML string we return from our render function, for now we're just rendering an empty <div></div>. If we visit the route we added on the server in our browser at http://localhost:2600/posts, we should see a blank white page.

Now we're ready to wire up our data. Let's add everything we need and walk through it (we don't need much code):

/ui/pages/posts/index.js

import ui from '@joystick.js/ui';

const Posts = ui.component({
  data: async (api = {}, req = {}, input = {}) => {
    return {
      posts: await api.get('posts', {
        input,
      }),
    };
  },
  render: ({ data, each }) => {
    return `
      <div>
        <ul>
          ${each(data?.posts, (post) => {
            return `
              <li>
                <h4>${post.title}</h4>
                <p>${post?.body?.slice(0, 80)}...</p>
              </li>
            `;
          })}
        </ul>
      </div>
    `;
  },
});

export default Posts;

Believe it or not, this is all we need to get our data fetched and server-side rendered in our app and rendered in the browser.

At the top of our component definition, we've added a new option data assigned to a function. This function receives three arguments:

  1. api which is an object containing an isomorphic (meaning it works in the browser and on the server) version of the get() and set() functions built-in to both @joystick.js/ui and @joystick.js/node for calling to our getters and setters.
  2. req which is a browser-safe version of the inbound HTTP request (this gives us access to req.params and req.context.user so we can reference them when fetching data).
  3. input any input data passed when refetching data via the data.refetch() method (we'll cover this in a bit).

Inside of that function, we return an object that we want to assign as the value of data on our component instance. Here, because we want to get back a list of posts, we define a property posts and set it equal to a call to api.get('posts') where the 'posts' part is the name of the getter we defined earlier in the tutorial.

Because we expect an array of objects representing our posts to be returned from that getter, we assign our call directly to that function, prefixing the await keyword (and adding async to the function we pass to data) to tell JavaScript to wait until this call responds before continuing to interpret the code.

The end result here is that on the server, our data is fetched automatically and set to the data property on our component instance. Down in the render function, we can see that we've added a call to destructure or "pluck off" a data and each property from the argument passed to the render function (this is an object representing the component instance).

Instead of doing this, we could pass a single argument component to the render function and then write something like component.data and component.each to reference the exact same values as we are here.

Down in our HTML, we've added a <ul></ul> unordered list tag, and inside of it, we're using the JavaScript interpolation ${} syntax to say "in these brackets, call the each() function passing the value of data.posts."

That function, each() will loop over the array of posts we're passing it and for each one, return a string of HTML from the function we pass as the second argument to it. That function takes in the current item or, in this case, post being looped over for use in the HTML being returned.

Here, we output the title of each post and a truncated version of the body for each post in the array.

If we load up our browser now, we should see some posts rendering in the browser.

afl22qIJ1nlT7LnS/29hDRYg4WIKQNd8E.0
Our posts rendering in the browser.

While we're technically done, before we wrap up, let's quickly learn how to refetch data after the initial page load.

/ui/pages/posts/index.js

import ui from '@joystick.js/ui';

const Posts = ui.component({
  data: async (api = {}, req = {}, input = {}) => {
    return {
      posts: await api.get('posts', {
        input,
      }),
    };
  },
  events: {
    'submit form': (event, component) => {
      event.preventDefault();
      const input = component.DOMNode.querySelector('input');

      if (input.value) {
        component.data.refetch({ id: input.value });
      } else {
        component.data.refetch();
      }
    },
  },
  render: ({ data, each }) => {
    return `
      <div>
        <form>
          <input type="text" placeholder="Type a post ID here..." />
          <button type="submit">Get Post</button>
        </form>
        <ul>
          ${each(data?.posts, (post) => {
            return `
              <li>
                <h4>${post.title}</h4>
                <p>${post?.body?.slice(0, 80)}...</p>
              </li>
            `;
          })}
        </ul>
      </div>
    `;
  },
});

export default Posts;

If we're building a non-trivial UI, it's likely that at some point we'll want to refetch data based on some sort of user interaction, or, on some interval (e.g., polling for new data every 5 seconds).

On the data property assigned to our component instance, Joystick gives us a .refetch() method that we can call to perform a refetch on-demand. If we look at the HTML returned from our render() function, we can see that we've added a few more lines, adding in a simple <form></form> with an input and a button.

Recall that earlier on the server when we defined our getter, we added the potential for an id to be passed so we could fetch a specific post. By default, we're not passing anything, but to demonstrate our use of data.refetch() (and the ability to pass input values to it), here, we're adding an event listener for our form's submit event to do exactly that.

Looking at the events property we've added to our component definition, when our form is submitted, first, we want to make sure we call to the event.preventDefault() function on the event argument we're passed (this is the browser DOM event as it's happening) to prevent the standard or built-in form submission handler from being called in the browser (this triggers a page refresh that we want to skip).

Beneath this, we take the component instance that's automatically passed as the second property to our event handlers in Joystick. On that object, a DOMNode property is added which gives us access to the current component as it's rendered in the browser (the code we write here—our Joystick component—is just an abstraction for generating these DOM nodes dynamically).

On that component.DOMNode value we call the querySelector method, passing in the selector of an element we want to access. Here, we want to get the <input /> tag that's rendered in our component. In return, we expect to get back the DOM node for that input element (why we're storing it in a variable called input).

Beneath this, we conditionally call to component.data.refetch() based on whether or not our input has a value. If it does, we want to pass that value as the id property on our input object. Here, the object we pass to component.data.refetch() is automatically assigned to the input value we pass to the server when we call api.get('posts') up in our data function.

If input.value is empty, we want to skip passing any input.

The end result of this is that if we do pass a value (the ID of a post, e.g., 1 or 5), we'll pass that to our getter and expect to get back a single post from the JSON Placeholder API. If we do not pass a value, we'll expect the default response of our full list of posts.

Back in the browser, if we load this up and type a number into the input and hit "Get Post," we should see our list automatically reduced down to that one post. If we remove the number and hit "Get Posts" again, we should see the full list restored.

Wrapping up

In this tutorial, we learned how to wire up an API endpoint using the getters feature in Joystick that we call from a component using the Joystick data property to automatically fetch and server-side render our HTML with the data inside. We also learned how to render a component via a route using the res.render() method in Joystick and how to refetch data inside of a component in response to a user's behavior.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode