|
8 | 8 | * @flow
|
9 | 9 | */
|
10 | 10 |
|
| 11 | +import type { ImageResult } from '../../modules/ImageLoader'; |
11 | 12 | import type { ImageProps, Source, ImageLoadingProps } from './types';
|
12 | 13 |
|
13 | 14 | import * as React from 'react';
|
@@ -108,6 +109,7 @@ function resolveAssetDimensions(source) {
|
108 | 109 | }
|
109 | 110 | }
|
110 | 111 |
|
| 112 | +// Todo: move this to ImageLoader, and handle URI create by URL.createObjectURL |
111 | 113 | function resolveAssetUri(source): ?string {
|
112 | 114 | let uri = null;
|
113 | 115 | if (typeof source === 'number') {
|
@@ -310,76 +312,84 @@ ImageWithStatics.queryCache = function (uris) {
|
310 | 312 | const useSource = (
|
311 | 313 | { onLoad, onLoadStart, onLoadEnd, onError }: ImageLoadingProps,
|
312 | 314 | source: ?Source
|
313 |
| -): { state: string, loadedUri: string } => { |
314 |
| - const lastLoadedSource = React.useRef(); |
315 |
| - const [loadedUri, setLoadedUri] = React.useState(''); |
| 315 | +): { state: string, loadedUri: ?string } => { |
| 316 | + const input = React.useRef({ uri: '' }); |
| 317 | + const [requestId, setRequestId] = React.useState(-1); |
316 | 318 | const [state, setState] = React.useState(() => {
|
317 | 319 | const uri = resolveAssetUri(source);
|
318 |
| - if (uri != null) { |
319 |
| - const isLoaded = ImageLoader.has(uri); |
320 |
| - if (isLoaded) return LOADED; |
321 |
| - } |
322 |
| - return IDLE; |
| 320 | + |
| 321 | + return { |
| 322 | + // Use the resolved URI for cases where it was already loaded or preloaded |
| 323 | + result: { uri }, |
| 324 | + status: ImageLoader.has(uri) ? LOADED : IDLE |
| 325 | + }; |
323 | 326 | });
|
324 | 327 |
|
325 |
| - // This object would only change when load related fields change |
326 |
| - // We try to maintain strict object reference to prevent the effect hook running due to object change |
327 |
| - const stableSource = React.useMemo(() => { |
| 328 | + const handleLoad = React.useCallback( |
| 329 | + (result: ImageResult) => { |
| 330 | + if (onLoad) onLoad(result); |
| 331 | + if (onLoadEnd) onLoadEnd(); |
| 332 | + setState({ status: LOADED, result }); |
| 333 | + }, |
| 334 | + [onLoad, onLoadEnd] |
| 335 | + ); |
| 336 | + |
| 337 | + const handleError = React.useCallback( |
| 338 | + (error) => { |
| 339 | + if (onError) { |
| 340 | + onError({ |
| 341 | + nativeEvent: { |
| 342 | + error: `Failed to load resource ${input.current.uri} (404)` |
| 343 | + } |
| 344 | + }); |
| 345 | + } |
| 346 | + if (onLoadEnd) onLoadEnd(); |
| 347 | + |
| 348 | + setState({ status: ERRORED, result: error }); |
| 349 | + }, |
| 350 | + [onError, onLoadEnd] |
| 351 | + ); |
| 352 | + |
| 353 | + const startLoading = React.useCallback( |
| 354 | + (nextInput) => { |
| 355 | + if (onLoadStart) onLoadStart(); |
| 356 | + const requestId = ImageLoader.load(nextInput, handleLoad, handleError); |
| 357 | + setRequestId(requestId); |
| 358 | + |
| 359 | + setState((prevState) => ({ ...prevState, status: LOADING })); |
| 360 | + }, |
| 361 | + [handleError, handleLoad, onLoadStart] |
| 362 | + ); |
| 363 | + |
| 364 | + // Cleanup on umount or after starting a new request |
| 365 | + React.useEffect(() => { |
| 366 | + return () => { |
| 367 | + if (requestId > 0) ImageLoader.release(requestId); |
| 368 | + }; |
| 369 | + }, [requestId]); |
| 370 | + |
| 371 | + // (Maybe) start loading on source changes |
| 372 | + React.useEffect(() => { |
328 | 373 | const uri = resolveAssetUri(source);
|
329 |
| - if (uri == null) { |
330 |
| - lastLoadedSource.current = null; |
331 |
| - return null; |
332 |
| - } |
| 374 | + if (uri == null) return; |
333 | 375 |
|
334 | 376 | let headers;
|
335 | 377 | if (source && typeof source.headers === 'object') {
|
336 | 378 | headers = ((source.headers: any): { [key: string]: string });
|
337 | 379 | }
|
338 | 380 |
|
339 | 381 | const nextInput = { uri, headers };
|
340 |
| - if ( |
341 |
| - JSON.stringify(nextInput) !== JSON.stringify(lastLoadedSource.current) |
342 |
| - ) { |
343 |
| - lastLoadedSource.current = nextInput; |
344 |
| - } |
345 |
| - |
346 |
| - return lastLoadedSource.current; |
347 |
| - }, [source]); |
348 | 382 |
|
349 |
| - React.useEffect(() => { |
350 |
| - if (stableSource == null) return; |
351 |
| - |
352 |
| - setState(LOADING); |
353 |
| - if (onLoadStart) onLoadStart(); |
354 |
| - |
355 |
| - const requestId = ImageLoader.load( |
356 |
| - stableSource, |
357 |
| - function load(result) { |
358 |
| - setState(LOADED); |
359 |
| - setLoadedUri(result.uri); |
360 |
| - if (onLoad) onLoad(result); |
361 |
| - if (onLoadEnd) onLoadEnd(); |
362 |
| - }, |
363 |
| - function error() { |
364 |
| - setState(ERRORED); |
365 |
| - if (onError) { |
366 |
| - onError({ |
367 |
| - nativeEvent: { |
368 |
| - error: `Failed to load resource ${stableSource.uri} (404)` |
369 |
| - } |
370 |
| - }); |
371 |
| - } |
372 |
| - if (onLoadEnd) onLoadEnd(); |
373 |
| - } |
374 |
| - ); |
| 383 | + // Do nothing if the input is virtually the same as the last loaded source |
| 384 | + if (JSON.stringify(nextInput) === JSON.stringify(input.current)) return; |
375 | 385 |
|
376 |
| - const effectCleanup = () => ImageLoader.release(requestId); |
377 |
| - return effectCleanup; |
378 |
| - }, [onError, onLoad, onLoadEnd, onLoadStart, stableSource]); |
| 386 | + input.current = nextInput; |
| 387 | + startLoading(nextInput); |
| 388 | + }, [source, startLoading]); |
379 | 389 |
|
380 | 390 | return {
|
381 |
| - state, |
382 |
| - loadedUri |
| 391 | + state: state.status, |
| 392 | + loadedUri: state.result.uri |
383 | 393 | };
|
384 | 394 | };
|
385 | 395 |
|
|
0 commit comments