Asset
Turn require('./logo.png') (or a plain URI) into a file you can hand to anything that wants a
local path: download it once, read localUri, reuse it. @symbiote-native/asset wraps
expo-asset so every SymbioteNative
adapter can do it, not just React. It exists mainly so
@symbiote-native/font can accept the same number and Asset font
sources as upstream expo-font, and works standalone too.
| OS platform | Support |
|---|---|
| iOS | live |
| Android | live |
| Framework adapter | Support |
|---|---|
| React | live |
| Vue | live |
| Angular | live |
| Svelte | live |
| Solid | live |
Installation
Section titled “Installation”npm install @symbiote-native/assetScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --asset (or
add --asset in an existing app) installs and wires this for you - see
@symbiote-native/cli.
expo-asset and expo-modules-core come along as regular dependencies, pinned to exact versions.
Never install either yourself, and never add the expo meta-package to your project (it bundles
its own Metro/Babel pipeline, which conflicts with this project’s own).
Each adapter wraps Asset.loadAsync and loads once on mount, matching upstream’s useAssets
contract. assets stays empty until the download finishes.
import { useAssets } from '@symbiote-native/asset/react';
export function Logo() { const [assets, error] = useAssets(require('./assets/logo.png')); return assets ? <image source={{ uri: assets[0].localUri ?? undefined }} /> : null;}<script setup lang="ts">import { useAssets } from '@symbiote-native/asset/vue';
const { assets, error } = useAssets(require('./assets/logo.png'));</script>
<template> <image v-if="assets" :source="{ uri: assets[0].localUri ?? undefined }" /></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { AssetsService } from '@symbiote-native/asset/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: `@if (assets.assets(); as loaded) { <image [source]="{ uri: loaded[0].localUri ?? undefined }" /> }`,})export class Logo { readonly assets = inject(AssetsService).connect(require('./assets/logo.png'));}connect() returns a pair of signals (assets and error).
<script lang="ts"> import { useAssets } from '@symbiote-native/asset/svelte';
const { assets, error } = useAssets(require('./assets/logo.png'));</script>
{#if assets} <image source={{ uri: assets[0].localUri ?? undefined }} />{/if}import { createAssets } from '@symbiote-native/asset/solid';
export function Logo() { const { assets } = createAssets(require('./assets/logo.png'));
return assets() ? ( <image source={{ uri: assets()?.[0]?.localUri ?? undefined }} /> ) : null;}Solid reserves use* for consuming existing state, so the primitive is createAssets. Call
the accessor (assets()): a Solid component body runs once.
Outside a component, use the Asset class directly:
import { Asset } from '@symbiote-native/asset';
const [logo] = await Asset.loadAsync(require('./assets/logo.png'));console.log(logo.localUri); // file:// path in the cache| Signature | Description |
|---|---|
Asset.fromModule(moduleIdOrUriOrSource): Asset |
Builds an Asset from a require() module id, a URI, or an object with uri, width, height |
Asset.fromMetadata(meta): Asset |
Builds an Asset from bundler metadata (AssetMetadata) |
Asset.fromURI(uri): Asset |
Builds an Asset for a plain URI, without downloading it |
Asset.loadAsync(moduleId): Promise<Asset[]> |
Resolves one or many module ids (or URIs) and downloads each. Resolves the assets, in order |
asset.downloadAsync(): Promise<Asset> |
Downloads this asset to a local cache file and sets localUri. Resolves the same asset |
useAssets / createAssets / AssetsService
Section titled “useAssets / createAssets / AssetsService”| Adapter | Entry point | Description |
|---|---|---|
| React | useAssets(moduleIds) |
Returns [assets: Asset[] | undefined, error: Error | undefined] |
| Vue | useAssets(moduleIds) |
Returns { assets, error } as refs |
| Svelte | useAssets(moduleIds) |
Returns an object with reactive assets and error |
| Solid | createAssets(moduleIds) |
Returns { assets(), error() } as accessors |
| Angular | inject(AssetsService).connect(moduleIds) |
Returns { assets, error } as signals |
The hook entry points accept number | number[] only, matching upstream’s own hook (narrower than
Asset.loadAsync, which also takes strings).
localUriisnulluntil the asset is downloaded. Read it afterloadAsyncordownloadAsyncresolves, and pass?? undefinedwhere an image source expectsstring | undefined.useAssetsdoes not reload. Like upstream, it loads once on mount; passing a different module id later has no effect.- Expo Go and
expo-updatescode paths are ported but inert.getLocalAssetUri, the manifest2 dev-server resolution and theImagesource transformer are real, tested code. This repo ships neither Expo Go norexpo-updates, soIS_ENV_WITH_LOCAL_ASSETSisfalse. If your app installsexpo-updatesitself, this package picks it up, same as upstream. - The
expo-assetconfig plugin is not ported. It links files into the native project at build time viaapp.json; this repo runs no Expo prebuild step. Bundle files through Metro withrequire(), and add unusual extensions (such as.lottie) toassetExtsin your Metro config. - The web platform is not ported. Only the native path exists, as with every package here.
Common questions
Section titled “Common questions”asset.localUri is null. It stays null until the asset is downloaded. Read it after
Asset.loadAsync(...) or asset.downloadAsync() resolves, or let useAssets do the wait for you.
How do I preload several assets before showing a screen? Pass them all to one call:
await Asset.loadAsync([require('./a.png'), require('./b.png')]). It resolves the assets in order.
Where does the downloaded file go? A file in the app’s cache directory, so the OS may remove it later. Do not store anything there you cannot re-create.
Does the hook reload when I change the module id? No. It loads once on mount, like upstream. Mount a new component (or use a key) for a different asset.
Sources: Expo docs: Asset, expo/expo#8222 downloadAsync should not download if file exists.
How the wrapper works
Section titled “How the wrapper works”expo-asset’s JS is hand-ported into this package’s core/, resolving ExpoAsset through
expo-modules-core rather than the expo meta-package:
packages/asset/src/|-- core/ # asset.ts, asset-sources.ts, asset-uris.ts, local-assets.ts, platform-utils.ts|-- react/ # hooks/ useAssets|-- vue/ # composables/ useAssets|-- svelte/ # runes/ useAssets|-- solid/ # primitives/ createAssets`-- angular/ # services/ AssetsServiceEach adapter file is lifecycle only. The native code is never vendored: expo-modules-autolinking
resolves it from node_modules (see the native setup guide).