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
}
}
Then you simply add it to your picker configuration
const options = {
fromSources: ['local_file_system', customSource, 'googledrive'],
};
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;
}
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>;
}
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)