Running Filestack React on Vite and Remix needs no plugin, no alias and no transpile entry. v7 corrected its ESM and CJS export maps, so both bundlers resolve the package on their own. Vite works with an empty config. Remix needs one extra install that nothing warns you about, and it fails at request time rather than at build time.
Both apps are in filestack-snippets, on Vite 7 and Remix 2.15.
Key takeaways
- Vite needs no Filestack-specific configuration at all.
- Remix needs
tslibinstalled, and the error appears at request time rather than at build. - Gate the picker behind a mounted flag on Remix, with a same-size placeholder.
- Do not reach for
ssr.noExternal; it swaps one error for another. - Vite bakes the key in at build time, while Remix can change it without rebuilding.
Vite
There is nothing to configure.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
That is the entire config in the example repo. Install both packages, since filestack-js is a peer dependency in v7, and the picker works:
npm install filestack-react@^7.0.1 filestack-js@^4.0.1
The environment variable needs the VITE_ prefix to reach the browser:
In .env:
VITE_FILESTACK_API_KEY=your_key
<FilestackProvider apikey={import.meta.env.VITE_FILESTACK_API_KEY}>
The key is meant to be readable in the bundle. It identifies the application rather than authenticating it. The app secret, which signs policies, is the value that must never carry a VITE_ prefix, since anything that does is published in your JavaScript.
If you are upgrading a Vite project from v6, the optimizeDeps.include and resolve.alias entries that were needed to make the old export map behave can come out. Remove them and rebuild. The corrected export map is one of the changes listed in the React SDK v7.0.0 notes.
Remix needs tslib installed
filestack-js resolves tslib at runtime. Vite’s browser pre-bundling elides the import and Next.js already carries tslib in its own tree, so neither surfaces it. Remix’s server pass does:
Cannot find module 'tslib'
Require stack:
- node_modules/filestack-js/build/main/index.js
The build succeeds and the error appears only when a route renders.
npm install tslib
Do not reach for ssr.noExternal to work around this. Bundling filestack-js into the server pass replaces the error with require is not defined, because its ESM build still contains a CommonJS require in the Node request adapter. Leaving the package external and installing tslib is the correct fix.
Remix and the server render
Remix renders on the server first, and the picker touches window as it mounts. That is the remaining difficulty, and the fix is a mounted flag.
// app/routes/_index.tsx
import { useEffect, useState } from 'react';
import { PickerInline } from 'filestack-react';
export default function Index() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return (
<main>
<h1>Upload</h1>
{mounted ? (
<div style={{ height: 500 }}>
<PickerInline />
</div>
) : (
<div style={{ height: 500 }} />
)}
</main>
);
}
The placeholder matters as much as the flag. Rendering nothing on the server and a 500 pixel picker on the client produces a layout shift and a hydration warning. Rendering an identically sized empty div in both passes avoids each.
The provider itself is fine at the root, because Remix has no server component boundary and the callbacks never cross a serialization line:
// app/root.tsx
<FilestackProvider apikey="YOUR_API_KEY" onUploadDone={handleDone}>
<Outlet />
</FilestackProvider>
That is the opposite of the Next.js App Router, where the provider needs its own client component.
What the builds produce
These are the production builds the example repo produces.
Vite produces a single 786 KB bundle, 197 KB gzipped, most of it the picker. Remix splits the picker into its own 580 KB chunk, 134 KB gzipped, without being asked, and the Remix server bundle stays at 7 KB, which confirms the picker never reached it.
The Remix split is the more useful default, since the chunk only loads on routes that reference it. In a Vite application the equivalent is a dynamic import around the picker component, which matters on a landing page and does not on an internal tool.
Neither number is small. The picker carries cloud source integrations, an image editor and the upload machinery, and its weight reflects that. Where that weight sits in the wider picture of a React app’s load is covered in the guide to React performance optimization.
Environment variables, side by side
The two frameworks differ most here. Both need a key, and you can create a free API key before you start.
Vite exposes variables prefixed VITE_ through import.meta.env, at build time. A variable added after the dev server started will not appear until it restarts.
Remix has no prefix convention and no client-side process.env. Values reach the browser through a loader, which is more ceremony and more explicit:
export async function loader() {
return { apikey: process.env.FILESTACK_API_KEY };
}
Then read it with useLoaderData in the component. The practical consequence is that a Remix application can change its API key without a rebuild, and a Vite application cannot.
Why the server render is the only real obstacle
The picker is a DOM widget. It creates elements, measures them and attaches listeners, and it does that when the component mounts. On the server there is no document to do any of it to. So the component cannot run in the server pass, and any approach that keeps it out of that pass works equally well.
What v7 fixed is the surrounding noise rather than this fact. The 'use client' directive on the package tells frameworks that understand the boundary where the component belongs. The move to useId() means the DOM id the picker mounts into is generated identically on both sides, which was previously a source of hydration mismatches even when the picker itself was correctly gated. The corrected export map means the bundler picks the right build without an alias.
None of that removes the need to keep a DOM widget out of a render that has no DOM. It removes the three problems that used to accompany it, so the remaining fix is a two-line mounted flag rather than a wrapper module.
On Remix specifically there is a second reason to prefer the flag over a dynamic import. A dynamic import defers the chunk until after hydration, so the picker cannot be part of the initial route load even when you want it to be. The flag keeps the chunk in the route graph and only defers the mounting, which is a smaller change to how the page loads.
Choosing between the two frameworks
If the choice is still open, the file upload part of it should not decide anything, because both work. What differs is what surrounds it.
Vite suits a client-rendered application where the upload screen is behind a login and first paint is not the concern. The configuration is minimal, the environment variable handling is the simplest of any framework here, and the single bundle is easy to reason about.
Remix suits an application where the upload sits on a page that also needs server-rendered content, and where the automatic chunk splitting means the picker’s weight is confined to the routes that use it. The cost is the mounted flag and the loader ceremony around the key.
Either way the picker code itself is identical. Moving an integration between them is a change to the surrounding files rather than to the upload.
Common failures
Cannot find module 'tslib' on Remix. The undeclared dependency described above. Run npm install tslib.
Invalid hook call and a blank Remix page. Two copies of React in the bundle. It happens when the project sits inside another one that has its own React, and resolve.dedupe in vite.config.ts fixes it:
// vite.config.ts
export default defineConfig({
plugins: [react()],
resolve: { dedupe: ['react', 'react-dom'] },
});
The picker never appears on Remix. The mounted flag is missing, or the effect never runs because the component is rendered inside a conditional that is false on the client too.
window is not defined during the Remix build. Something imports the picker at module scope in a file that also runs on the server. Move the import inside the component or gate the route.
The key is undefined in Vite. Either the prefix is missing or the dev server predates the variable.
Hydration mismatch warnings. In v7 these are rarely the picker itself, since ids come from useId(). Check the placeholder is the same size in both passes.
What happens after the upload
The framework has no bearing on the result. Each file returns a handle, transformations are path segments in front of it, and the same URL renders identically whichever bundler built the page. The guide to crop, resize and filter images in React works through that syntax from a React component.
Every prop the components accept is the same on both frameworks, and the reference is on the React file upload SDK page.
FAQ
Why does my Remix build succeed but the page fail?
Because filestack-js resolves tslib at runtime and only Remix’s server pass surfaces it. The error appears when a route renders rather than at build time. Run npm install tslib.
Can I use ssr.noExternal instead of installing tslib?
No. Bundling the package into the server pass swaps the missing-module error for require is not defined, because the ESM build still contains a CommonJS require in its Node request adapter. Keep it external.
Why does the Remix page shift when the picker appears?
The placeholder is missing or a different size. Render an identically sized empty div in the server pass so the layout does not move and hydration matches when the mounted flag flips.
Does Vite or Remix handle the API key better?
Remix, if you need to change it without redeploying. Vite bakes VITE_ variables in at build time, while Remix passes the value through a loader at request time. The trade is more ceremony on the Remix side.
Joshua is a web developer with over 4 years of experience building responsive, high-performance websites and web applications. Currently working as an AI Automation Specialist, he combines modern web development with automation to create efficient, scalable digital solutions. He shares practical insights on WordPress, web development, and emerging technologies.
Read More →