DEV Community

Cover image for Connect Any File Source to Your Upload Flow with Custom Source
IderaDevTools
IderaDevTools

Posted on • Originally published at blog.filestack.com

Connect Any File Source to Your Upload Flow with Custom Source

You’ve probably run into this situation before: your File Picker works fine with local uploads, Google Drive, and Dropbox, but your users need to pull files from somewhere else. Maybe it’s your company’s internal DAM, a headless CMS, or a custom media library.

The typical approach is to build a separate upload flow, handle the file fetching yourself, then pass the files to Filestack. It works, but now you’re maintaining two different experiences.

Custom Source lets you skip that workaround entirely. You can add any file source directly into the File Picker UI, giving your users a single, consistent interface regardless of where their files live.

How Custom Source Works

At its core, Custom Source is a JavaScript object you pass into the picker configuration alongside built-in sources. You define what the source looks like and what happens when users interact with it through mounted and unmounted lifecycle methods.

Here’s the basic structure:

const customSource = {
  label: 'My Custom Source',
  name: 'myCustomSource',
  icon: '<svg>...</svg>',
  mounted: (element, actions) => {
    // Build your UI here
  },
  unmounted: (element) => {
    // Cleanup when user navigates away
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you simply add it to your picker configuration

const options = {
  fromSources: ['local_file_system', customSource, 'googledrive'],
};
Enter fullscreen mode Exit fullscreen mode

The picker treats your custom source the same as built-in ones. It shows up in the sidebar with your label and icon, and when users click it, your mounted function executes.

Building a Custom Source in React

Since many teams use React, let’s walk through a complete implementation that connects an internal brand asset library to the File Picker.

Setting Up the Custom Source Hook

First, create a custom hook that encapsulates the Custom Source logic. This approach follows React best practices by using useCallback to memoize functions and useRef to persist values across renders without triggering re-renders:

// useCustomSource.js
import { useCallback, useRef } from 'react';

export function useCustomSource({ fetchAssets, getAuthHeaders }) {
  const cleanupRef = useRef(null);
  const createCustomSource = useCallback(() => {
    return {
      label: 'Brand Assets',
      name: 'brandAssets',
      icon: '<svg>...</svg>',
      mounted: async (element, actions) => {
        const container = document.createElement('div');
        container.innerHTML = '<p>Loading assets...</p>';
        element.appendChild(container);
        const assets = await fetchAssets();
        container.innerHTML = '';
        const selectedIds = new Set();
        assets.forEach(asset => {
          const item = document.createElement('div');
          item.innerHTML = `
            <img src="${asset.thumbnail}" alt="${asset.name}" />
            <p>${asset.name}</p>
          `;
          item.addEventListener('click', () => {
            if (selectedIds.has(asset.id)) {
              selectedIds.delete(asset.id);
            } else {
              selectedIds.add(asset.id);
              actions.fetchAndAddUrl(asset.downloadUrl, getAuthHeaders());
            }
          });
          container.appendChild(item);
        });
        // Add submit button
        const submitBtn = document.createElement('button');
        submitBtn.textContent = 'Review Selected';
        submitBtn.addEventListener('click', () => {
          if (selectedIds.size > 0) {
            actions.showSummaryView();
          }
        });
        container.appendChild(submitBtn);
        cleanupRef.current = () => selectedIds.clear();
      },
      unmounted: () => {
        if (cleanupRef.current) {
          cleanupRef.current();
        }
      }
    };
  }, [fetchAssets, getAuthHeaders]);
  return createCustomSource;
}
Enter fullscreen mode Exit fullscreen mode

Creating the File Picker Component

Next, build a component that initializes the picker with your custom source:

// FilePicker.jsx
import { useCallback } from 'react';
import * as filestack from 'filestack-js';
import { useCustomSource } from './useCustomSource';

export function FilePicker({ onUploadComplete }) {
  const fetchAssets = useCallback(async () => {
    const response = await fetch('/api/brand-assets');
    return response.json();
  }, []);
  const getAuthHeaders = useCallback(() => ({
    'Authorization': `Bearer ${localStorage.getItem('authToken')}`
  }), []);
  const createCustomSource = useCustomSource({ fetchAssets, getAuthHeaders });
  const openPicker = useCallback(() => {
    const client = filestack.init('your_api_key');

    const picker = client.picker({
      fromSources: ['local_file_system', createCustomSource(), 'googledrive'],
      maxFiles: 10,
      onUploadDone: (result) => onUploadComplete(result.filesUploaded)
    });

    picker.open();
  }, [createCustomSource, onUploadComplete]);
  return <button onClick={openPicker}>Select Files</button>;
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Actions API

The actions object provides several methods for interacting with the picker. Here are the ones you’ll use most often.

fetchAndAddUrl(url, headers) accepts a URL and optional headers, fetches the file, extracts metadata, and adds it to the upload queue. This is ideal when files live behind an authenticated API.

addRawFile(fileObj) lets you add a File or Blob directly without an additional network request.

showSummaryView() switches to the review screen after users finish selecting files.

metadata(url, headers) fetches file information before adding to the queue, which is helpful for displaying sizes or validating types.

The actions object also includes filesList and count properties to track selection state, along with getFile(uuid) and removeFile(uuid) for managing individual files. For the complete list of available methods, see the Custom Source API reference.

When to Use Custom Source

Custom Source works best when you need files from a source without a built-in integration, yet still want the standard picker experience.

Internal asset management systems like corporate DAMs or media libraries are prime candidates. Users can browse your internal systems directly within the picker instead of downloading and re-uploading.

Third-party APIs that store files but lack native Filestack connectors become accessible through Custom Source. You build the bridge, and users get the same seamless experience.

Curated collections like product images or template libraries can guide users toward approved assets without building a separate UI.

Implementation Tips

Before you start building, keep these considerations in mind.

Clean Up Properly

The unmounted function runs when users navigate away from your custom source. Use it to clear event listeners and cancel pending requests, or you’ll create memory leaks. As noted in MDN’s documentation on removeEventListener, you must keep a reference to the handler function so it can be properly removed later.

Handle Authentication Early

For sources that require authentication, manage the auth flow before the picker opens. Alternatively, you can build a login step into your mounted function if the auth state might expire mid-session.

Include Headers for Protected URLs

When fetching from URLs that require authorization, always pass the headers parameter to fetchAndAddUrl. Without it, the request goes out without credentials and will likely fail.

Getting Started

The full API reference is available in our documentation at Custom Source API, and there’s a step-by-step tutorial that walks through building a working custom source from scratch. You can also find the example source code on GitHub.

If you’re already using the File Picker, adding a custom source typically takes just a few hours. The lifecycle methods are straightforward, the actions API handles the complex parts, and you end up with a unified upload experience instead of maintaining multiple code paths.

This article was published on the Filestack blog.

Top comments (0)