Skip to content

RESOURCES / BLOG

How to Build a TanStack Start Project for Image Optimization and Uploading

Why It Matters

TanStack Start offers a performance-first approach to SSR, routing, and data management – ideal for dynamic applications requiring speed, flexibility, and control.

If you’ve been working in the React ecosystem for a while, you’re probably familiar with some of the TanStack libraries like Router or Query. TanStack Start is a new full-stack React framework powered by TanStack Router. It doesn’t yet have all the bells and whistles that a more established framework like Next.js has, so there isn’t a component that automatically handles optimizing images on your site or a third-party Cloudinary framework integration. Still, we can add image/video optimization and much more to our TanStack Start website with the Cloudinary React SDK.

In this blog post, we’ll create a simple TanStack Start project and add image optimization and uploading features using the Cloudinary React SDK.

There isn’t a fancy tool for starting a new TanStack Start project like you might get with Next or Astro quite yet. The easiest way to get started is to copy one of the example projects available in the project repository. For this demo, I’ll copy over the start-basic example project.

Once you have the start-basic directory copied over to your computer, navigate to the directory in your terminal. We can install the dependencies and start the dev server to see what the basic project looks like.

npm i
npm run dev

The dev server will start at http://localhost:3000. We get a basic web page with a navigation bar that we can use to navigate to some of the example pages.

screen shot of dev server

Now that we know what the site looks like, we’ll dive into how the project is structured. All the code lives in the app directory.

Here are the important parts that we’ll do most of our work in:

- components # directory for our React components
- routes # TanStack Start/Router file-based routing
- styles # css styles
- utils # js utilities
Code language: PHP (php)

If you open up the routes directory, the naming convention may look a little strange at first. You can find all the details on the file-based routing conventions here.

Now that we’re familiar with our new TanStack Start website, let’s plan out our new features.

First, we want to be able to upload images of our users to our Media Library.

Next, we’ll create an Avatar component to serve optimized avatar images of our users.

Because we haven’t set up a real database yet, our avatar images won’t be linked to any specific users on the demo site. In this post, they’re purely for illustration, but you can easily configure them to use real data.

To get started, we’ll need to install our Cloudinary dependencies:

npm i @cloudinary/url-gen @cloudinary/react
Code language: CSS (css)
npm i -D @types/cloudinary-uploadwidget-browser
Code language: CSS (css)

Add the following key to your compilerOptions in tsconfig.json

    "types": ["@types/cloudinary-uploadwidget-browser"],
Code language: JavaScript (javascript)

Next, let’s create a file named cloudinary.ts in our app directory. This file will create and export our configured Cloudinary instance:

import { Cloudinary } from "@cloudinary/url-gen";

export const cld = new Cloudinary({
  cloud: {
    cloudName: 'demo'
  }
});
Code language: JavaScript (javascript)

Replace ‘demo’ with your own cloud name.

To use the Upload widget, we’ll need to add the script to our RootDocument component /app/routes/__root.tsx. We can put it right below the framework’s <Scripts /> tag:

        {children}
        <ScrollRestoration />
        <TanStackRouterDevtools position="bottom-right" />
        <Scripts />
        <script
          src="https://upload-widget.cloudinary.com/latest/global/all.js"
          type="text/javascript"
        ></script>
      </body>
    </html>
  ) 
Code language: HTML, XML (xml)

We’ll use the Upload widget to handle uploading avatar images to our media library. Let’s create the file for our component app/components/CloudinaryUploadWidget.tsx.

We’ll need to add our cloud name and an upload preset to the Upload widget config. If you’re not familiar with upload presets, you can learn how to set one up here.

import { useEffect, useRef } from 'react';

declare global {
  interface Window {
    cloudinary: CloudinaryBase;
  }
}

interface CloudinaryUploadWidgetProps {
 onUpload?: (publicId: string) => void;
}

export const CloudinaryUploadWidget: React.FC<CloudinaryUploadWidgetProps> = ({ uwConfig, setPublicId }) => {
  const uploadWidgetRef = useRef<any>(null);
  const uploadButtonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    const initializeUploadWidget = () => {
      if (window.cloudinary && uploadButtonRef.current) {
        // Create upload widget
        uploadWidgetRef.current = window.cloudinary.createUploadWidget(
          {
	          cloudName: "your_cloud_name",
	          uploadPreset: "upload_preset_id"
          },
          (error: any, result: any) => {
            if (!error && result && result.event === 'success') {
              console.log('Upload successful:', result.info);
              onUpload?.(result.info.public_id);
            }
          }
        );

        // Add click event to open widget
        const handleUploadClick = () => {
          if (uploadWidgetRef.current) {
            uploadWidgetRef.current.open();
          }
        };

        const buttonElement = uploadButtonRef.current;
        buttonElement.addEventListener('click', handleUploadClick);

        // Cleanup
        return () => {
          buttonElement.removeEventListener('click', handleUploadClick);
        };
      }
    };

    initializeUploadWidget();
  }, [onUpload]);

  return (
    <button
      ref={uploadButtonRef}
      id="upload_widget"
      className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
    >
      Upload
    </button>
  );
};
Code language: JavaScript (javascript)

The component we created will display a button that opens the Upload Widget.

Now that we have our Upload widget ready, we can add it to our user page. Let’s open the page up in our routes directory app/routes/users.$userId.tsx

Currently, this page just shows the user name and email. Let’s add our uploader button below them:

// existing code...
import { CloudinaryUploadWidget } from '~/components/CloudinaryUploadWidget'

// exisiting code...

function UserComponent() {
  const user = Route.useLoaderData()
  
  const handleUpload = (publicId: string) => {
    // In our real application we could update the user to set the avatar value in the database
    // IE: updateUser({ avatarPublicId: publicId });
    console.log(publicId);
  }

  return (
    <div className="space-y-2">
      <h4 className="text-xl font-bold underline">{user.name}</h4>
      <div className="text-sm">{user.email}</div>
      <CloudinaryUploadWidget />
    </div>
  )
}
Code language: JavaScript (javascript)

You can imagine using the onUpload prop to our CloudinaryUploadWidget to save the avatar id to our user record in the database. We added an example in the comments. We’ll use this example field to add avatar images to our User route.

screenshot of user route

Next we’ll create a UserAvatar component and use it to render optimized avatar images on our user route. We’ll use the AdvancedImage component from the @cloudinary/react SDK to render the image after we apply some transformations with @cloudinary/url-gen to optimize the image for user avatars.

import { AdvancedImage } from '@cloudinary/react'
import { thumbnail } from '@cloudinary/url-gen/actions/resize'
import { focusOn } from '@cloudinary/url-gen/qualifiers/gravity'
import { FocusOn } from '@cloudinary/url-gen/qualifiers/focusOn'
import { cld } from '../cloudinary'

interface UserAvatarProps {
  publicId?: string
  size?: number
  className?: string
}

export function UserAvatar({ publicId, size = 150, className = '' }: UserAvatarProps) {
  if (!publicId) {
    return (
      <div 
        className={`bg-gray-200 rounded-full flex items-center justify-center ${className}`}
        style={{ width: size, height: size }}
      >
        <span className="text-gray-500 text-2xl">?</span>
      </div>
    )
  }

  const image = cld.image(publicId)
    .resize(thumbnail().width(size).height(size).gravity(focusOn(FocusOn.face())))

  return (
    <div className={className}>
      <AdvancedImage cldImg={image} />
    </div>
  )
} 
Code language: JavaScript (javascript)

We’ll create a transformation pipeline for our user avatar images that uses AI to detect faces, crops the image to focus on the detected face, and resizes it to the specified dimensions. The AdvancedImage component serves and renders this image through Cloudinary’s CDN.

Now that we have our UserAvatar component, we can use it to add avatar images to our Users route.

// existing code...
import { UserAvatar } from '~/components/UserAvatar'

// exisiting code...

function UserComponent() {
  const user = Route.useLoaderData()

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-4">
        <UserAvatar publicId={user.avatarPublicId} size={100} />
        <div>
          <h4 className="text-xl font-bold underline">{user.name}</h4>
          <div className="text-sm">{user.email}</div>
        </div>
      </div>
      <CloudinaryUploadWidget onUpload={handleUpload} />
    </div>
  )
}
Code language: JavaScript (javascript)

upload

Cloudinary’s React SDK and Upload widget make integrating media optimizations and uploads a quick and simple process for any React framework. TanStack Start is a powerful new framework for building React applications and has quite a few stand-out features and a lot in common with the popular Remix framework.

In this post, we added the Cloudinary Upload widget to upload user avatar images in our application. After that, we created an optimized UserAvatar component using Cloudinary’s transformations and the AdvancedImage component.

This is a great starting point, so you can easily add optimizations for other images throughout your application. Check out other React sample projects to help you get started integrating Cloudinary and sign up for a free account today.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free