Guides
Building a Remix app

Build a Stacks app with Remix

Remix is a full stack web framework that lets you focus on the user interface and work back through web standards to deliver a fast, slick, and resilient user experience. People are gonna love using your stuff.

Remix is a wonderful framework built on top of React. With @micro-stacks/react and @micro-stacks/client, we will be able to create robust and high performance Stacks apps that provide a delightful user experience.

In this guide you will learn about:

  • 🏗️ Setting up @micro-stacks/react
  • 🔒 Adding authentication via Stacks based wallets
  • 🔥 How to share a user session between client and server
  • 🤩 Ensuring a better user experience via SSR

Getting started

To get started, we're going to create a new Remix app with Typescript by running the following command and following the prompts:

npx create-remix
  • Where would you like to create your app? ./micro-stacks-remix-example
  • What type of app do you want to create? Just the basics
  • Where do you want to deploy? Choose Remix if you're unsure; it's easy to change deployment targets. Vercel
  • Do you want me to run npm install? No
  • TypeScript or JavaScript? TypeScript

Next up, we need to install our micro-stacks related dependencies:

pnpm i 

@micro-stacks/client @micro-stacks/react

Client Provider

Our first step will be to import * as MicroStacks from "@micro-stacks/react"; and wrap the root.tsx file in our MicroStacks.ClientProvider component. This component allows for all of the related hooks to access a shared session client within the application. We've also added two props to the ClientProvider: appName and appIconUrl.

// root.tsx

import type { MetaFunction } from '@remix-run/node';
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';

// import added here
import * as MicroStacks from '@micro-stacks/react';

export const meta: MetaFunction = () => ({
  charset: 'utf-8',
  title: 'New Remix App',
  viewport: 'width=device-width,initial-scale=1',
});

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <MicroStacks.ClientProvider
          appName={'New Remix App'}
          appIconUrl={'https://remix.run/remix-v1.jpg'}
        >
          <Outlet />
          <ScrollRestoration />
          <Scripts />
          <LiveReload />
        </MicroStacks.ClientProvider>
      </body>
    </html>
  );
}

Authentication

Now that we've wrapped our app in the ClientProvider, we can start making use of all the hooks and other functions that @micro-stacks/react exports. Here is a simple WalletConnectButton component we can use to authenticate a user.

Wallet connect button

// components/wallet-connect-button.tsx

import { useAuth } from '@micro-stacks/react';

export const WalletConnectButton = () => {
  const { authenticate, isLoading, signOut, isSignedIn } = useAuth();
  const label = isLoading ? 'Loading...' : isSignedIn ? 'Sign out' : 'Connect Stacks wallet';
  return (
    <button
      onClick={() => {
        if (isSignedIn) void signOut();
        else void authenticate();
      }}
    >
      {label}
    </button>
  );
};

In our routes/index.tsx file, we can import { WalletConnectButton } from "~/components/wallet-connect-button"; and render it:

// routes/index.tsx

import { WalletConnectButton } from "~/components/wallet-connect-button";

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>
      <WalletConnectButton />
    </div>
  );
}

Now, if you click the button, you should see the state change to loading and the Hiro Web Wallet pop up. Before we authenticate, we should create a component that will let us know who is currently logged in:

User card component

import { useStxAddress } from "@micro-stacks/react";

export const UserCard = () => {
	const stxAddress = useStxAddress();
	if (!stxAddress) return <h2>No active session</h2>;
	return (
		<h2>{stxAddress}</h2>
	);
};

And we can add that to our index route:

// routes/index.tsx

import { WalletConnectButton } from "~/components/wallet-connect-button";
import { UserCard } from "~/components/user-card";

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>
      <WalletConnectButton />
	  <UserCard />
    </div>
  );
}

Authentication only on client

Great! now we've successfully implemented stacks authentication. However, if you play around with what we've got so far, and refresh the page many times after logging in, you'll notice that there is a momentary flash of the unauthenticated state.

We can do better! Since we're using Remix, a SSR focused framework, let's learn how we can share session state between the client and server.

Server side session data

At a high level, what we're going to add now is the ability for our app to share state between the client and server contexts. What this means is we're able to "dehydrate" (aka deserialize) the micro-stacks client state, and save it in a way that the server is able to easily use. We're going to use cookies to share this state.

This gives us the ability to make use of an active session on the server and:

  • fetch data for a currently signed in user
  • gate certain parts of our app to authenticated users only
  • no more flash of unauthenticated state
  • and more!

Cookie sessions

To start off, we're going to create a new file: session.server.ts in /app/common. This file is based off the Remix starter kit called the Blues Stack. See here for their version.

In this file we're going to create a few things:

  • sessionStorage: using Remix's createCookieSessionStorage (see their docs)
  • saveSession: a helper function to save the session to a cookie
  • destroySession: a helper function to remove the session
  • getSession: a helper function to get the current session
import type { Session } from "@remix-run/server-runtime";
import { createCookieSessionStorage, json } from "@remix-run/node";

const DEFAULT_SECRET = "this_is_a_long_password";

export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    httpOnly: true,
    maxAge: 0,
    path: "/",
    sameSite: "lax",
    secrets: [process.env.SECRET ?? DEFAULT_SECRET],
    secure: process.env.NODE_ENV === "production",
  },
});

export async function saveSession(session: Session) {
  return json(null, {
    headers: {
      "Set-Cookie": await sessionStorage.commitSession(session, {
        maxAge: 60 * 60 * 24 * 7,
      }),
    },
  });
}

export async function destroySession(session: Session) {
  return json(null, {
    headers: {
      "Set-Cookie": await sessionStorage.destroySession(session),
    },
  });
}

export async function getSession(request: Request) {
  const cookie = request.headers.get("Cookie");
  return sessionStorage.getSession(cookie);
}

Micro-stacks Client on the server

Next up, we'll create another file in the same /app/common folder named client.server.ts. This file will contain our micro-stacks specific code for replicating the client state on the server.

The first thing we'll add to this file is a function to create our micro-stacks client on the server, createServerClient. We are going to pass it a custom storage driver, one that will make use of the cookie session storage we created above.

import type { Session } from '@remix-run/server-runtime/sessions';
import { createClient, createStorage } from '@micro-stacks/client';

export const createServerClient = (session: Session) =>
  createClient({
    storage: createStorage({
      storage: {
        getItem(key) {
          return session.get(key);
        },
        setItem(key, value) {
          session.set(key, value);
        },
        removeItem(key) {
          session.unset(key);
        },
      },
    }),
  });

Action handler

Now, in that same file, we're going to add one more function that we're going to use in just a little bit. Let's add some imports to make use of our session helper functions from session.server.ts:

import { getSession, saveSession } from "~/common/session.server";

Then add the function serverSideSessionHandler:

export async function serverSideSessionHandler(request: Request) {
  const [session, data] = await Promise.all([
    getSession(request),
    request.formData(),
  ]);

  const dehydratedState = data.get("dehydratedState") as string | undefined;

  if (dehydratedState) {
    const client = createServerClient(session);
    client.hydrate(dehydratedState);
    return saveSession(session);
  }
  return null;
}

At a high level, this function will:

  • Get the current session and any FormData from the current request in parallel
  • Check to see if dehydratedState exists in the FormData
  • If so, it will create a new server Client and then hydrate it
  • Finally it will save the session to the cookie, using the saveSession helper

API Routes

Now that we have all our server side helpers created, we need to create two API routes that our application can use to safely interact with setting or deleting the server side session. The two files we will create are:

  • /app/routes/session/save.ts - this route will handle saving our session
  • /app/routes/session/destroy.ts - this route will handle removing our session

Saving the session

import type { ActionFunction } from "@remix-run/node";
import { serverSideSessionHandler } from "~/common/client.server";

export const action: ActionFunction = async ({ request }) => {
  return serverSideSessionHandler(request);
};

Destroying the session

import type { ActionFunction } from '@remix-run/node';
import { destroySession, getSession } from '~/common/session.server';

export const action: ActionFunction = async ({ request }) => {
  const session = await getSession(request);
  return destroySession(session);
};

Authentication callbacks

Now, to tie everything together, we need to create one more file: /app/common/use-session-callbacks.ts. This hook will contain two methods that will make use of the API routes and session helpers we made previously.

  • handleSetSession: this callback takes a string as an argument, and POSTs to the /session/save route as FormData
  • handleDestroySession: this callback will POST to the /session/destroy route

Both of these callbacks make use of the useFetcher hook from Remix. Check out their docs if you want to learn more.

import { useFetcher } from "@remix-run/react";
import { useCallback } from "react";

export const useSessionCallbacks = () => {
  const fetcher = useFetcher();

  /**
   * Session callback
   *
   * This function will pass the dehydrated state of the micro-stacks client
   * to the /session/save endpoint via a Remix Action.
   *
   */
  const handleSetSession = useCallback(
    async (dehydatedState: string) => {
      console.log("setting session");
      const data = new FormData();
      data.set("dehydratedState", dehydatedState);
      await fetcher.submit(data, {
        method: "post",
        action: "/session/save",
      });
    },
    [fetcher]
  );

  /**
   * Sign out callback
   *
   * This function will destroy a session by sending a POST request to
   * /session/destroy.
   */
  const handleDestroySession = useCallback(async () => {
    await fetcher.submit(null, {
      method: "post",
      action: "/session/destroy",
    });
  }, [fetcher]);

  return {
    handleSetSession,
    handleDestroySession,
  };
};

Hooking into authentication

Now that we have all the pieces put together, if we go back to our /app/root.tsx file, we can import our new useSessionCallbacks hook, add two props to the MicroStacks.ClientProvider component, and we should be good to go!

The two props we wil be:

  • onPersistState: this is a callback that runs anytime there is a state change that needs to be persisted
  • onSignOut: this callback runs when the user signs out of the current session
// root.tsx

import type { MetaFunction } from '@remix-run/node';
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';

import * as MicroStacks from '@micro-stacks/react';
import { useSessionCallbacks } from '~/common/use-session-callbacks';

export const meta: MetaFunction = () => ({
  charset: 'utf-8',
  title: 'New Remix App',
  viewport: 'width=device-width,initial-scale=1',
});

export default function App() {
  const { handleSetSession, handleDestroySession } = useSessionCallbacks();
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <MicroStacks.ClientProvider
          appName={'New Remix App'}
          appIconUrl={'https://remix.run/remix-v1.jpg'}
          onPersistState={handleSetSession}
          onSignOut={handleDestroySession}
        >
          <Outlet />
          <ScrollRestoration />
          <Scripts />
          <LiveReload />
        </MicroStacks.ClientProvider>
      </body>
    </html>
  );
}

Wrap up

To wrap up, in this guide we covered:

  • Creating a new Remix app
  • Installing out micro-stacks related dependencies
  • Adding Stacks auth
  • Creating some session related helpers
  • Creating API routes for saving/destroying session
  • Hooking into authentication callbacks
  • Fetching the current session and passing it to ClientProvider