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