Permalink
Browse files

Cache policy control for image source

Summary:
In the context of an app an image exists in three resolutions on the server: `thumb` (30px) `feed` (300px) `full` (900px). When looking at an individual item a user can come either from the feed, via a permalink or from other parts of the app. This allows a situation where the `feed` image might or might not already be loaded somewhere in the app. In the detail view I want to render `thumb` with a blur (to quickly display something), then the `feed` image if it exists to have something decent to display until `full` loads. However it is quite a waste to load the `feed` image if it isn't already in cache, and will slow down the time until `full` is loaded. It is possible to track the navigation from feed->detail and that the `feed` image has actually completed loading by the feed component however as component hierarchies grow this turns into quite a lot of prop passing and bad separation of concerns.

NSURLRequests accepts a [Cache Policy](https://developer.apple.com/reference/fo
Closes #10844

Differential Revision: D4425959

Pulled By: lacker

fbshipit-source-id: 679835439c761a2fc894f56eb6d744c036cf0b49
  • Loading branch information...
salanki authored and facebook-github-bot committed Jan 18, 2017
1 parent a6844bd commit 52d8851fc8d7dc95d0ccff0d9775eece30b983b8
@@ -67,6 +67,7 @@ - (void)testTheTester_ExpectError
RCT_TEST(TimersTest)
RCT_TEST(AsyncStorageTest)
RCT_TEST(AppEventsTest)
RCT_TEST(ImageCachePolicyTest)
//RCT_TEST(ImageSnapshotTest) // Disabled: #8985988
//RCT_TEST(LayoutEventsTest) // Disabled due to flakiness: #8686784
RCT_TEST(SimpleSnapshotTest)
@@ -304,6 +304,35 @@ exports.examples = [
},
platform: 'ios',
},
{
title: 'Cache Policy',
description: 'First image has never been loaded before and is instructed not to load unless in cache.' +
'Placeholder image from above will stay. Second image is the same but forced to load regardless of' +
' local cache state.',
render: function () {
return (
<View style={styles.horizontal}>
<Image
defaultSource={require('./bunny.png')}
source={{
uri: smallImage.uri + '?cacheBust=notinCache' + Date.now(),
cache: 'only-if-cached'
}}
style={styles.base}
/>
<Image
defaultSource={require('./bunny.png')}
source={{
uri: smallImage.uri + '?cacheBust=notinCache' + Date.now(),
cache: 'reload'
}}
style={styles.base}
/>
</View>
);
},
platform: 'ios',
},
{
title: 'Border Color',
render: function() {
@@ -0,0 +1,115 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
var React = require('react');
var ReactNative = require('react-native');
var {
Image,
View,
Text,
StyleSheet,
} = ReactNative;
var { TestModule } = ReactNative.NativeModules;
/*
* The reload and force-cache tests don't actually verify that the complete functionality.
*
* reload: Should have the server set a long cache header, then swap the image on next load
* with the test comparing the old image to the new image and making sure they are different.
*
* force-cache: Should do the above but set a no-cache header. The test should compare the first
* image with the new one and make sure they are the same.
*/
const TESTS = ['only-if-cached', 'default', 'reload', 'force-cache'];
type Props = {}
type State = {
'only-if-cached'?: boolean,
'default'?: boolean,
'reload'?: boolean,
'force-cache'?: boolean,
}
class ImageCachePolicyTest extends React.Component {
state = {}
shouldComponentUpdate(nextProps: Props, nextState: State) {
const results: Array<?boolean> = TESTS.map(x => nextState[x]);
if (!results.includes(undefined)) {
const result: boolean = results.reduce((x,y) => x === y === true, true)
TestModule.markTestPassed(result);
}
return false;
}
testComplete(name: string, pass: boolean) {
this.setState({[name]: pass});
}
render() {
return (
<View style={{flex: 1}}>
<Text>Hello</Text>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'only-if-cached'
}}
onLoad={() => this.testComplete('only-if-cached', false)}
onError={() => this.testComplete('only-if-cached', true)}
style={styles.base}
/>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'default'
}}
onLoad={() => this.testComplete('default', true)}
onError={() => this.testComplete('default', false)}
style={styles.base}
/>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'reload'
}}
onLoad={() => this.testComplete('reload', true)}
onError={() => this.testComplete('reload', false)}
style={styles.base}
/>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'force-cache'
}}
onLoad={() => this.testComplete('force-cache', true)}
onError={() => this.testComplete('force-cache', false)}
style={styles.base}
/>
</View>
);
}
}
const styles = StyleSheet.create({
base: {
width: 100,
height: 100,
},
});
ImageCachePolicyTest.displayName = 'ImageCachePolicyTest';
module.exports = ImageCachePolicyTest;
@@ -29,6 +29,7 @@ var TESTS = [
require('./LayoutEventsTest'),
require('./AppEventsTest'),
require('./SimpleSnapshotTest'),
require('./ImageCachePolicyTest'),
require('./ImageSnapshotTest'),
require('./PromiseTest'),
require('./WebSocketTest'),
@@ -140,7 +140,8 @@ const Image = React.createClass({
* This prop can also contain several remote URLs, specified together with
* their width and height and potentially with scale/other URI arguments.
* The native side will then choose the best `uri` to display based on the
* measured size of the image container.
* measured size of the image container. A `cache` property can be added to
* control how networked request interacts with the local cache.
*/
source: ImageSourcePropType,
/**
@@ -42,6 +42,32 @@ const ImageURISourcePropType = PropTypes.shape({
* additional encoding (e.g. URL-escaping or base64) applied.
*/
body: PropTypes.string,
/**
* `cache` determines how the requests handles potentially cached
* responses.
*
* - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS.
*
* - `reload`: The data for the URL will be loaded from the originating source.
* No existing cache data should be used to satisfy a URL load request.
*
* - `force-cache`: The existing cached data will be used to satisfy the request,
* regardless of its age or expiration date. If there is no existing data in the cache
* corresponding the request, the data is loaded from the originating source.
*
* - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
* its age or expiration date. If there is no existing data in the cache corresponding
* to a URL load request, no attempt is made to load the data from the originating source,
* and the load is considered to have failed.
*
* @platform ios
*/
cache: PropTypes.oneOf([
'default',
'reload',
'force-cache',
'only-if-cached',
]),
/**
* `width` and `height` can be specified if known at build time, in which case
* these will be used to set the default `<Image/>` component dimensions.
View
@@ -47,6 +47,7 @@
+ (NSData *)NSData:(id)json;
+ (NSIndexSet *)NSIndexSet:(id)json;
+ (NSURLRequestCachePolicy)NSURLRequestCachePolicy:(id)json;
+ (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json;
View
@@ -118,6 +118,14 @@ + (NSURL *)NSURL:(id)json
}
}
RCT_ENUM_CONVERTER(NSURLRequestCachePolicy, (@{
@"default": @(NSURLRequestUseProtocolCachePolicy),
@"reload": @(NSURLRequestReloadIgnoringLocalCacheData),
@"force-cache": @(NSURLRequestReturnCacheDataElseLoad),
@"only-if-cached": @(NSURLRequestReturnCacheDataDontLoad),
}), NSURLRequestUseProtocolCachePolicy, integerValue)
+ (NSURLRequest *)NSURLRequest:(id)json
{
if ([json isKindOfClass:[NSString class]]) {
@@ -140,8 +148,9 @@ + (NSURLRequest *)NSURLRequest:(id)json
NSData *body = [self NSData:json[@"body"]];
NSString *method = [self NSString:json[@"method"]].uppercaseString ?: @"GET";
NSURLRequestCachePolicy cachePolicy = [self NSURLRequestCachePolicy:json[@"cache"]];
NSDictionary *headers = [self NSDictionary:json[@"headers"]];
if ([method isEqualToString:@"GET"] && headers == nil && body == nil) {
if ([method isEqualToString:@"GET"] && headers == nil && body == nil && cachePolicy == NSURLRequestUseProtocolCachePolicy) {
return [NSURLRequest requestWithURL:URL];
}
@@ -164,6 +173,7 @@ + (NSURLRequest *)NSURLRequest:(id)json
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
request.HTTPBody = body;
request.HTTPMethod = method;
request.cachePolicy = cachePolicy;
request.allHTTPHeaderFields = headers;
return [request copy];
}
View
@@ -89,6 +89,26 @@ Many of the images you will display in your app will not be available at compile
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}} />
```
### Cache Control (iOS Only)
In some cases you might only want to display an image if it is already in the local cache, i.e. a low resolution placeholder until a higher resolution is available. In other cases you do not care if the image is outdated and are willing to display an outdated image to save bandwidth. The `cache` source property gives you control over how the network layer interacts with the cache.
* `default`: Use the native platforms default strategy.
* `reload`: The data for the URL will be loaded from the originating source.
No existing cache data should be used to satisfy a URL load request.
* `force-cache`: The existing cached data will be used to satisfy the request,
regardless of its age or expiration date. If there is no existing data in the cache
corresponding the request, the data is loaded from the originating source.
* `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
its age or expiration date. If there is no existing data in the cache corresponding
to a URL load request, no attempt is made to load the data from the originating source,
and the load is considered to have failed.
```javascript
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png', cache: 'only-if-cached'}}
style={{width: 400, height: 400}} />
````
## Local Filesystem Images
See [CameraRoll](docs/cameraroll.html) for an example of

1 comment on commit 52d8851

@devKrishnan

This comment has been minimized.

Show comment
Hide comment
@devKrishnan

devKrishnan May 4, 2017

The cache policy attached in the description is broken.

devKrishnan commented on 52d8851 May 4, 2017

The cache policy attached in the description is broken.

Please sign in to comment.