A smart, type-safe asset management system for React Native that automatically generates type definitions and provides a simple, intuitive API for loading images and SVG icons.
- 🎯 Type-Safe: Automatically generates TypeScript definitions for all your assets
- 🚀 Zero Configuration: Works out of the box with sensible defaults
- 📦 Asset Registry: Automatic scanning and registration of assets
- 🎨 SVG Support: Native SVG icon rendering with tinting and sizing
- 🔄 Variants: Support for density variants (@2x, @3x), dark mode, and platform-specific assets
- 🌐 Remote Assets: Load remote images with caching and fallback support
- ⚡ Preloading: Preload critical assets with progress tracking
- 🌙 Dark Mode:
useAssetThemeautomatically switches between-dark/-lightasset variants - �️ Placeholders: Shimmer, blur, and colour skeletons while images load
- 🗜️ CLI Optimize: Compress PNG/JPEG assets and warn about oversized files
- 🔍 CLI Tools: Generate, validate, and get statistics about your assets
- 👀 Watch Mode: Auto-regenerate types when assets change
- ⚙️ Expo Config Plugin: Auto-run
generateon everyexpo prebuild— no manual steps - 🔒 Babel/Metro Plugin: Catch asset-name typos as build errors with "did you mean?" suggestions
npm install @weprodev/react-native-smart-assets
# or
yarn add @weprodev/react-native-smart-assetsCreate an assets directory in your project root and add your images and icons:
assets/
├── images/
│ ├── logo.png
│ └── background.jpg
└── icons/
├── home.svg
└── user.svg
Run the CLI tool to generate type-safe asset definitions:
npx @weprodev/react-native-smart-assets generateThis creates an assets/index.ts file with all your assets registered and typed.
In your app entry point (e.g., App.tsx or index.js):
import { setAssetRegistry } from '@weprodev/react-native-smart-assets';
import * as Assets from './assets';
setAssetRegistry(Assets.ASSETS, Assets.ASSET_METADATA);The ASSET_METADATA parameter is optional but recommended as it provides additional information about your assets (type, category, etc.) that can be used for better asset handling and type checking.
import { Asset } from '@weprodev/react-native-smart-assets';
function MyComponent() {
return (
<>
<Asset name="images/logo" size={100} />
<Asset
name="icons/home"
size={24}
tintColor="#000"
/>
<Asset
name="images/background"
size={{ width: 300, height: 200 }}
resizeMode="cover"
/>
</>
);
}Here's a complete example demonstrating all the key features:
import { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import {
setAssetRegistry,
Asset,
useAsset,
useAssetPreloader,
preloadRemoteAsset,
isRemoteUrl,
} from '@weprodev/react-native-smart-assets';
import * as Assets from './assets';
import type { AssetName } from './assets';
// Initialize the asset registry
setAssetRegistry(Assets.ASSETS, Assets.ASSET_METADATA);
const REMOTE_ASSET_URLS = [
'https://picsum.photos/200/200?random=1',
'https://picsum.photos/200/200?random=2',
];
export default function App() {
const [selectedAsset, setSelectedAsset] = useState<AssetName>('icon');
const assetInfo = useAsset(selectedAsset);
const { preload, progress, isLoading, error } = useAssetPreloader();
// Preload assets on mount
useEffect(() => {
preload();
}, [preload]);
const handlePreloadSelected = () => {
preload([selectedAsset]);
};
const handlePreloadRemoteAssets = async () => {
try {
await Promise.allSettled(
REMOTE_ASSET_URLS.map((url) => preloadRemoteAsset(url, 10000))
);
} catch (err) {
console.error('Failed to preload remote assets:', err);
}
};
return (
<ScrollView>
{/* Local Asset Selection */}
<View>
<Text>Select Asset:</Text>
{Assets.getAllAssetNames().map((name) => (
<TouchableOpacity
key={name}
onPress={() => setSelectedAsset(name)}
>
<Text>{name}</Text>
</TouchableOpacity>
))}
</View>
{/* Display Selected Asset */}
<View>
<Asset<AssetName> name={selectedAsset} size={64} />
<Text>Asset: {selectedAsset}</Text>
<Text>Exists: {assetInfo.exists ? 'Yes' : 'No'}</Text>
<Text>Is SVG: {assetInfo.isSvg ? 'Yes' : 'No'}</Text>
</View>
{/* Preloader Controls */}
<View>
<TouchableOpacity
onPress={handlePreloadSelected}
disabled={isLoading}
>
<Text>Preload Selected</Text>
</TouchableOpacity>
<Text>
Progress: {progress.loaded} / {progress.total} ({progress.percentage}%)
</Text>
{isLoading && <Text>Loading...</Text>}
{error && <Text>Error: {error.message}</Text>}
</View>
{/* Remote Assets */}
<View>
<Text>Remote Assets:</Text>
{REMOTE_ASSET_URLS.map((url) => (
<View key={url}>
<Asset name={url} size={48} />
<Text>{isRemoteUrl(url) ? 'Remote' : 'Local'}</Text>
</View>
))}
<TouchableOpacity onPress={handlePreloadRemoteAssets}>
<Text>Preload Remote Assets</Text>
</TouchableOpacity>
</View>
{/* Asset Grid */}
<View>
{Assets.getAllAssetNames().map((name) => (
<View key={name}>
<Asset<AssetName> name={name} size={48} />
<Text>{name}</Text>
</View>
))}
</View>
</ScrollView>
);
}The <Asset /> component is the main way to render your assets:
<Asset
name="images/logo" // Asset name (type-safe)
size={24} // Size as number (square) or { width, height }
style={styles.customStyle} // Additional styles
tintColor="#FF0000" // Tint color (for SVG icons)
resizeMode="contain" // Image resize mode
variant="dark" // Variant (default, dark, light)
testID="logo" // Test ID for testing
/>Use predefined size constants:
import { Asset, AssetSizes } from '@weprodev/react-native-smart-assets';
<Asset name="icons/home" size={AssetSizes.medium} />
<Asset name="icons/user" size={AssetSizes.large} />Available presets: xs, sm, md, lg, xl, xxl, small, medium, large, xlarge
Load remote images directly:
<Asset
name="https://example.com/image.png"
size={200}
/>Preload critical assets on app start:
import { useAssetPreloader } from '@weprodev/react-native-smart-assets';
function App() {
const { preload, progress, isLoading } = useAssetPreloader([
'images/logo',
'images/background',
]);
useEffect(() => {
preload();
}, []);
if (isLoading) {
return <Text>Loading assets: {progress.percentage}%</Text>;
}
return <YourApp />;
}Automatically resolves the correct asset variant for the current system color scheme.
Follows the existing -dark / -light filename convention with zero manual logic.
import { useAssetTheme } from '@weprodev/react-native-smart-assets';
function Logo() {
// Returns 'images/logo-dark' in dark mode, 'images/logo' in light mode.
// Falls back to 'images/logo' gracefully if the -dark variant isn't registered.
const { name } = useAssetTheme('images/logo');
return <Asset name={name} size={100} />;
}Options:
const { name, colorScheme, isDarkVariant } = useAssetTheme('images/logo', {
darkSuffix: '-dark', // default — suffix appended in dark mode
lightSuffix: '-light', // optional — also switch the asset in light mode
colorScheme: 'dark', // optional — force a scheme (e.g. Storybook)
});| Return value | Type | Description |
|---|---|---|
name |
string |
Resolved asset name to pass to <Asset /> |
colorScheme |
'light' | 'dark' |
Currently active scheme |
isDarkVariant |
boolean |
Whether the dark variant was found and used |
isLightVariant |
boolean |
Whether the light variant was found and used |
Tip: When the themed variant does not exist in the registry the hook silently falls back to the base name, so
<Asset />shows its own warning.
Show a visual skeleton while an image loads. No effect on SVG assets.
<Asset
name="images/hero"
size={{ width: 300, height: 200 }}
placeholder="shimmer" // 'shimmer' | 'blur' | 'color' | 'none'
placeholderColor="#DDE3EC" // optional base color (default: '#E0E0E0')
/>| Placeholder | Description |
|---|---|
shimmer |
Animated bright-band sweep — classic skeleton loading look |
blur |
Soft pulsing opacity — suggests hazy content beneath |
color |
Static flat background — zero animation overhead |
none |
No placeholder (default, preserves existing behaviour) |
The placeholder disappears automatically once onLoad or onError fires.
Swapping the name prop resets the loading state instantly.
You can also use the standalone <AssetPlaceholder /> component:
import { AssetPlaceholder } from '@weprodev/react-native-smart-assets';
<AssetPlaceholder type="shimmer" color="#DDE3EC" style={styles.skeleton} />Get asset information:
import { useAsset } from '@weprodev/react-native-smart-assets';
function MyComponent() {
const { asset, exists, isSvg } = useAsset('images/logo');
if (!exists) {
return <Text>Asset not found</Text>;
}
return <Asset name="images/logo" size={100} />;
}npx @weprodev/react-native-smart-assets generateOptions:
--assets-dir <path>: Custom assets directory (default:assets)--output-dir <path>: Custom output directory (default:assets)--format <format>: Output format:typescriptorjavascript(default:typescript)
npx @weprodev/react-native-smart-assets validateChecks for:
- Missing files
- Invalid file formats
- Duplicate asset names
- Missing variants
npx @weprodev/react-native-smart-assets statsShows:
- Total asset count
- Images vs SVG breakdown
- Assets with variants
- Category distribution
npx @weprodev/react-native-smart-assets watchAutomatically regenerates the asset registry when files change.
Compress PNG and JPEG files directly from the CLI and warn about oversized assets:
npx @weprodev/react-native-smart-assets optimizeExample output:
✓ Compressed icons/logo.png: 240KB → 48KB (-80%)
✓ Compressed images/background.jpg: 1.2MB → 310KB (-74%)
⚠ images/hero.jpg is 2.4MB after compression — recommended max is 512KB
Saved 1.4MB across 2 file(s)
Options:
--quality <0-100>: JPEG / PNG quality (default:80)--max-size <bytes>: Warn when an asset exceeds this size (default:524288= 512 KB)--dry-run: Preview savings without writing any files--assets-dir <path>: Custom assets directory (default:assets)
Note: Real compression requires
sharp. Without it the command still runs in analysis-only mode and reports oversized assets, so it's always safe to run in CI:npm install --save-dev sharp # or yarn add --dev sharp
Create an assets.config.js file in your project root:
module.exports = {
assetsDir: 'assets',
outputDir: 'assets',
format: 'typescript',
};Automatically regenerates the asset registry on every expo prebuild so you never forget to run generate after adding new assets.
// app.config.js
export default {
name: 'MyApp',
slug: 'my-app',
plugins: [
[
'@weprodev/react-native-smart-assets/plugin',
{
assetsDir: './src/assets', // where your raw assets live
outputDir: './src/assets', // where the generated index.ts is written
},
],
],
};TypeScript config (app.config.ts):
import type { ExpoConfig } from 'expo/config';
import type { SmartAssetsPluginOptions } from '@weprodev/react-native-smart-assets/plugin';
const pluginOptions: SmartAssetsPluginOptions = {
assetsDir: './src/assets',
outputDir: './src/assets',
};
const config: ExpoConfig = {
name: 'MyApp',
slug: 'my-app',
plugins: [['@weprodev/react-native-smart-assets/plugin', pluginOptions]],
};
export default config;Run prebuild as usual — generation happens automatically:
npx expo prebuildYou'll see this in the output:
[@weprodev/react-native-smart-assets] Running asset registry generation…
assetsDir : /your/project/src/assets
outputDir : /your/project/src/assets
✓ [@weprodev/react-native-smart-assets] Registry generated — 12 asset(s)
Output: /your/project/src/assets/index.ts
| Option | Type | Default | Description |
|---|---|---|---|
assetsDir |
string |
'./assets' |
Directory containing your raw asset files |
outputDir |
string |
Same as assetsDir |
Directory where the generated registry is written |
format |
'typescript' | 'javascript' |
'typescript' |
Output file format |
failOnError |
boolean |
false |
true = hard-fail the build on errors (recommended for CI) |
Validates asset names at build time instead of runtime. Typos in <Asset name="…" /> or getAsset("…") surface as build errors with helpful suggestions — before the app even launches.
// ❌ Build error: Asset "icons/hom" not found.
// Did you mean "icons/home"?
<Asset name="icons/hom" size={24} />
// ✅ Fine
<Asset name="icons/home" size={24} />Add the plugin to your babel.config.js:
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'@weprodev/react-native-smart-assets/babel-plugin',
{
registryPath: './src/assets/index.ts', // path to generated registry
mode: 'error', // 'error' | 'warn' | 'off'
},
],
],
};After changing babel.config.js clear Metro's cache once:
npx expo start --clear
# or
npx react-native start --reset-cache| Pattern | Example |
|---|---|
JSX name prop |
<Asset name="icons/hom" /> |
getAsset() first argument |
getAsset('images/lgoo') |
hasAsset() first argument |
hasAsset('icons/ghost') |
Dynamic values (e.g. name={myVar}) are silently skipped — only string literals are checked.
| Option | Type | Default | Description |
|---|---|---|---|
registryPath |
string |
'./assets/index.ts' |
Path to the generated asset registry |
mode |
'error' | 'warn' | 'off' |
'error' |
How to report unknown asset names |
assetComponents |
string[] |
['Asset'] |
JSX components whose name prop is validated |
assetFunctions |
string[] |
['getAsset', 'hasAsset'] |
Function calls whose first argument is validated |
// babel.config.js
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'@weprodev/react-native-smart-assets/babel-plugin',
{
registryPath: './src/assets/index.ts',
mode: isDev ? 'warn' : 'error', // warn locally, hard-fail in CI
},
],
],
};plugins: [
['@weprodev/react-native-smart-assets/babel-plugin', {
registryPath: './src/assets/index.ts',
mode: 'error',
assetComponents: ['Asset', 'AppIcon', 'SmartImage'],
assetFunctions: ['getAsset', 'hasAsset', 'useAsset'],
}],
],expo prebuild ← Expo plugin auto-generates the registry
↓
Metro bundler starts ← Babel plugin reads the fresh registry
↓
<Asset name="typo"> ← caught immediately as a build error
For local development, you can also keep the file watcher running:
# Terminal 1 — regenerate registry whenever assets change
npx @weprodev/react-native-smart-assets watch --assets-dir ./src/assets
# Terminal 2 — normal Metro dev server
npx expo startReact Native automatically picks the right density variant:
assets/
├── icon.png (1x)
├── icon@2x.png (2x)
└── icon@3x.png (3x)
Use -dark suffix for dark mode assets:
assets/
├── icon.svg
└── icon-dark.svg
Use platform-specific extensions:
assets/
├── icon.ios.png
└── icon.android.png
Main component for rendering assets.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Asset name (type-safe from generated types) |
size |
number | { width, height } |
— | Asset dimensions |
style |
StyleProp<ImageStyle> |
— | Additional styles |
tintColor |
string |
— | Tint color (SVG only) |
resizeMode |
ImageResizeMode |
'contain' |
Image resize mode |
variant |
'default' | 'dark' | 'light' |
'default' |
Asset variant |
placeholder |
'shimmer' | 'blur' | 'color' | 'none' |
'none' |
Loading placeholder style |
placeholderColor |
string |
'#E0E0E0' |
Placeholder base color |
testID |
string |
— | Test identifier |
Standalone skeleton component, useful when you need a placeholder outside of <Asset />.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
type |
'shimmer' | 'blur' | 'color' | 'none' |
— | Visual style |
color |
string |
'#E0E0E0' |
Base color |
style |
ViewStyle |
— | Additional styles |
testID |
string |
— | Test identifier |
Returns asset information.
Returns:
asset: any— The raw asset objectexists: boolean— Whether the asset is registeredisSvg: boolean— Whether the asset is an SVG
Resolves the correct asset variant for the current system color scheme.
Options:
colorScheme?: 'light' | 'dark'— Override the detected schemedarkSuffix?: string— Suffix for dark variants (default:'-dark')lightSuffix?: string— Suffix for light variants (default:undefined)
Returns:
name: string— Resolved asset name (ready to pass to<Asset />)colorScheme: 'light' | 'dark'— Currently active schemeisDarkVariant: boolean— Whether the dark variant was resolvedisLightVariant: boolean— Whether the light variant was resolved
Preloads assets with progress tracking.
Returns:
preload: (names?: string[]) => Promise<void>- Preload functionprogress: { loaded: number; total: number; percentage: number }- Progress infoisLoading: boolean- Loading stateerror: Error | null- Error if any
Initialize the asset registry with generated assets.
Parameters:
registry: AssetRegistry- The asset registry object (typicallyAssets.ASSETS)metadata?: AssetMetadataMap- Optional metadata object (typicallyAssets.ASSET_METADATA) containing asset information like type, category, etc.
Get an asset by name.
Check if an asset exists.
Get all registered asset names.
Predefined size constants.
Get a size preset value.
Calculate responsive size.
The generated assets/index.ts file includes:
AssetNametype with all your asset namesASSETSconstant with all asset importsASSET_METADATAwith asset information- Helper functions for type-safe asset access
import type { SmartAssetsPluginOptions } from '@weprodev/react-native-smart-assets/plugin';
import type { BabelPluginOptions } from '@weprodev/react-native-smart-assets/babel-plugin';See the Contributing Guide for details.
MIT
Made with ❤️ for the React Native community