Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit 42f3441

Browse files
authored
Merge pull request #9 from billmalarky/issue-6-remove-makecancelable-reimplement-patch
Patch for issue 6 and 2.
2 parents 71de384 + d25b8d6 commit 42f3441

File tree

4 files changed

+51
-36
lines changed

4 files changed

+51
-36
lines changed

lib/imageCacheHoc.js

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import FileSystemFactory, { FileSystem } from '../lib/FileSystem';
2121
import traverse from 'traverse';
2222
import validator from 'validator';
2323
import uuid from 'react-native-uuid';
24-
import makeCancelable from 'makecancelable';
2524

2625
export default function imageCacheHoc(Image, options = {}) {
2726

@@ -99,6 +98,9 @@ export default function imageCacheHoc(Image, options = {}) {
9998
// Assign component unique ID for cache locking.
10099
this.componentId = uuid.v4();
101100

101+
// Track component mount status to avoid calling setState() on unmounted component.
102+
this._isMounted = false;
103+
102104
// Set default options
103105
this.options = {
104106
validProtocols: options.validProtocols || ['https'],
@@ -140,30 +142,35 @@ export default function imageCacheHoc(Image, options = {}) {
140142
// See: https://reactjs.org/docs/react-component.html#componentdidmount
141143
async componentDidMount() {
142144

145+
// Track component mount status to avoid calling setState() on unmounted component.
146+
this._isMounted = true;
147+
143148
// Add a cache lock to file with this name (prevents concurrent <CacheableImage> components from pruning a file with this name from cache).
144-
let fileName = await this.fileSystem.getFileNameFromUrl(traverse(this.props).get(['source', 'uri']));
149+
const fileName = await this.fileSystem.getFileNameFromUrl(traverse(this.props).get(['source', 'uri']));
145150
FileSystem.lockCacheFile(fileName, this.componentId);
146151

147152
// Check local fs for file, fallback to network and write file to disk if local file not found.
148-
// This must be cancelable in case component is unmounted before request completes.
149-
let permanent = this.props.permanent ? true : false;
153+
const permanent = this.props.permanent ? true : false;
154+
let localFilePath = null;
155+
try {
156+
localFilePath = await this.fileSystem.getLocalFilePathFromUrl(traverse(this.props).get(['source', 'uri']), permanent);
157+
} catch (error) {
158+
console.error(error); // eslint-disable-line no-console
159+
}
150160

151-
this.cancelLocalFilePathRequest = makeCancelable(
152-
this.fileSystem.getLocalFilePathFromUrl(traverse(this.props).get(['source', 'uri']), permanent),
153-
localFilePath => this.setState({ localFilePath }),
154-
error => console.error(error) // eslint-disable-line no-console
155-
);
161+
// Check component is still mounted to avoid calling setState() on components that were quickly
162+
// mounted then unmounted before componentDidMount() finishes.
163+
// See: https://github.com/billmalarky/react-native-image-cache-hoc/issues/6#issuecomment-354490597
164+
if (this._isMounted && localFilePath) {
165+
this.setState({ localFilePath });
166+
}
156167

157168
}
158169

159170
async componentWillUnmount() {
160171

161-
// Cancel pending setState() actions.
162-
// NOTE: must check this.cancelLocalFilePathRequest is set to avoid edge case where component is mounted then immediately unmounted before componentDidMount() fires.
163-
if (this.cancelLocalFilePathRequest) {
164-
this.cancelLocalFilePathRequest();
165-
}
166-
172+
// Track component mount status to avoid calling setState() on unmounted component.
173+
this._isMounted = false;
167174

168175
// Remove component cache lock on associated image file on component teardown.
169176
let fileName = await this.fileSystem.getFileNameFromUrl(traverse(this.props).get(['source', 'uri']));

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"react-test-renderer": "^16.0.0",
4343
"should": "^13.1.2",
4444
"should-sinon": "^0.0.6",
45-
"sinon": "^4.1.2"
45+
"sinon": "^4.1.3"
4646
},
4747
"jest": {
4848
"preset": "react-native",
@@ -53,7 +53,6 @@
5353
"dependencies": {
5454
"buffer": "^5.0.8",
5555
"crypto-js": "^3.1.9-1",
56-
"makecancelable": "^1.0.0",
5756
"path": "^0.12.7",
5857
"prop-types": "^15.6.0",
5958
"react-native-fetch-blob": "0.10.8",

tests/CacheableImage.test.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -262,24 +262,37 @@ describe('CacheableImage', function() {
262262

263263
});
264264

265-
it('setState() actions on component mount should create cancelable functions and use on unmount.', async () => {
265+
it('Verify component is actually still mounted before calling setState() in componentDidMount().', async () => {
266266

267267
// Set up mocks
268268
const FileSystem = require('../lib/FileSystem').default;
269269
FileSystem.prototype.getLocalFilePathFromUrl = jest.fn();
270270
FileSystem.prototype.getLocalFilePathFromUrl.mockReturnValue(new Promise((resolve) => {
271-
resolve(mockData.basePath + '/react-native-image-cache-hoc/permanent/cd7d2199cd8e088cdfd9c99fc6359666adc36289.png');
271+
setTimeout(() => {
272+
resolve(mockData.basePath + '/react-native-image-cache-hoc/permanent/cd7d2199cd8e088cdfd9c99fc6359666adc36289.png');
273+
}, 1000); // Mock 1 second delay for this async function to complete.
272274
}));
273275

274276
const CacheableImage = imageCacheHoc(Image);
275277
const cacheableImage = new CacheableImage(mockData.mockCacheableImageProps);
276278

277-
// Mount and unmount
278-
await cacheableImage.componentDidMount();
279-
cacheableImage.cancelLocalFilePathRequest(); // Call it once manually to actually cancel local file path request
280-
cacheableImage.cancelLocalFilePathRequest = sinon.spy(); // Set up a spy to make sure it gets called in componentWillUnmount().
281-
await cacheableImage.componentWillUnmount();
282-
cacheableImage.cancelLocalFilePathRequest.should.be.calledOnce();
279+
// Ensure that if component is mounted then immediately unmounted before componentDidMount() finishes
280+
// executing, setState() will not be called by an unmounted component when componentDidMount() resumes execution after
281+
// completing async work.
282+
// See: https://github.com/billmalarky/react-native-image-cache-hoc/issues/6#issuecomment-354490597
283+
cacheableImage.setState = sinon.spy(); // Mock setState with function tracker to ensure it doesn't get called on unmounted component.
284+
cacheableImage.componentDidMount();
285+
cacheableImage._isMounted.should.be.true();
286+
cacheableImage.componentWillUnmount();
287+
cacheableImage._isMounted.should.be.false();
288+
289+
// Wait for componentDidMount() to complete execution.
290+
await new Promise((resolve) => {
291+
setTimeout(resolve, 2000);
292+
});
293+
294+
// Ensure that setState() was not called on unmounted component.
295+
cacheableImage.setState.should.not.be.called();
283296

284297
});
285298

yarn.lock

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,8 +3031,8 @@ lolex@^1.6.0:
30313031
resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
30323032

30333033
lolex@^2.2.0:
3034-
version "2.3.0"
3035-
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.0.tgz#d6bad0f0aa5caebffcfebb09fb2caa89baaff51c"
3034+
version "2.3.1"
3035+
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.1.tgz#3d2319894471ea0950ef64692ead2a5318cff362"
30363036

30373037
longest@^1.0.1:
30383038
version "1.0.1"
@@ -3055,10 +3055,6 @@ macos-release@^1.0.0:
30553055
version "1.1.0"
30563056
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb"
30573057

3058-
makecancelable@^1.0.0:
3059-
version "1.0.0"
3060-
resolved "https://registry.yarnpkg.com/makecancelable/-/makecancelable-1.0.0.tgz#c7e2606e59db7a4bf8098ff5b52d7f13173499e5"
3061-
30623058
30633059
version "1.0.11"
30643060
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@@ -4274,17 +4270,17 @@ simple-plist@^0.2.1:
42744270
bplist-parser "0.1.1"
42754271
plist "2.0.1"
42764272

4277-
sinon@^4.1.2:
4278-
version "4.1.2"
4279-
resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.1.2.tgz#65610521d926fb53742dd84cd599f0b89a82f440"
4273+
sinon@^4.1.3:
4274+
version "4.1.3"
4275+
resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.1.3.tgz#fc599eda47ed9f1a694ce774b94ab44260bd7ac5"
42804276
dependencies:
42814277
diff "^3.1.0"
42824278
formatio "1.2.0"
42834279
lodash.get "^4.4.2"
42844280
lolex "^2.2.0"
42854281
nise "^1.2.0"
42864282
supports-color "^4.4.0"
4287-
type-detect "^4.0.0"
4283+
type-detect "^4.0.5"
42884284

42894285
slash@^1.0.0:
42904286
version "1.0.0"
@@ -4616,7 +4612,7 @@ type-check@~0.3.2:
46164612
dependencies:
46174613
prelude-ls "~1.1.2"
46184614

4619-
type-detect@^4.0.0:
4615+
type-detect@^4.0.5:
46204616
version "4.0.5"
46214617
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2"
46224618

0 commit comments

Comments
 (0)