Remix - Simple Fetch and Render Data, Styled with TailwindCSS 3.0

A quick Remix tutorial on fetching data

Remix is a Web Framework for creating amazing user experiences.

We will be covering how to fetch data from GitHub Organization Members. This will fetch a default company, and render the members avatar picture, their username, and a link to their profile. It can be used for any organization on GitHub.

Here is an image of what we will be creating:

We are going to create a default page that fetches data. We are also going to have a search feature so that users can type on a search and be re-directed to a page that shows their results. I'm using TailwindCSS for a small bit of styling and their new columns feature in V3.0. We will be using the power of Remix to avoid any useState or useEffect hooks and just let the web do what it was designed to do. We will also get to peek at the network tab and see how Remix is pulling our cached data without us doing any work!

Let's get started! 🚀

Remix App

Let's create the default Remix app.

For this tutorial, I'm going to be using the default create-remix@latest command which sets up our project and gives us a demo site, we will also be using the Remix App Server for local testing. You can change this at a later time if you wish to deploy it.

Open your terminal and run npx create-remix@latest
When prompted where you want to deploy, choose Remix App Server. Name your project however you like, I'm going to name mine remix-fetch-example.

TailwindCSS 3.0 Setup with Remix

Open your terminal and let's install tailwind
npm install -D tailwindcss
We also need Concurrently for running tailwind.css in our dev environment.
npm install concurrently

Perfect, now lets initialize Tailwind to create our tailwind.config.js file
npx tailwindcss init

Let's update our tailwind.config.js file in our application for their purge function for any javascript .js or .jsx file.

module.exports = {
  purge: ["./app/**/*.{js,jsx}"], // Here we are going to tell Tailwind to use any javascript .js or .jsx file
  theme: { extend: {

  } },
  variants: {},
  plugins: [], 
};

Perfect, now we need to update our package.json file with scripts to generate our tailwind.css file.
Update your package.json scripts section to match this

"scripts": {
    "build": "npm run build:css && remix build",
    "build:css": "tailwindcss -o ./app/tailwind.css",
    "dev": "concurrently \"npm run dev:css\" \"remix dev\"",
    "dev:css": "tailwindcss -o ./app/tailwind.css --watch",
    "postinstall": "remix setup node",
    "start": "remix-serve build"
  },

Now when we run npm run dev it will generate a tailwind.css file in the root of our /app/ folder. We need to tell Remix that we can to use this style sheet. I'm going to set this up in our Root file so that TailwindCSS styles are imported to the entire site. Remix does this by importing our styles and using their links function to apply the stylesheet to the head of the HTML file.

Open your root.jsx file under (/app)
Add the following import statement and then update the exported links function:

import tailwindstyles from "./tailwind.css";


// https://remix.run/api/app#links
export let links = () => {
  return [
    { rel: "stylesheet", href: globalStylesUrl },
    {
      rel: "stylesheet",
      href: darkStylesUrl,
      media: "(prefers-color-scheme: dark)"
    },
    { rel: "stylesheet", href: tailwindstyles }
  ];
};

Perfect, TailwindCSS is all setup in our Remix app

Add a link to our new page

Open your root.jsx file under (/app/root.jsx)

Locate the section function Layout({ children }) {
Add a list item to /fetch

<li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/fetch">Fetch</Link>
              </li>
              <li>
                <a href="https://remix.run/docs">Remix Docs</a>
              </li>

Create Fetch Route

In Remix, creating a route is as simple. In your (/app/routes/) folder, create a new folder called fetch.

Creating fetch module

We are going to create a single module that will be used to pull in data to our app, it will be used both by our default fetch route and our search route.

This module will have a single function that fetches and returns data for a given company. Our function will accept a parameter called 'company'. If the parameter is not used then, by default we are going to fetch the Remix-run GitHub organization.

Create a new file called github.js
Add the following 5 lines of code, that's it, 5 lines to fetch data 🚀

export async function getMembers(company){
   const searchCompany = !company ? "remix-run" : company;
       let res = await fetch(`https://api.github.com/orgs/${searchCompany}/members`)
    return res;
}

Creating Fetch Index Page

We need a default page when users visit our /fetch route, to tell Remix which default page to load, we are going to create a index.jsx file inside our /fetch folder.

First we are going to need to load data, we are going to use the Remix loader function for this and we need to import our getMembers function from our GitHub module.

Update your (/app/routes/fetch/index.jsx) as follows:

import { getMembers } from "./github";

export let loader = async () => {
    return getMembers();
}

Perfect, now we want to use this loader in our default function so we can access the content.

First, we have to import useLoaderFunction from Remix so let's add this to the top.
import { Form, useLoaderData, redirect } from "remix";

Then we need to create our default function. Add this to the same index.jsx file. There are some basic TailwindCSS styles being applied here, be sure to include them.

export default function Fetch(){
    let members = useLoaderData();

    return (
        <div>
            <div>
            <h1 className="text-xl text-red-500 my-5">Hello Fetch</h1>
            </div>
            <div>
                <div className="columns-1 md:columns-2 lg:columns-xl">
                   {members.map(member => (
                    <div key={member.login} className="flex flex-col w-full border-solid border-2 border-blue-400 rounded-lg px-2 py-2 my-0.5">
                        <img src={member.avatar_url} className="w-10 h-10 rounded-full" />
                        <h3 className="text-1xl font-semibold">User: {member.login}</h3>
                        <a href={member.html_url}>Github Profile: {member.html_url}</a>
                    </div>
                    ))}
                </div>
            </div>
        </div>
    )
}

Perfect, we should now be rendering data on our site. Let's load it up and give it a try.

Start your server using npm run dev. If it doesn't open automatically, then bring up your site using http://localhost:3000/

If you see your content being displayed then we are half-way done!

Creating Search Functionality

We want users to be able to search and put in any organization name on GitHub and see a list of members. We are going to use the Remix 'Form' for this along with Redirect and their Action function. Our search is going to accept input from the user, then redirect them to a new page and display the results, it will also work purely on routing by looking at the URL parameters.

In our Index.jsx file under (/app/routes/fetch/index.jsx) let's update our imports to include Form and redirect.
import { Form, useLoaderData, redirect } from "remix";

Perfect, now let's setup our Form that the user will see. In the return section, let's add this right under Hello Fetch, but before our data renders. We will create a Form, add a label, add an input text field named search, and a button to submit the form.

Update your return section as follows

export default function Fetch(){
    let members = useLoaderData();

    return (
        <div>
            <div>
            <h1 className="text-xl text-red-500 my-5">Hello Fetch</h1>
            <Form className="search" method="post">
                <label htmlFor="search">Search:</label>
                <input type="text" name="search" id="search"/>
                <button type="submit" className="bg-blue-200 m-2 p-2 rounded hover:bg-blue-500">Search</button>
            </Form>
            </div>
            <div>
                <div className="columns-1 md:columns-2 lg:columns-3">
                   {members.map(member => (
                    <div key={member.login} className="flex flex-col w-full border-solid border-2 border-blue-400 rounded-lg px-2 py-2 my-0.5">
                        <img src={member.avatar_url} className="w-10 h-10 rounded-full" />
                        <h3 className="text-1xl font-semibold">User: {member.login}</h3>
                        <a href={member.html_url}>Github Profile: {member.html_url}</a>
                    </div>
                    ))}
                </div>
            </div>
        </div>
    )
}

Awesomesauce. Now we need to setup our Action so it knows what to do when the user submits our form.

Our Action is going to extract the form data from our serialized request and get the value of the "search" text field. We are then going to use that with Redirect to send our user to the results page

At the top of the same index.jsx file (/app/routes/fetch/index.jsx) add the following action function below our existing loader function.

export let action = async ({request}) => {
    //When a user searches, the form data will be submitted as formData in the request
    // we will look in there for the form field "search" and obtain it's value for the redirect
    const formData = await request.formData();
    const searchCompany = formData.get("search")
    return redirect(`/fetch/${searchCompany}`)
}

Now we have the ability to search, it's time to setup the route where the redirect is sending us.

For our Search function, we are going to setup a parameterized route, this means our file name will begin with a $ and will act as a variable for fetching data from our GitHub module.

In your (/app/routes/fetch) folder, create a file called $search.jsx. Be sure to include the $.

Our Search file will be a more condensed version of our fetch index. We are again using the Remix loader function, but this time we are going to look at the URL parameters, and thanks to parameterized routing, we have a URL param named search that we can pass to our GitHub module to fetch data. We will then render that using the useLoaderData() function.

Update your $search.jsx file as follows:

import { useLoaderData } from "remix";
import { getMembers } from "./github";

export let loader = async ({params}) => {
    let res = await getMembers(params.search);
    return res;
}

export default function Search(){
    let members = useLoaderData();
    return (
        <div>
                <h1 className="text-xl text-red-500 my-5">Hello Search</h1>
                <div className="columns-1 md:columns-2 lg:columns-xl">
                    {members.map(member => (
                    <div key={member.login} className="flex flex-col w-full border-solid border-2 border-blue-400 rounded-lg px-2 py-2 my-0.5">
                        <img src={member.avatar_url} className="w-10 h-10 rounded-full" />
                        <h3 className="text-1xl font-semibold">User: {member.login}</h3>
                        <a href={member.html_url}>Github Profile: {member.html_url}</a>
                    </div>
                    ))}
                </div>
        </div>
    )
}

Your App should now look like this with the search form:

Let's give it a go, open your app and search for "microsoft" and press Search. You should be redirected and get a result similar to this:

Perfect, your app is now complete! The rest of this tutorial will go over some network tab information and looking at how this content is Cached.

Let's look at the network tab for caching

If you pull up your developer tools and look at the network tab. You can see that your fetch route is now pulling in the images from memory cache instead of fetching them from the server. The same for our CSS file, and most of the JavaScript is coming from our disk cache. Keep in mind this is all localhost and the experience would be slightly different if hosted on the web.

Let's look at a bigger one with the Microsoft search

Now let's look at a organization we haven't loaded, I'm going to choose Nasa. Here we can see that our Style is still pulling from Cache and it's loading in all of the images. As the page loaded, there was a brief pause and then all the content was loaded at once.

I turned on Slow 3G and searched "vercel". All of the columns were populated with Users and Profile links, while the images took a bit longer to download, however on the user experience, loading the usable content first creates a better experience.

Now that it's loaded, if the user refreshes, all of the previous unchanged content will pull in from cache which will give a much more responsive experience if the user has visited the page before or navigates back to this page.

Conclusion

Remix is an amazing web framework that makes it quick and easy to pull data into your site and render that content, it can be done in very few lines of code. It's also quite simple to submit new requests for data and have that rendered. The built in caching functionality greatly improves user experience but simultaneous downloading of content ensures the user has functional content before their network downloads the larger bandwidth required data.

27