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'; import LoadingScreen from 'component/common/loading-screen';
// @if TARGET='web' // @if TARGET='web'
import useStream from 'effects/use-stream' import useStream from 'effects/use-stream';
// @endif // @endif
// @if TARGET='app' // @if TARGET='app'
import useFileStream from 'effects/use-stream-file' import useFileStream from 'effects/use-stream-file';
// @endif // @endif
// Import default styles for Villain // Import default styles for Villain
@ -29,22 +29,18 @@ if (process.env.NODE_ENV !== 'production') {
workerUrl = `/${workerUrl}`; workerUrl = `/${workerUrl}`;
} }
const ComicBookViewer = ( props: Props) => { const ComicBookViewer = (props: Props) => {
const { source, theme } = props const { source, theme } = props;
const { stream, file } = source let finalSource;
// @if TARGET='web' // @if TARGET='web'
const finalSource = useStream(stream) finalSource = useStream(source.stream);
// @endif // @endif
// @if TARGET='app' // @if TARGET='app'
const finalSource = useFileStream(file) finalSource = useFileStream(source.file);
// @endif // @endif
const { error, loading, content } = finalSource
const ready = content !== null && !loading
// Villain options // Villain options
const opts = { const opts = {
theme: theme === 'dark' ? 'Dark' : 'Light', theme: theme === 'dark' ? 'Dark' : 'Light',
@ -53,15 +49,19 @@ const ComicBookViewer = ( props: Props) => {
allowGlobalShortcuts: true, allowGlobalShortcuts: true,
}; };
const { error, loading, content } = finalSource;
const ready = content !== null && !loading;
const errorMessage = __("Sorry, looks like we can't load the archive."); const errorMessage = __("Sorry, looks like we can't load the archive.");
return ( return (
<div className="file-render__viewer file-render__viewer--comic"> <div className="file-render__viewer file-render__viewer--comic">
{ loading && <LoadingScreen status={__('Loading')} />} {loading && <LoadingScreen status={__('Loading')} />}
{ ready && <Villain source={finalSource.content} className={'comic-viewer'} options={opts} workerUrl={workerUrl} /> } {ready && (
{ error && <LoadingScreen status={errorMessage} spinner={false} /> } <Villain source={finalSource.content} className={'comic-viewer'} options={opts} workerUrl={workerUrl} />
)}
{error && <LoadingScreen status={errorMessage} spinner={false} />}
</div> </div>
); );
} };
export default ComicBookViewer; 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 React from 'react';
import useIsMounted from 'effects/use-is-mounted';
// Returns a blob from the download path // 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({ const [state, setState] = React.useState({
error: false, error: false,
content: null,
loading: true, loading: true,
content: null,
}); });
React.useEffect(() => { React.useEffect(() => {
if (fileStream) { if (fileStream && isMounted.current) {
let chunks = [] let chunks = [];
const stream = fileStream(); const stream = fileStream();
// Handle steam chunk recived
stream.on('data', chunk => { 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', () => { stream.on('end', () => {
const buffer = Buffer.concat(chunks) if (isMounted.current) {
const blob = new Blob([buffer]) const buffer = Buffer.concat(chunks);
const blob = new Blob([buffer]);
setState({ content: blob, loading: false }); setState({ content: blob, loading: false });
}
}); });
// Handle stream error
stream.on('error', () => { stream.on('error', () => {
if (isMounted.current) {
setState({ error: true, loading: false }); setState({ error: true, loading: false });
}
}); });
} }
}, []); }, [fileStream, isMounted]);
return state; return state;
} }

View file

@ -1,45 +1,56 @@
// @flow
import React from 'react'; import React from 'react';
import https from 'https'; import https from 'https';
import useIsMounted from 'effects/use-is-mounted';
// Returns web blob from the streaming url // 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({ const [state, setState] = React.useState({
error: false, error: false,
loading: true,
content: null, content: null,
loading: false,
}); });
React.useEffect(() => { React.useEffect(() => {
if (url) { if (url && isMounted.current) {
https.get(url, response => {
if (isMounted && response.statusCode >= 200 && response.statusCode < 300) {
let chunks = []; let chunks = [];
// Handle stream chunk recived
// Start loading state
setState({loading: true})
https.get(
url,
response => {
if (response.statusCode >= 200 && response.statusCode < 300) {
let chunks = []
response.on('data', function(chunk) { response.on('data', function(chunk) {
if (isMounted.current) {
chunks.push(chunk); 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', () => { response.on('end', () => {
const buffer = Buffer.concat(chunks) if (isMounted.current) {
const blob = new Blob([buffer]) const buffer = Buffer.concat(chunks);
console.info(response) const blob = new Blob([buffer]);
console.info(response);
setState({ content: blob, loading: false }); setState({ content: blob, loading: false });
}
});
// Handle stream error
response.on('error', () => {
if (isMounted.current) {
setState({ error: true, loading: false });
}
}); });
} else { } else {
console.info(response) // Handle network error
if (isMounted.current) {
setState({ error: true, loading: false }); setState({ error: true, loading: false });
} }
} }
); });
} }
}, []); }, [url, isMounted]);
return state; return state;
} }