react-native-fast-image 10.6.0 → 10.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,7 +101,7 @@ const YourImage = () => (
101
101
 
102
102
  ### `FastImage`
103
103
 
104
- It's a single native view, which also takes React Native's View props, such as accessibility props, `testID`, `nativeID`, `onLayout`, `pointerEvents` and `hitSlop`. A ref to it is that view (e.g. for `measure`). It doesn't take children: use [`FastImageBackground`](#fastimagebackground) for content over an image.
104
+ It's a single native view, which also takes React Native's View props, such as accessibility props, `testID`, `nativeID`, `onLayout`, `pointerEvents` and `hitSlop`. A ref to it is that view (e.g. for `measure`), with [`reload()`](#refreload). It doesn't take children: use [`FastImageBackground`](#fastimagebackground) for content over an image.
105
105
 
106
106
  <!-- api:props start (generated from src/ by website/scripts/generate.mts) -->
107
107
 
@@ -537,6 +537,27 @@ If supplied, changes the color of all the non-transparent pixels to the given co
537
537
 
538
538
  <!-- api:props end -->
539
539
 
540
+ ---
541
+
542
+ #### `ref.reload()`
543
+
544
+ Loads the image again, e.g. to try again after `onError`. The image showing stays until the new one has loaded (or the placeholder, if it failed), and the events are sent as for a new source. An image in the cache loads from it. In TypeScript, the ref's type is `FastImageRef`.
545
+
546
+ ```tsx
547
+ const image = useRef<FastImageRef>(null)
548
+
549
+ <FastImage ref={image} source={{ uri }} onError={() => setFailed(true)} />
550
+ {failed && (
551
+ <Button
552
+ title="Try again"
553
+ onPress={() => {
554
+ setFailed(false)
555
+ image.current?.reload()
556
+ }}
557
+ />
558
+ )}
559
+ ```
560
+
540
561
  ### `FastImageBackground`
541
562
 
542
563
  An image with content on top of it: a view that the image fills, with the children on top.
@@ -577,7 +598,7 @@ The image's style.
577
598
 
578
599
  #### `imageRef`
579
600
 
580
- **Type:** `Ref<any>`
601
+ **Type:** `Ref<FastImageRef>`
581
602
 
582
603
  A ref to the image (the FastImage inside).
583
604
 
@@ -16,7 +16,6 @@ import com.bumptech.glide.load.model.LazyHeaders;
16
16
  import com.bumptech.glide.request.RequestOptions;
17
17
  import com.bumptech.glide.signature.ApplicationVersionSignature;
18
18
  import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
19
- import com.facebook.react.bridge.NoSuchKeyException;
20
19
  import com.facebook.react.bridge.ReadableMap;
21
20
  import com.facebook.react.bridge.ReadableMapKeySetIterator;
22
21
  import com.facebook.react.bridge.ReadableType;
@@ -34,6 +33,10 @@ class FastImageViewConverter {
34
33
  put("immutable", FastImageCacheControl.IMMUTABLE);
35
34
  put("web", FastImageCacheControl.WEB);
36
35
  put("cacheOnly", FastImageCacheControl.CACHE_ONLY);
36
+ // React Native's ImageURISource value, which views take. Its
37
+ // others (e.g. 'reload') are immutable, as any value this map
38
+ // doesn't have (see getValueFromSource).
39
+ put("only-if-cached", FastImageCacheControl.CACHE_ONLY);
37
40
  }};
38
41
 
39
42
  private static final Map<String, Priority> FAST_IMAGE_PRIORITY_MAP =
@@ -189,13 +192,12 @@ class FastImageViewConverter {
189
192
  return value;
190
193
  }
191
194
 
195
+ // A value the map doesn't have (e.g. React Native's `cache: 'reload'`, or
196
+ // one that isn't a string) gets the default, as on iOS.
192
197
  private static <T> T getValueFromSource(String propName, String defaultProp, Map<String, T> map, ReadableMap source) {
193
- String propValue;
194
- try {
195
- propValue = source != null ? source.getString(propName) : null;
196
- } catch (NoSuchKeyException e) {
197
- propValue = null;
198
- }
199
- return getValue(propName, defaultProp, map, propValue);
198
+ T value = source != null && source.hasKey(propName) && source.getType(propName) == ReadableType.String
199
+ ? map.get(source.getString(propName))
200
+ : null;
201
+ return value != null ? value : map.get(defaultProp);
200
202
  }
201
203
  }
@@ -435,6 +435,12 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
435
435
  view.setObjectFit(FastImageViewConverter.getScaleType(objectFit));
436
436
  }
437
437
 
438
+ // The ref's reload() (a command).
439
+ @Override
440
+ public void reload(FastImageViewWithUrl view) {
441
+ view.reload();
442
+ }
443
+
438
444
  @Override
439
445
  public void onDropViewInstance(@NonNull FastImageViewWithUrl view) {
440
446
  // Cancels its request, and nothing loads or tracks progress for it
@@ -364,6 +364,15 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
364
364
  return true;
365
365
  }
366
366
 
367
+ // Loads the image again, as a new source would, keeping the image showing
368
+ // until then (the ref's reload()). Before the first props there's nothing
369
+ // to load yet.
370
+ void reload() {
371
+ if (mManager == null) return;
372
+ mNeedsReload = true;
373
+ onAfterUpdate(mManager, requestManager);
374
+ }
375
+
367
376
  // Shows at once, as a new source placeholder does (see updatePlaceholder),
368
377
  // without loading the image again.
369
378
  public void setPlaceholder(@Nullable Drawable placeholder) {
@@ -0,0 +1,2 @@
1
+ import type React from 'react';
2
+ export declare function composeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T>;
package/dist/index.cjs.js CHANGED
@@ -255,6 +255,7 @@ function objectPositionOrCentered(position) {
255
255
  }
256
256
  //#endregion
257
257
  //#region src/specs/FastImageViewNativeComponent.ts
258
+ const Commands = (0, react_native.codegenNativeCommands)({ supportedCommands: ["reload"] });
258
259
  var FastImageViewNativeComponent_default = (0, react_native.codegenNativeComponent)("FastImageView");
259
260
  //#endregion
260
261
  //#region src/specs/NativeFastImageModule.ts
@@ -279,6 +280,32 @@ function dataUri(value) {
279
280
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
280
281
  }
281
282
  //#endregion
283
+ //#region src/composeRefs.ts
284
+ function composeRefs(...refs) {
285
+ return (value) => {
286
+ const cleanups = refs.map((ref) => {
287
+ if (typeof ref === "function") {
288
+ const cleanup = ref(value);
289
+ return typeof cleanup === "function" ? cleanup : () => {
290
+ ref(null);
291
+ };
292
+ }
293
+ if (ref) {
294
+ ref.current = value;
295
+ return () => {
296
+ ref.current = null;
297
+ };
298
+ }
299
+ });
300
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
301
+ };
302
+ }
303
+ //#endregion
304
+ //#region src/reactNativeCache.ts
305
+ function warnIfReload(source) {
306
+ if (typeof __DEV__ !== "undefined" && __DEV__ && (source === null || source === void 0 ? void 0 : source.cache) === "reload") warnOnce("cache-reload", "react-native-fast-image: `cache: 'reload'` loads the image as the default, `'immutable'`: it isn't loaded again each time. For an image that changes at the same url, use `cache: 'web'` (it follows the server's cache headers), or change its `cacheKey`.", "warn");
307
+ }
308
+ //#endregion
282
309
  //#region src/index.tsx
283
310
  const resolvePlaceholder = (placeholder) => {
284
311
  if (typeof placeholder === "string") {
@@ -352,14 +379,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
352
379
  const fit = resolveObjectFit(objectFit, style);
353
380
  const position = objectPositionOrCentered(objectPosition);
354
381
  const resolvedSource = (0, react.useMemo)(() => {
355
- for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
382
+ for (const entry of [source].flat()) {
383
+ dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
384
+ warnIfReload(entry);
385
+ }
356
386
  const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
357
387
  return Array.isArray(single) ? single.map((entry) => react_native.Image.resolveAssetSource(entry)) : react_native.Image.resolveAssetSource(single);
358
388
  }, [source]);
359
389
  const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
390
+ const ref = (0, react.useMemo)(() => composeRefs(addReload, forwardedRef), [forwardedRef]);
360
391
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
361
392
  ...props,
362
- ref: forwardedRef,
393
+ ref,
363
394
  style: [styles.image, style],
364
395
  recyclingKey: (_props$recyclingKey = props.recyclingKey) !== null && _props$recyclingKey !== void 0 ? _props$recyclingKey : void 0,
365
396
  loopCount: loopCount(loop),
@@ -386,6 +417,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
386
417
  objectPositionYOffset: position.y.offset
387
418
  });
388
419
  }
420
+ function addReload(view) {
421
+ if (view) view.reload = () => Commands.reload(view);
422
+ }
389
423
  const FastImageMemo = (0, react.memo)(FastImageBase);
390
424
  const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
391
425
  forwardedRef: ref,
@@ -137,6 +137,15 @@ export type FastImageSource = {
137
137
  scale?: number,
138
138
  }
139
139
 
140
+ // React Native's ImageURISource cache values, which the views' `source` takes
141
+ // so a React Native source works as it is: native reads 'only-if-cached' as
142
+ // 'cacheOnly', and the others as the default, 'immutable'.
143
+ type ReactNativeCache = 'default' | 'reload' | 'force-cache' | 'only-if-cached'
144
+ type ViewSource = {
145
+ ...FastImageSource,
146
+ cache?: CacheControls | ReactNativeCache,
147
+ }
148
+
140
149
  export type FastImageProps = $ReadOnly<{|
141
150
  ...ViewProps,
142
151
  onError?: ?(event: OnErrorEvent) => void,
@@ -145,7 +154,7 @@ export type FastImageProps = $ReadOnly<{|
145
154
  onLoadStart?: ?() => void,
146
155
  onProgress?: ?(event: OnProgressEvent) => void,
147
156
 
148
- source?: ?(FastImageSource | Array<FastImageSource | number> | number),
157
+ source?: ?(ViewSource | Array<ViewSource | number> | number),
149
158
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
159
  // Deprecated: use placeholder, which is the same.
151
160
  defaultSource?: ?number,
@@ -196,4 +205,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
205
  source: FastImageSource,
197
206
  file: string,
198
207
  ) => Promise<CachePathResult>;
208
+ // On a ref: loads the image again, e.g. to try again after onError.
209
+ reload(): void;
199
210
  }
@@ -282,6 +282,27 @@ function dataUri(value) {
282
282
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
283
283
  }
284
284
  //#endregion
285
+ //#region src/composeRefs.ts
286
+ function composeRefs(...refs) {
287
+ return (value) => {
288
+ const cleanups = refs.map((ref) => {
289
+ if (typeof ref === "function") {
290
+ const cleanup = ref(value);
291
+ return typeof cleanup === "function" ? cleanup : () => {
292
+ ref(null);
293
+ };
294
+ }
295
+ if (ref) {
296
+ ref.current = value;
297
+ return () => {
298
+ ref.current = null;
299
+ };
300
+ }
301
+ });
302
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
303
+ };
304
+ }
305
+ //#endregion
285
306
  //#region src/index.web.tsx
286
307
  const notSupported = {
287
308
  ok: false,
@@ -298,10 +319,15 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
298
319
  const src = sized ? sized.src : resolveUri(source);
299
320
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
300
321
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
322
+ const [reloads, setReloads] = (0, react.useState)(0);
323
+ const ref = (0, react.useMemo)(() => composeRefs((view) => {
324
+ if (view) view.reload = () => setReloads((count) => count + 1);
325
+ }, forwardedRef), [forwardedRef]);
301
326
  const priority = sourcePriority(source);
302
327
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
303
328
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
304
329
  const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
330
+ const placeholderKey = `${urlPlaceholder} ${reloads}`;
305
331
  const [failedPlaceholder, setFailedPlaceholder] = (0, react.useState)();
306
332
  const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
307
333
  const [result, setResult] = (0, react.useState)();
@@ -315,14 +341,14 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
315
341
  const showDefault = ok !== true;
316
342
  useClientLayoutEffect(() => {
317
343
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
318
- }, [key]);
344
+ }, [key, reloads]);
319
345
  return /* @__PURE__ */ react.default.createElement(react_native.View, {
320
346
  ...viewProps,
321
347
  "aria-label": label,
322
348
  style: [styles.container, style],
323
349
  onLayout,
324
350
  pointerEvents,
325
- ref: forwardedRef
351
+ ref
326
352
  }, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
327
353
  key: `default ${defaultUri}`,
328
354
  src: defaultUri,
@@ -334,7 +360,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
334
360
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
335
361
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
336
362
  }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
337
- key: `placeholder ${urlPlaceholder}`,
363
+ key: `placeholder ${placeholderKey}`,
338
364
  src: urlPlaceholder,
339
365
  loading,
340
366
  priority: "high",
@@ -342,11 +368,11 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
342
368
  fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
343
369
  position,
344
370
  filter: tintColor ? `url(#${tintId})` : void 0,
345
- hidden: failedPlaceholder === urlPlaceholder,
346
- onError: () => setFailedPlaceholder(urlPlaceholder),
371
+ hidden: failedPlaceholder === placeholderKey,
372
+ onError: () => setFailedPlaceholder(placeholderKey),
347
373
  alt: ""
348
374
  }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
349
- key: several ? "sizes" : key,
375
+ key: `${several ? "sizes" : key} ${reloads}`,
350
376
  src,
351
377
  srcSet,
352
378
  sizes,
@@ -604,11 +630,10 @@ FastImage.setDefaults = () => {};
604
630
  FastImage.writeToCache = () => Promise.resolve(notSupported);
605
631
  FastImage.getCachePath = () => Promise.resolve(notSupported);
606
632
  const styles = react_native.StyleSheet.create({ container: { overflow: "hidden" } });
607
- const FastImageWithRef = FastImageComponent;
608
633
  const FastImageBackground = (0, react.forwardRef)(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ react.default.createElement(react_native.View, {
609
634
  style,
610
635
  ref
611
- }, /* @__PURE__ */ react.default.createElement(FastImageWithRef, {
636
+ }, /* @__PURE__ */ react.default.createElement(FastImageComponent, {
612
637
  ...props,
613
638
  style: [react_native.StyleSheet.absoluteFill, imageStyle],
614
639
  ref: imageRef
@@ -137,6 +137,15 @@ export type FastImageSource = {
137
137
  scale?: number,
138
138
  }
139
139
 
140
+ // React Native's ImageURISource cache values, which the views' `source` takes
141
+ // so a React Native source works as it is: native reads 'only-if-cached' as
142
+ // 'cacheOnly', and the others as the default, 'immutable'.
143
+ type ReactNativeCache = 'default' | 'reload' | 'force-cache' | 'only-if-cached'
144
+ type ViewSource = {
145
+ ...FastImageSource,
146
+ cache?: CacheControls | ReactNativeCache,
147
+ }
148
+
140
149
  export type FastImageProps = $ReadOnly<{|
141
150
  ...ViewProps,
142
151
  onError?: ?(event: OnErrorEvent) => void,
@@ -145,7 +154,7 @@ export type FastImageProps = $ReadOnly<{|
145
154
  onLoadStart?: ?() => void,
146
155
  onProgress?: ?(event: OnProgressEvent) => void,
147
156
 
148
- source?: ?(FastImageSource | Array<FastImageSource | number> | number),
157
+ source?: ?(ViewSource | Array<ViewSource | number> | number),
149
158
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
159
  // Deprecated: use placeholder, which is the same.
151
160
  defaultSource?: ?number,
@@ -196,4 +205,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
205
  source: FastImageSource,
197
206
  file: string,
198
207
  ) => Promise<CachePathResult>;
208
+ // On a ref: loads the image again, e.g. to try again after onError.
209
+ reload(): void;
199
210
  }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { ColorValue, View, StyleProp, ViewStyle, ImageRequireSource, AccessibilityProps, ViewProps } from 'react-native';
3
3
  import { cacheControl, priority, resizeMode } from './constants';
4
4
  import { type ObjectPosition } from './objectPosition';
5
+ import { type ReactNativeCache } from './reactNativeCache';
5
6
  /** How the image fits the view: see [`objectFit`](#objectfit). */
6
7
  export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
7
8
  export type { ObjectPosition, PositionLength } from './objectPosition';
@@ -152,6 +153,9 @@ export type Source = {
152
153
  /** With several sources, the scale `width` and `height` are multiplied by. */
153
154
  scale?: number;
154
155
  };
156
+ type ViewSource = Source | (Omit<Source, 'cache'> & {
157
+ cache?: ReactNativeCache;
158
+ });
155
159
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
156
160
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>;
157
161
  /** A url as `placeholder`, loaded like a source (see `placeholder`). */
@@ -334,7 +338,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
334
338
  * - A `require()`d image can be one of the sizes: its size is the file's.
335
339
  * - An array of one is the same as that source.
336
340
  */
337
- source?: Source | Array<Source | ImageRequireSource> | ImageRequireSource;
341
+ source?: ViewSource | Array<ViewSource | ImageRequireSource> | ImageRequireSource;
338
342
  /**
339
343
  * A placeholder shown while the image loads, and if it fails to load:
340
344
  *
@@ -664,6 +668,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
664
668
  children?: "FastImage doesn't take children: use FastImageBackground for content over an image";
665
669
  }
666
670
  type ViewRef = React.ComponentRef<typeof View>;
671
+ /**
672
+ * What a ref to `FastImage` gets: its native view, with a View's methods
673
+ * (e.g. `measure`), and `reload()`. E.g. `useRef<FastImageRef>(null)`.
674
+ */
675
+ export type FastImageRef = ViewRef & {
676
+ /**
677
+ * Loads the image again, e.g. to try again after `onError`. The image
678
+ * showing stays until the new one has loaded, and the events are sent as
679
+ * for a new source. An image in the cache loads from it.
680
+ */
681
+ reload(): void;
682
+ };
667
683
  /** A preloaded source that loaded (and is now cached). */
668
684
  export interface PreloadSuccess {
669
685
  /** The source's uri. */
@@ -762,14 +778,14 @@ export interface FastImageDefaults {
762
778
  */
763
779
  transition?: boolean | number | Transition | null;
764
780
  }
765
- declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<ViewRef>> & FastImageStaticProperties;
781
+ declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<FastImageRef>> & FastImageStaticProperties;
766
782
  export interface FastImageBackgroundProps extends Omit<FastImageProps, 'style' | 'children'> {
767
783
  /** The container's style; the image fills it. */
768
784
  style?: StyleProp<ViewStyle>;
769
785
  /** The image's style. */
770
786
  imageStyle?: StyleProp<ImageStyle>;
771
787
  /** A ref to the image (the FastImage inside). */
772
- imageRef?: React.Ref<any>;
788
+ imageRef?: React.Ref<FastImageRef>;
773
789
  /** Content shown on top of the image. */
774
790
  children?: React.ReactNode;
775
791
  }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { Children, forwardRef, memo, useMemo } from "react";
2
- import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeComponent } from "react-native";
2
+ import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeCommands, codegenNativeComponent } from "react-native";
3
3
  //#region src/constants.ts
4
4
  const resizeMode = {
5
5
  contain: "contain",
@@ -228,6 +228,7 @@ function objectPositionOrCentered(position) {
228
228
  }
229
229
  //#endregion
230
230
  //#region src/specs/FastImageViewNativeComponent.ts
231
+ const Commands = codegenNativeCommands({ supportedCommands: ["reload"] });
231
232
  var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView");
232
233
  //#endregion
233
234
  //#region src/specs/NativeFastImageModule.ts
@@ -252,6 +253,32 @@ function dataUri(value) {
252
253
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
253
254
  }
254
255
  //#endregion
256
+ //#region src/composeRefs.ts
257
+ function composeRefs(...refs) {
258
+ return (value) => {
259
+ const cleanups = refs.map((ref) => {
260
+ if (typeof ref === "function") {
261
+ const cleanup = ref(value);
262
+ return typeof cleanup === "function" ? cleanup : () => {
263
+ ref(null);
264
+ };
265
+ }
266
+ if (ref) {
267
+ ref.current = value;
268
+ return () => {
269
+ ref.current = null;
270
+ };
271
+ }
272
+ });
273
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
274
+ };
275
+ }
276
+ //#endregion
277
+ //#region src/reactNativeCache.ts
278
+ function warnIfReload(source) {
279
+ if (typeof __DEV__ !== "undefined" && __DEV__ && (source === null || source === void 0 ? void 0 : source.cache) === "reload") warnOnce("cache-reload", "react-native-fast-image: `cache: 'reload'` loads the image as the default, `'immutable'`: it isn't loaded again each time. For an image that changes at the same url, use `cache: 'web'` (it follows the server's cache headers), or change its `cacheKey`.", "warn");
280
+ }
281
+ //#endregion
255
282
  //#region src/index.tsx
256
283
  const resolvePlaceholder = (placeholder) => {
257
284
  if (typeof placeholder === "string") {
@@ -325,14 +352,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
325
352
  const fit = resolveObjectFit(objectFit, style);
326
353
  const position = objectPositionOrCentered(objectPosition);
327
354
  const resolvedSource = useMemo(() => {
328
- for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
355
+ for (const entry of [source].flat()) {
356
+ dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
357
+ warnIfReload(entry);
358
+ }
329
359
  const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
330
360
  return Array.isArray(single) ? single.map((entry) => Image.resolveAssetSource(entry)) : Image.resolveAssetSource(single);
331
361
  }, [source]);
332
362
  const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
363
+ const ref = useMemo(() => composeRefs(addReload, forwardedRef), [forwardedRef]);
333
364
  return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
334
365
  ...props,
335
- ref: forwardedRef,
366
+ ref,
336
367
  style: [styles.image, style],
337
368
  recyclingKey: (_props$recyclingKey = props.recyclingKey) !== null && _props$recyclingKey !== void 0 ? _props$recyclingKey : void 0,
338
369
  loopCount: loopCount(loop),
@@ -359,6 +390,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
359
390
  objectPositionYOffset: position.y.offset
360
391
  });
361
392
  }
393
+ function addReload(view) {
394
+ if (view) view.reload = () => Commands.reload(view);
395
+ }
362
396
  const FastImageMemo = memo(FastImageBase);
363
397
  const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
364
398
  forwardedRef: ref,
@@ -137,6 +137,15 @@ export type FastImageSource = {
137
137
  scale?: number,
138
138
  }
139
139
 
140
+ // React Native's ImageURISource cache values, which the views' `source` takes
141
+ // so a React Native source works as it is: native reads 'only-if-cached' as
142
+ // 'cacheOnly', and the others as the default, 'immutable'.
143
+ type ReactNativeCache = 'default' | 'reload' | 'force-cache' | 'only-if-cached'
144
+ type ViewSource = {
145
+ ...FastImageSource,
146
+ cache?: CacheControls | ReactNativeCache,
147
+ }
148
+
140
149
  export type FastImageProps = $ReadOnly<{|
141
150
  ...ViewProps,
142
151
  onError?: ?(event: OnErrorEvent) => void,
@@ -145,7 +154,7 @@ export type FastImageProps = $ReadOnly<{|
145
154
  onLoadStart?: ?() => void,
146
155
  onProgress?: ?(event: OnProgressEvent) => void,
147
156
 
148
- source?: ?(FastImageSource | Array<FastImageSource | number> | number),
157
+ source?: ?(ViewSource | Array<ViewSource | number> | number),
149
158
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
159
  // Deprecated: use placeholder, which is the same.
151
160
  defaultSource?: ?number,
@@ -196,4 +205,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
205
  source: FastImageSource,
197
206
  file: string,
198
207
  ) => Promise<CachePathResult>;
208
+ // On a ref: loads the image again, e.g. to try again after onError.
209
+ reload(): void;
199
210
  }
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
- import type { FastImageBackgroundProps, FastImageProps, FastImageStaticProperties } from './index';
2
+ import { View } from 'react-native';
3
+ import type { FastImageBackgroundProps, FastImageProps, FastImageRef, FastImageStaticProperties } from './index';
3
4
  export type * from './index';
4
- declare const FastImage: React.ComponentType<FastImageProps> & FastImageStaticProperties;
5
- export declare const FastImageBackground: React.ComponentType<FastImageBackgroundProps>;
5
+ type ViewRef = React.ComponentRef<typeof View>;
6
+ declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<FastImageRef>> & FastImageStaticProperties;
7
+ export declare const FastImageBackground: React.ForwardRefExoticComponent<FastImageBackgroundProps & React.RefAttributes<ViewRef>>;
6
8
  export default FastImage;
package/dist/index.web.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
1
+ import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { Image, PixelRatio, StyleSheet, View } from "react-native";
3
3
  import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
4
4
  //#region src/constants.ts
@@ -255,6 +255,27 @@ function dataUri(value) {
255
255
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
256
256
  }
257
257
  //#endregion
258
+ //#region src/composeRefs.ts
259
+ function composeRefs(...refs) {
260
+ return (value) => {
261
+ const cleanups = refs.map((ref) => {
262
+ if (typeof ref === "function") {
263
+ const cleanup = ref(value);
264
+ return typeof cleanup === "function" ? cleanup : () => {
265
+ ref(null);
266
+ };
267
+ }
268
+ if (ref) {
269
+ ref.current = value;
270
+ return () => {
271
+ ref.current = null;
272
+ };
273
+ }
274
+ });
275
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
276
+ };
277
+ }
278
+ //#endregion
258
279
  //#region src/index.web.tsx
259
280
  const notSupported = {
260
281
  ok: false,
@@ -271,10 +292,15 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
271
292
  const src = sized ? sized.src : resolveUri(source);
272
293
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
273
294
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
295
+ const [reloads, setReloads] = useState(0);
296
+ const ref = useMemo(() => composeRefs((view) => {
297
+ if (view) view.reload = () => setReloads((count) => count + 1);
298
+ }, forwardedRef), [forwardedRef]);
274
299
  const priority = sourcePriority(source);
275
300
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
276
301
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
277
302
  const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
303
+ const placeholderKey = `${urlPlaceholder} ${reloads}`;
278
304
  const [failedPlaceholder, setFailedPlaceholder] = useState();
279
305
  const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
280
306
  const [result, setResult] = useState();
@@ -288,14 +314,14 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
288
314
  const showDefault = ok !== true;
289
315
  useClientLayoutEffect(() => {
290
316
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
291
- }, [key]);
317
+ }, [key, reloads]);
292
318
  return /* @__PURE__ */ React.createElement(View, {
293
319
  ...viewProps,
294
320
  "aria-label": label,
295
321
  style: [styles.container, style],
296
322
  onLayout,
297
323
  pointerEvents,
298
- ref: forwardedRef
324
+ ref
299
325
  }, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
300
326
  key: `default ${defaultUri}`,
301
327
  src: defaultUri,
@@ -307,7 +333,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
307
333
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
308
334
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
309
335
  }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
310
- key: `placeholder ${urlPlaceholder}`,
336
+ key: `placeholder ${placeholderKey}`,
311
337
  src: urlPlaceholder,
312
338
  loading,
313
339
  priority: "high",
@@ -315,11 +341,11 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
315
341
  fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
316
342
  position,
317
343
  filter: tintColor ? `url(#${tintId})` : void 0,
318
- hidden: failedPlaceholder === urlPlaceholder,
319
- onError: () => setFailedPlaceholder(urlPlaceholder),
344
+ hidden: failedPlaceholder === placeholderKey,
345
+ onError: () => setFailedPlaceholder(placeholderKey),
320
346
  alt: ""
321
347
  }) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
322
- key: several ? "sizes" : key,
348
+ key: `${several ? "sizes" : key} ${reloads}`,
323
349
  src,
324
350
  srcSet,
325
351
  sizes,
@@ -577,11 +603,10 @@ FastImage.setDefaults = () => {};
577
603
  FastImage.writeToCache = () => Promise.resolve(notSupported);
578
604
  FastImage.getCachePath = () => Promise.resolve(notSupported);
579
605
  const styles = StyleSheet.create({ container: { overflow: "hidden" } });
580
- const FastImageWithRef = FastImageComponent;
581
606
  const FastImageBackground = forwardRef(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ React.createElement(View, {
582
607
  style,
583
608
  ref
584
- }, /* @__PURE__ */ React.createElement(FastImageWithRef, {
609
+ }, /* @__PURE__ */ React.createElement(FastImageComponent, {
585
610
  ...props,
586
611
  style: [StyleSheet.absoluteFill, imageStyle],
587
612
  ref: imageRef
@@ -137,6 +137,15 @@ export type FastImageSource = {
137
137
  scale?: number,
138
138
  }
139
139
 
140
+ // React Native's ImageURISource cache values, which the views' `source` takes
141
+ // so a React Native source works as it is: native reads 'only-if-cached' as
142
+ // 'cacheOnly', and the others as the default, 'immutable'.
143
+ type ReactNativeCache = 'default' | 'reload' | 'force-cache' | 'only-if-cached'
144
+ type ViewSource = {
145
+ ...FastImageSource,
146
+ cache?: CacheControls | ReactNativeCache,
147
+ }
148
+
140
149
  export type FastImageProps = $ReadOnly<{|
141
150
  ...ViewProps,
142
151
  onError?: ?(event: OnErrorEvent) => void,
@@ -145,7 +154,7 @@ export type FastImageProps = $ReadOnly<{|
145
154
  onLoadStart?: ?() => void,
146
155
  onProgress?: ?(event: OnProgressEvent) => void,
147
156
 
148
- source?: ?(FastImageSource | Array<FastImageSource | number> | number),
157
+ source?: ?(ViewSource | Array<ViewSource | number> | number),
149
158
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
159
  // Deprecated: use placeholder, which is the same.
151
160
  defaultSource?: ?number,
@@ -196,4 +205,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
205
  source: FastImageSource,
197
206
  file: string,
198
207
  ) => Promise<CachePathResult>;
208
+ // On a ref: loads the image again, e.g. to try again after onError.
209
+ reload(): void;
199
210
  }
@@ -0,0 +1,2 @@
1
+ export type ReactNativeCache = 'default' | 'reload' | 'force-cache' | 'only-if-cached';
2
+ export declare function warnIfReload(source: unknown): void;
@@ -1,3 +1,4 @@
1
+ import type * as React from 'react';
1
2
  import type { CodegenTypes, ColorValue, HostComponent, ViewProps } from 'react-native';
2
3
  type LoadStartEvent = Readonly<{}>;
3
4
  type ProgressEvent = Readonly<{
@@ -44,5 +45,9 @@ export interface NativeProps extends ViewProps {
44
45
  onFastImageError?: CodegenTypes.DirectEventHandler<ErrorEvent>;
45
46
  onFastImageLoadEnd?: CodegenTypes.DirectEventHandler<LoadEndEvent>;
46
47
  }
48
+ interface NativeCommands {
49
+ reload: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
50
+ }
51
+ export declare const Commands: NativeCommands;
47
52
  declare const _default: HostComponent<NativeProps>;
48
53
  export default _default;
@@ -82,6 +82,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
82
82
  // the view has a size, when it needs one).
83
83
  - (void) didSetProps;
84
84
 
85
+ // Loads the image again, as a new source would (once the view has a size,
86
+ // when it needs one): the ref's reload().
87
+ - (void) reload;
88
+
85
89
  // Keeps the view's mask (React Native's, for its rounded corners) on the box
86
90
  // the view was laid out at, while objectPosition moves the view: after React
87
91
  // Native sets or removes it.
@@ -1047,6 +1047,11 @@ NSString *FFFErrorMessage(NSError *error)
1047
1047
  }
1048
1048
  }
1049
1049
 
1050
+ - (void) reload {
1051
+ _needsReload = YES;
1052
+ [self didSetProps];
1053
+ }
1054
+
1050
1055
  // The same image decoded again for a dynamicRangeLimit between HDR and SDR,
1051
1056
  // as a resize is (reloadIfResized): without events once it has loaded, and
1052
1057
  // the image showing stays if the load fails. A display setting shouldn't
@@ -388,6 +388,17 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
388
388
  }
389
389
  }
390
390
 
391
+ // The view's commands (reload), from the ref's methods.
392
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
393
+ {
394
+ RCTFastImageViewHandleCommand(self, commandName, args);
395
+ }
396
+
397
+ - (void)reload
398
+ {
399
+ [_imageView reload];
400
+ }
401
+
391
402
  // React Native makes the image view's mask again when the appearance changes
392
403
  // (as after an update): where objectPosition moved the view, it moves back.
393
404
  - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
@@ -21,6 +21,9 @@ RCT_ENUM_CONVERTER(FFFCacheControl, (@{
21
21
  @"immutable": @(FFFCacheControlImmutable),
22
22
  @"web": @(FFFCacheControlWeb),
23
23
  @"cacheOnly": @(FFFCacheControlCacheOnly),
24
+ // React Native's ImageURISource value, which views
25
+ // take. Its others (e.g. 'reload') are the default.
26
+ @"only-if-cached": @(FFFCacheControlCacheOnly),
24
27
  }), FFFCacheControlImmutable, integerValue);
25
28
 
26
29
  + (FFFastImageSource *)FFFastImageSource:(id)json {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "10.6.0",
3
+ "version": "10.8.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
@@ -0,0 +1,29 @@
1
+ // Shared by the native (index.tsx) and web (index.web.tsx) versions.
2
+ import type React from 'react'
3
+
4
+ // One ref that sets each of these refs (as React Native's useMergeRefs does),
5
+ // and clears them when it's cleared.
6
+ export function composeRefs<T>(
7
+ ...refs: (React.Ref<T> | undefined)[]
8
+ ): React.RefCallback<T> {
9
+ return (value) => {
10
+ const cleanups = refs.map((ref) => {
11
+ if (typeof ref === 'function') {
12
+ const cleanup = ref(value)
13
+ return typeof cleanup === 'function'
14
+ ? cleanup
15
+ : () => {
16
+ ref(null)
17
+ }
18
+ }
19
+ if (ref) {
20
+ ref.current = value
21
+ return () => {
22
+ ref.current = null
23
+ }
24
+ }
25
+ return undefined
26
+ })
27
+ return () => cleanups.forEach((cleanup) => cleanup?.())
28
+ }
29
+ }
package/src/index.js.flow CHANGED
@@ -137,6 +137,15 @@ export type FastImageSource = {
137
137
  scale?: number,
138
138
  }
139
139
 
140
+ // React Native's ImageURISource cache values, which the views' `source` takes
141
+ // so a React Native source works as it is: native reads 'only-if-cached' as
142
+ // 'cacheOnly', and the others as the default, 'immutable'.
143
+ type ReactNativeCache = 'default' | 'reload' | 'force-cache' | 'only-if-cached'
144
+ type ViewSource = {
145
+ ...FastImageSource,
146
+ cache?: CacheControls | ReactNativeCache,
147
+ }
148
+
140
149
  export type FastImageProps = $ReadOnly<{|
141
150
  ...ViewProps,
142
151
  onError?: ?(event: OnErrorEvent) => void,
@@ -145,7 +154,7 @@ export type FastImageProps = $ReadOnly<{|
145
154
  onLoadStart?: ?() => void,
146
155
  onProgress?: ?(event: OnProgressEvent) => void,
147
156
 
148
- source?: ?(FastImageSource | Array<FastImageSource | number> | number),
157
+ source?: ?(ViewSource | Array<ViewSource | number> | number),
149
158
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
159
  // Deprecated: use placeholder, which is the same.
151
160
  defaultSource?: ?number,
@@ -196,4 +205,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
205
  source: FastImageSource,
197
206
  file: string,
198
207
  ) => Promise<CachePathResult>;
208
+ // On a ref: loads the image again, e.g. to try again after onError.
209
+ reload(): void;
199
210
  }
package/src/index.tsx CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { cacheControl, priority, resizeMode } from './constants'
15
15
  import { fromStyle, resolveObjectFit } from './objectFit'
16
16
  import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
17
- import FastImageView from './specs/FastImageViewNativeComponent'
17
+ import FastImageView, { Commands } from './specs/FastImageViewNativeComponent'
18
18
  import NativeFastImageModule from './specs/NativeFastImageModule'
19
19
  import {
20
20
  dataUri,
@@ -22,6 +22,8 @@ import {
22
22
  isImageName,
23
23
  placeholderSource,
24
24
  } from './placeholders'
25
+ import { composeRefs } from './composeRefs'
26
+ import { type ReactNativeCache, warnIfReload } from './reactNativeCache'
25
27
  import { warnIfChildren, warnOnce } from './warnings'
26
28
 
27
29
  /** How the image fits the view: see [`objectFit`](#objectfit). */
@@ -177,6 +179,14 @@ export type Source = {
177
179
  scale?: number
178
180
  }
179
181
 
182
+ // What the views' `source` takes: a Source, or one with React Native's cache
183
+ // values (see reactNativeCache.ts), so a React Native `ImageURISource` works as
184
+ // it is. Only the views: preload, getCachePath, writeToCache and placeholder
185
+ // take Fast Image's values.
186
+ type ViewSource =
187
+ | Source
188
+ | (Omit<Source, 'cache'> & { cache?: ReactNativeCache })
189
+
180
190
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
181
191
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
182
192
 
@@ -365,7 +375,10 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
365
375
  * - A `require()`d image can be one of the sizes: its size is the file's.
366
376
  * - An array of one is the same as that source.
367
377
  */
368
- source?: Source | Array<Source | ImageRequireSource> | ImageRequireSource
378
+ source?:
379
+ | ViewSource
380
+ | Array<ViewSource | ImageRequireSource>
381
+ | ImageRequireSource
369
382
  /**
370
383
  * A placeholder shown while the image loads, and if it fails to load:
371
384
  *
@@ -900,9 +913,11 @@ function FastImageBase({
900
913
  // only those change. Memoized on source, so an unchanged source reaches
901
914
  // the native view as the same object, and React's prop diff stops at it.
902
915
  const resolvedSource = useMemo(() => {
903
- // A blurDataUri that isn't a data uri is ignored, with a warning.
916
+ // A blurDataUri that isn't a data uri is ignored, with a warning, and
917
+ // React Native's cache: 'reload' warns (native reads its cache values).
904
918
  for (const entry of [source].flat()) {
905
919
  dataUri((entry as Source | undefined)?.blurDataUri)
920
+ warnIfReload(entry)
906
921
  }
907
922
  const single =
908
923
  Array.isArray(source) && source.length === 1 ? source[0] : source
@@ -920,11 +935,15 @@ function FastImageBase({
920
935
  : resolvePlaceholder(defaultSource)
921
936
  : (placeholderSource(placeholder) ??
922
937
  resolvePlaceholder(placeholder ?? defaultSource))
938
+ const ref = useMemo(
939
+ () => composeRefs<FastImageRef>(addReload, forwardedRef),
940
+ [forwardedRef],
941
+ )
923
942
 
924
943
  return (
925
944
  <FastImageView
926
945
  {...props}
927
- ref={forwardedRef}
946
+ ref={ref}
928
947
  // Clips the image to borderRadius by default, as React Native's
929
948
  // Image does.
930
949
  style={[styles.image, style]}
@@ -967,19 +986,37 @@ function FastImageBase({
967
986
  )
968
987
  }
969
988
 
989
+ // The ref is the native view, with reload() added, as React Native's
990
+ // TextInput adds its methods to its native view.
991
+ function addReload(view: FastImageRef | null) {
992
+ if (view) view.reload = () => Commands.reload(view as any)
993
+ }
994
+
970
995
  const FastImageMemo = memo(FastImageBase)
971
996
 
972
- // What a ref to FastImage or FastImageBackground gets: FastImage's native
973
- // view, or FastImageBackground's view. Typed as a View's (they have the same
974
- // methods, e.g. measure). ComponentRef, as
975
- // React Native's types declare View as a class, and its Strict TypeScript API
976
- // (opt-in from 0.80, the default from 0.87) as a function component that takes
977
- // a ref.
997
+ // What a ref to FastImageBackground gets (its view), and, with reload(), one
998
+ // to FastImage (its native view, which has the same methods, e.g. measure).
999
+ // ComponentRef, as React Native's types declare View as a class, and its
1000
+ // Strict TypeScript API (opt-in from 0.80, the default from 0.87) as a
1001
+ // function component that takes a ref.
978
1002
  type ViewRef = React.ComponentRef<typeof View>
979
1003
 
1004
+ /**
1005
+ * What a ref to `FastImage` gets: its native view, with a View's methods
1006
+ * (e.g. `measure`), and `reload()`. E.g. `useRef<FastImageRef>(null)`.
1007
+ */
1008
+ export type FastImageRef = ViewRef & {
1009
+ /**
1010
+ * Loads the image again, e.g. to try again after `onError`. The image
1011
+ * showing stays until the new one has loaded, and the events are sent as
1012
+ * for a new source. An image in the cache loads from it.
1013
+ */
1014
+ reload(): void
1015
+ }
1016
+
980
1017
  const FastImageComponent: React.ForwardRefExoticComponent<
981
- FastImageProps & React.RefAttributes<ViewRef>
982
- > = forwardRef((props: FastImageProps, ref: React.Ref<ViewRef>) => (
1018
+ FastImageProps & React.RefAttributes<FastImageRef>
1019
+ > = forwardRef((props: FastImageProps, ref: React.Ref<FastImageRef>) => (
983
1020
  <FastImageMemo forwardedRef={ref} {...props} />
984
1021
  ))
985
1022
 
@@ -1112,7 +1149,7 @@ export interface FastImageDefaults {
1112
1149
  }
1113
1150
 
1114
1151
  const FastImage: React.ForwardRefExoticComponent<
1115
- FastImageProps & React.RefAttributes<ViewRef>
1152
+ FastImageProps & React.RefAttributes<FastImageRef>
1116
1153
  > &
1117
1154
  FastImageStaticProperties = FastImageComponent as any
1118
1155
 
@@ -1223,7 +1260,7 @@ export interface FastImageBackgroundProps extends Omit<
1223
1260
  /** The image's style. */
1224
1261
  imageStyle?: StyleProp<ImageStyle>
1225
1262
  /** A ref to the image (the FastImage inside). */
1226
- imageRef?: React.Ref<any>
1263
+ imageRef?: React.Ref<FastImageRef>
1227
1264
  /** Content shown on top of the image. */
1228
1265
  children?: React.ReactNode
1229
1266
  }
package/src/index.web.tsx CHANGED
@@ -23,6 +23,7 @@ import React, {
23
23
  useEffect,
24
24
  useId,
25
25
  useLayoutEffect,
26
+ useMemo,
26
27
  useRef,
27
28
  useState,
28
29
  } from 'react'
@@ -37,12 +38,14 @@ import {
37
38
  hasSourcePlaceholder,
38
39
  placeholderSource,
39
40
  } from './placeholders'
41
+ import { composeRefs } from './composeRefs'
40
42
  import { warnIfChildren, warnOnce } from './warnings'
41
43
  import type {
42
44
  CachePathResult,
43
45
  CacheState,
44
46
  FastImageBackgroundProps,
45
47
  FastImageProps,
48
+ FastImageRef,
46
49
  FastImageStaticProperties,
47
50
  ObjectFit,
48
51
  PreloadResult,
@@ -126,6 +129,17 @@ function FastImageBase({
126
129
  // one doesn't stay while it loads; with several sizes, the <img> stays,
127
130
  // and the browser shows the previous image until the new one has loaded.
128
131
  const key = srcSet ?? src
132
+ // Counts the ref's reload() calls: each gives the image a new <img>, which
133
+ // the browser loads again (as a new view would). The image showing stays
134
+ // (it has loaded), or stays hidden (it failed), until then.
135
+ const [reloads, setReloads] = useState(0)
136
+ const ref = useMemo(
137
+ () =>
138
+ composeRefs((view: FastImageRef | null) => {
139
+ if (view) view.reload = () => setReloads((count) => count + 1)
140
+ }, forwardedRef),
141
+ [forwardedRef],
142
+ )
129
143
  const priority = sourcePriority(source)
130
144
  // The source's blurDataUri, or its ThumbHash made into one (with
131
145
  // `react-native-fast-image/thumbhash`), shows blurred (see PreviewBlur)
@@ -140,6 +154,8 @@ function FastImageBase({
140
154
  // placeholders as it loads, as on iOS and Android. The browser loads it
141
155
  // like any image: it can't look in its cache only (cache: 'cacheOnly').
142
156
  const urlPlaceholder = placeholderSource(placeholder)?.uri
157
+ // reload() gives it a new <img> too, so one that failed is asked for again.
158
+ const placeholderKey = `${urlPlaceholder} ${reloads}`
143
159
  const [failedPlaceholder, setFailedPlaceholder] = useState<string>()
144
160
  // defaultSource only shows without `placeholder`, as on iOS and Android,
145
161
  // or in place of a thumbhash or blurDataUri the web can't show (one of
@@ -179,7 +195,7 @@ function FastImageBase({
179
195
  if (key) onLoadStart?.()
180
196
  // Once per image the browser is asked for.
181
197
  // eslint-disable-next-line react-hooks/exhaustive-deps
182
- }, [key])
198
+ }, [key, reloads])
183
199
 
184
200
  return (
185
201
  <View
@@ -188,7 +204,7 @@ function FastImageBase({
188
204
  style={[styles.container, style]}
189
205
  onLayout={onLayout}
190
206
  pointerEvents={pointerEvents}
191
- ref={forwardedRef}
207
+ ref={ref}
192
208
  >
193
209
  {defaultUri && showDefault ? (
194
210
  <Picture
@@ -226,7 +242,7 @@ function FastImageBase({
226
242
  ) : null}
227
243
  {urlPlaceholder && showDefault ? (
228
244
  <Picture
229
- key={`placeholder ${urlPlaceholder}`}
245
+ key={`placeholder ${placeholderKey}`}
230
246
  src={urlPlaceholder}
231
247
  loading={loading}
232
248
  // It only helps if it comes before the image, as on iOS
@@ -241,14 +257,14 @@ function FastImageBase({
241
257
  // Tinted, not blurred, as on iOS and Android.
242
258
  filter={tintColor ? `url(#${tintId})` : undefined}
243
259
  // Nothing (no broken image icon) if it fails.
244
- hidden={failedPlaceholder === urlPlaceholder}
245
- onError={() => setFailedPlaceholder(urlPlaceholder)}
260
+ hidden={failedPlaceholder === placeholderKey}
261
+ onError={() => setFailedPlaceholder(placeholderKey)}
246
262
  alt=""
247
263
  />
248
264
  ) : null}
249
265
  {key && src ? (
250
266
  <Picture
251
- key={several ? 'sizes' : key}
267
+ key={`${several ? 'sizes' : key} ${reloads}`}
252
268
  src={src}
253
269
  srcSet={srcSet}
254
270
  sizes={sizes}
@@ -383,10 +399,13 @@ function assetScale(asset: PackagerAsset): number {
383
399
  )
384
400
  }
385
401
 
402
+ // What sizedSources reads of a source.
403
+ type SizedSource = Pick<Source, 'uri' | 'width' | 'height' | 'scale'>
404
+
386
405
  // One of several sizes: a require()d image in react-native-web's asset
387
406
  // registry as a source with its file's size (Expo's web builds make it a
388
407
  // { uri, width, height } object already).
389
- function sizedSource(source: Source | number): Source | undefined {
408
+ function sizedSource(source: SizedSource | number): SizedSource | undefined {
390
409
  if (typeof source !== 'number') return source
391
410
  const asset: PackagerAsset | undefined = getAssetByID(source)
392
411
  return asset
@@ -406,10 +425,12 @@ function sizedSource(source: Source | number): Source | undefined {
406
425
  // is laid out at; it only applies to lazy images, and otherwise, or in
407
426
  // browsers without it, the next value, 100vw (the viewport's width). src is the largest, for browsers without
408
427
  // srcset.
409
- function sizedSources(entries: (Source | number)[]) {
428
+ function sizedSources(entries: (SizedSource | number)[]) {
410
429
  const sources = entries.map(sizedSource)
411
430
  const widths = sources
412
- .filter((source): source is Source => !!source?.uri && !!source.width)
431
+ .filter(
432
+ (source): source is SizedSource => !!source?.uri && !!source.width,
433
+ )
413
434
  .map((source) => ({
414
435
  uri: source.uri as string,
415
436
  width: Math.round((source.width ?? 0) * (source.scale ?? 1)),
@@ -653,11 +674,15 @@ const tintStyle = {
653
674
 
654
675
  const FastImageMemo = memo(FastImageBase)
655
676
 
656
- const FastImageComponent: React.ComponentType<FastImageProps> = forwardRef(
657
- (props: FastImageProps, ref: React.Ref<any>) => (
658
- <FastImageMemo forwardedRef={ref} {...props} />
659
- ),
660
- )
677
+ // What a ref to FastImageBackground gets, as in index.tsx: a View's
678
+ // (react-native-web's element). FastImage's has reload() too (FastImageRef).
679
+ type ViewRef = React.ComponentRef<typeof View>
680
+
681
+ const FastImageComponent: React.ForwardRefExoticComponent<
682
+ FastImageProps & React.RefAttributes<FastImageRef>
683
+ > = forwardRef((props: FastImageProps, ref: React.Ref<FastImageRef>) => (
684
+ <FastImageMemo forwardedRef={ref} {...props} />
685
+ ))
661
686
 
662
687
  FastImageComponent.displayName = 'FastImage'
663
688
 
@@ -672,7 +697,9 @@ function getSize(uri: string) {
672
697
  })
673
698
  }
674
699
 
675
- const FastImage: React.ComponentType<FastImageProps> &
700
+ const FastImage: React.ForwardRefExoticComponent<
701
+ FastImageProps & React.RefAttributes<FastImageRef>
702
+ > &
676
703
  FastImageStaticProperties = FastImageComponent as any
677
704
 
678
705
  FastImage.cacheControl = cacheControl
@@ -720,33 +747,29 @@ const styles = StyleSheet.create({
720
747
  },
721
748
  })
722
749
 
723
- // FastImage forwards its ref, which its type doesn't say.
724
- const FastImageWithRef = FastImageComponent as React.ComponentType<
725
- FastImageProps & { ref?: React.Ref<any> }
726
- >
727
-
728
- export const FastImageBackground: React.ComponentType<FastImageBackgroundProps> =
729
- forwardRef(
730
- (
731
- {
732
- style,
733
- imageStyle,
734
- imageRef,
735
- children,
736
- ...props
737
- }: FastImageBackgroundProps,
738
- ref: React.Ref<any>,
739
- ) => (
740
- <View style={style} ref={ref}>
741
- <FastImageWithRef
742
- {...props}
743
- style={[StyleSheet.absoluteFill, imageStyle]}
744
- ref={imageRef}
745
- />
746
- {children}
747
- </View>
748
- ),
749
- )
750
+ export const FastImageBackground: React.ForwardRefExoticComponent<
751
+ FastImageBackgroundProps & React.RefAttributes<ViewRef>
752
+ > = forwardRef(
753
+ (
754
+ {
755
+ style,
756
+ imageStyle,
757
+ imageRef,
758
+ children,
759
+ ...props
760
+ }: FastImageBackgroundProps,
761
+ ref: React.Ref<ViewRef>,
762
+ ) => (
763
+ <View style={style} ref={ref}>
764
+ <FastImageComponent
765
+ {...props}
766
+ style={[StyleSheet.absoluteFill, imageStyle]}
767
+ ref={imageRef}
768
+ />
769
+ {children}
770
+ </View>
771
+ ),
772
+ )
750
773
 
751
774
  FastImageBackground.displayName = 'FastImageBackground'
752
775
 
@@ -0,0 +1,28 @@
1
+ // React Native's `ImageURISource` cache values, which the views' `source`
2
+ // takes so a source typed for React Native's Image works as it is. Not
3
+ // documented: Fast Image's own values are the ones to use. Native reads
4
+ // 'only-if-cached' as 'cacheOnly', and the others as the default, 'immutable'.
5
+ import { warnOnce } from './warnings'
6
+
7
+ declare const __DEV__: boolean | undefined
8
+
9
+ export type ReactNativeCache =
10
+ | 'default'
11
+ | 'reload'
12
+ | 'force-cache'
13
+ | 'only-if-cached'
14
+
15
+ // Warns for a source with 'reload', as the image isn't loaded again each time.
16
+ export function warnIfReload(source: unknown) {
17
+ if (
18
+ typeof __DEV__ !== 'undefined' &&
19
+ __DEV__ &&
20
+ (source as { cache?: unknown } | null | undefined)?.cache === 'reload'
21
+ ) {
22
+ warnOnce(
23
+ 'cache-reload',
24
+ "react-native-fast-image: `cache: 'reload'` loads the image as the default, `'immutable'`: it isn't loaded again each time. For an image that changes at the same url, use `cache: 'web'` (it follows the server's cache headers), or change its `cacheKey`.",
25
+ 'warn',
26
+ )
27
+ }
28
+ }
@@ -1,15 +1,16 @@
1
1
  // The native image view, from React Native's Codegen (the New Architecture's
2
2
  // component). FastImage (index.tsx) renders it as its only view, and turns
3
3
  // its props into these.
4
+ import type * as React from 'react'
4
5
  import type {
5
6
  CodegenTypes,
6
7
  ColorValue,
7
8
  HostComponent,
8
9
  ViewProps,
9
10
  } from 'react-native'
10
- // React Native's Babel plugin replaces the call below with the component's
11
- // view config, so the import isn't used when the app runs.
12
- import { codegenNativeComponent } from 'react-native'
11
+ // React Native's Babel plugin replaces the calls below with the component's
12
+ // view config and commands, so the imports aren't used when the app runs.
13
+ import { codegenNativeCommands, codegenNativeComponent } from 'react-native'
13
14
 
14
15
  type LoadStartEvent = Readonly<{}>
15
16
 
@@ -86,6 +87,17 @@ export interface NativeProps extends ViewProps {
86
87
  onFastImageLoadEnd?: CodegenTypes.DirectEventHandler<LoadEndEvent>
87
88
  }
88
89
 
90
+ interface NativeCommands {
91
+ // Loads the image again, as a new source would (with its events), keeping
92
+ // the image showing until then. ElementRef, which React Native 0.83's
93
+ // Codegen needs (later ones also take ComponentRef).
94
+ reload: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void
95
+ }
96
+
97
+ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
98
+ supportedCommands: ['reload'],
99
+ })
100
+
89
101
  export default codegenNativeComponent<NativeProps>(
90
102
  'FastImageView',
91
103
  ) as HostComponent<NativeProps>