prevent state updates after component unmounted
This commit is contained in:
parent
c8f21025d1
commit
036cf734c6
5 changed files with 110 additions and 72 deletions
|
@ -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,39 +29,39 @@ 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
|
// Villain options
|
||||||
|
const opts = {
|
||||||
|
theme: theme === 'dark' ? 'Dark' : 'Light',
|
||||||
|
allowFullScreen: true,
|
||||||
|
autoHideControls: false,
|
||||||
|
allowGlobalShortcuts: true,
|
||||||
|
};
|
||||||
|
|
||||||
const ready = content !== null && !loading
|
const { error, loading, content } = finalSource;
|
||||||
|
const ready = content !== null && !loading;
|
||||||
|
const errorMessage = __("Sorry, looks like we can't load the archive.");
|
||||||
|
|
||||||
// Villain options
|
return (
|
||||||
const opts = {
|
<div className="file-render__viewer file-render__viewer--comic">
|
||||||
theme: theme === 'dark' ? 'Dark' : 'Light',
|
{loading && <LoadingScreen status={__('Loading')} />}
|
||||||
allowFullScreen: true,
|
{ready && (
|
||||||
autoHideControls: false,
|
<Villain source={finalSource.content} className={'comic-viewer'} options={opts} workerUrl={workerUrl} />
|
||||||
allowGlobalShortcuts: true,
|
)}
|
||||||
};
|
{error && <LoadingScreen status={errorMessage} spinner={false} />}
|
||||||
|
</div>
|
||||||
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} /> }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComicBookViewer;
|
export default ComicBookViewer;
|
||||||
|
|
17
ui/effects/use-is-mounted.js
Normal file
17
ui/effects/use-is-mounted.js
Normal 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;
|
|
@ -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);
|
||||||
setState({ content: blob, loading: false });
|
const blob = new Blob([buffer]);
|
||||||
|
setState({ content: blob, loading: false });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// Handle stream error
|
||||||
stream.on('error', () => {
|
stream.on('error', () => {
|
||||||
setState({ error: true, loading: false });
|
if (isMounted.current) {
|
||||||
|
setState({ error: true, loading: false });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [fileStream, isMounted]);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
let chunks = [];
|
https.get(url, response => {
|
||||||
|
if (isMounted && response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
// Start loading state
|
let chunks = [];
|
||||||
setState({loading: true})
|
// Handle stream chunk recived
|
||||||
|
response.on('data', function(chunk) {
|
||||||
https.get(
|
if (isMounted.current) {
|
||||||
url,
|
|
||||||
response => {
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
||||||
let chunks = []
|
|
||||||
response.on('data', function(chunk) {
|
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
});
|
} else {
|
||||||
response.on('end', () => {
|
// Cancel stream if component is not mounted:
|
||||||
const buffer = Buffer.concat(chunks)
|
// The user has left the viewer page
|
||||||
const blob = new Blob([buffer])
|
response.destroy();
|
||||||
console.info(response)
|
}
|
||||||
|
});
|
||||||
|
// Handle stream ended
|
||||||
|
response.on('end', () => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const blob = new Blob([buffer]);
|
||||||
|
console.info(response);
|
||||||
setState({ content: blob, loading: false });
|
setState({ content: blob, loading: false });
|
||||||
});
|
}
|
||||||
} else {
|
});
|
||||||
console.info(response)
|
// Handle stream error
|
||||||
|
response.on('error', () => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setState({ error: true, loading: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Handle network error
|
||||||
|
if (isMounted.current) {
|
||||||
setState({ error: true, loading: false });
|
setState({ error: true, loading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [url, isMounted]);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue