prevent state updates after component unmounted

This commit is contained in:
btzr-io 2020-05-08 20:40:52 -05:00 committed by Sean Yesmunt
parent c8f21025d1
commit 036cf734c6
5 changed files with 110 additions and 72 deletions

View file

@ -4,11 +4,11 @@ import Villain from 'villain-react';
import LoadingScreen from 'component/common/loading-screen';
// @if TARGET='web'
import useStream from 'effects/use-stream'
import useStream from 'effects/use-stream';
// @endif
// @if TARGET='app'
import useFileStream from 'effects/use-stream-file'
import useFileStream from 'effects/use-stream-file';
// @endif
// Import default styles for Villain
@ -29,22 +29,18 @@ if (process.env.NODE_ENV !== 'production') {
workerUrl = `/${workerUrl}`;
}
const ComicBookViewer = ( props: Props) => {
const { source, theme } = props
const { stream, file } = source
const ComicBookViewer = (props: Props) => {
const { source, theme } = props;
let finalSource;
// @if TARGET='web'
const finalSource = useStream(stream)
finalSource = useStream(source.stream);
// @endif
// @if TARGET='app'
const finalSource = useFileStream(file)
finalSource = useFileStream(source.file);
// @endif
const { error, loading, content } = finalSource
const ready = content !== null && !loading
// Villain options
const opts = {
theme: theme === 'dark' ? 'Dark' : 'Light',
@ -53,15 +49,19 @@ const ComicBookViewer = ( props: Props) => {
allowGlobalShortcuts: true,
};
const { error, loading, content } = finalSource;
const ready = content !== null && !loading;
const errorMessage = __("Sorry, looks like we can't load the archive.");
return (
<div className="file-render__viewer file-render__viewer--comic">
{ loading && <LoadingScreen status={__('Loading')} />}
{ ready && <Villain source={finalSource.content} className={'comic-viewer'} options={opts} workerUrl={workerUrl} /> }
{ error && <LoadingScreen status={errorMessage} spinner={false} /> }
{loading && <LoadingScreen status={__('Loading')} />}
{ready && (
<Villain source={finalSource.content} className={'comic-viewer'} options={opts} workerUrl={workerUrl} />
)}
{error && <LoadingScreen status={errorMessage} spinner={false} />}
</div>
);
}
};
export default ComicBookViewer;

View file

@ -0,0 +1,17 @@
import React from 'react';
// Check if component is mounted, useful to prevent state updates after component unmounted
function useIsMounted() {
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
// Returning "isMounted.current" wouldn't work because we would return unmutable primitive
return isMounted;
}
export default useIsMounted;

View file

@ -1,36 +1,46 @@
// @flow
import React from 'react';
import useIsMounted from 'effects/use-is-mounted';
// Returns a blob from the download path
export default function useFileStream(fileStream: (?string) => any) {
export default function useFileStream(fileStream) {
const isMounted = useIsMounted();
const [state, setState] = React.useState({
error: false,
content: null,
loading: true,
content: null,
});
React.useEffect(() => {
if (fileStream) {
let chunks = []
if (fileStream && isMounted.current) {
let chunks = [];
const stream = fileStream();
// Handle steam chunk recived
stream.on('data', chunk => {
chunks.push(chunk)
if (isMounted.current) {
chunks.push(chunk);
} else {
// Cancel stream if component is not mounted:
// The user has left the viewer page
stream.destroy();
}
});
// Handle stream ended
stream.on('end', () => {
const buffer = Buffer.concat(chunks)
const blob = new Blob([buffer])
if (isMounted.current) {
const buffer = Buffer.concat(chunks);
const blob = new Blob([buffer]);
setState({ content: blob, loading: false });
}
});
// Handle stream error
stream.on('error', () => {
if (isMounted.current) {
setState({ error: true, loading: false });
}
});
}
}, []);
}, [fileStream, isMounted]);
return state;
}

View file

@ -1,45 +1,56 @@
// @flow
import React from 'react';
import https from 'https';
import useIsMounted from 'effects/use-is-mounted';
// Returns web blob from the streaming url
export default function useStream(url: (?string) => any) {
export default function useStream(url) {
const isMounted = useIsMounted();
const [state, setState] = React.useState({
error: false,
loading: true,
content: null,
loading: false,
});
React.useEffect(() => {
if (url) {
if (url && isMounted.current) {
https.get(url, response => {
if (isMounted && response.statusCode >= 200 && response.statusCode < 300) {
let chunks = [];
// Start loading state
setState({loading: true})
https.get(
url,
response => {
if (response.statusCode >= 200 && response.statusCode < 300) {
let chunks = []
// Handle stream chunk recived
response.on('data', function(chunk) {
if (isMounted.current) {
chunks.push(chunk);
} else {
// Cancel stream if component is not mounted:
// The user has left the viewer page
response.destroy();
}
});
// Handle stream ended
response.on('end', () => {
const buffer = Buffer.concat(chunks)
const blob = new Blob([buffer])
console.info(response)
if (isMounted.current) {
const buffer = Buffer.concat(chunks);
const blob = new Blob([buffer]);
console.info(response);
setState({ content: blob, loading: false });
}
});
// Handle stream error
response.on('error', () => {
if (isMounted.current) {
setState({ error: true, loading: false });
}
});
} else {
console.info(response)
// Handle network error
if (isMounted.current) {
setState({ error: true, loading: false });
}
}
);
});
}
}, []);
}, [url, isMounted]);
return state;
}