mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 10:22:31 +01:00
improve renderibng performance #1881
This commit is contained in:
parent
d5fbac6f11
commit
18785e1f88
@ -31,6 +31,38 @@ const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z);
|
||||
|
||||
const leftRightWidth = 100;
|
||||
|
||||
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onYinYangClick = useCallback(() => {
|
||||
setInvertCutSegments(v => {
|
||||
const newVal = !v;
|
||||
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
|
||||
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
|
||||
return newVal;
|
||||
});
|
||||
}, [setInvertCutSegments, t]);
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: 5 }}>
|
||||
<motion.div
|
||||
style={{ width: 24, height: 24 }}
|
||||
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<FaYinYang
|
||||
size={24}
|
||||
role="button"
|
||||
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
|
||||
style={{ color: invertCutSegments ? primaryTextColor : undefined }}
|
||||
onClick={onYinYangClick}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
@ -179,15 +211,6 @@ const BottomBar = memo(({
|
||||
|
||||
const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings();
|
||||
|
||||
const onYinYangClick = useCallback(() => {
|
||||
setInvertCutSegments(v => {
|
||||
const newVal = !v;
|
||||
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
|
||||
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
|
||||
return newVal;
|
||||
});
|
||||
}, [setInvertCutSegments, t]);
|
||||
|
||||
const rotationStr = `${rotation}°`;
|
||||
|
||||
useEffect(() => {
|
||||
@ -365,21 +388,7 @@ const BottomBar = memo(({
|
||||
|
||||
{!simpleMode && (
|
||||
<>
|
||||
<div style={{ marginLeft: 5 }}>
|
||||
<motion.div
|
||||
style={{ width: 24, height: 24 }}
|
||||
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<FaYinYang
|
||||
size={24}
|
||||
role="button"
|
||||
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
|
||||
style={{ color: invertCutSegments ? primaryTextColor : undefined }}
|
||||
onClick={onYinYangClick}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
<InvertCutModeButton invertCutSegments={invertCutSegments} setInvertCutSegments={setInvertCutSegments} />
|
||||
|
||||
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div>
|
||||
|
||||
|
@ -24,8 +24,7 @@ const buttonBaseStyle = {
|
||||
|
||||
const neutralButtonColor = 'var(--gray8)';
|
||||
|
||||
|
||||
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
|
||||
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateSegOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
@ -33,15 +32,18 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
|
||||
|
||||
const contextMenuTemplate = useMemo(() => {
|
||||
if (invertCutSegments) return [];
|
||||
|
||||
const updateOrder = (dir) => updateSegOrder(index, index + dir);
|
||||
|
||||
return [
|
||||
{ label: t('Jump to start time'), click: jumpSegStart },
|
||||
{ label: t('Jump to end time'), click: jumpSegEnd },
|
||||
{ label: t('Jump to start time'), click: () => jumpSegStart(index) },
|
||||
{ label: t('Jump to end time'), click: () => jumpSegEnd(index) },
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{ label: t('Add segment'), click: addSegment },
|
||||
{ label: t('Label segment'), click: onLabelPress },
|
||||
{ label: t('Remove segment'), click: onRemovePress },
|
||||
{ label: t('Label segment'), click: () => onLabelPress(index) },
|
||||
{ label: t('Remove segment'), click: () => onRemovePress(index) },
|
||||
{ label: t('Duplicate segment'), click: () => onDuplicateSegmentClick(seg) },
|
||||
|
||||
{ type: 'separator' },
|
||||
@ -60,7 +62,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{ label: t('Change segment order'), click: onReorderPress },
|
||||
{ label: t('Change segment order'), click: () => onReorderPress(index) },
|
||||
{ label: t('Increase segment order'), click: () => updateOrder(1) },
|
||||
{ label: t('Decrease segment order'), click: () => updateOrder(-1) },
|
||||
|
||||
@ -69,7 +71,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
|
||||
{ label: t('Segment tags'), click: () => onEditSegmentTags(index) },
|
||||
{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) },
|
||||
];
|
||||
}, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, updateOrder, onEditSegmentTags, index, onExtractSegmentFramesAsImages]);
|
||||
}, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]);
|
||||
|
||||
useContextMenu(ref, contextMenuTemplate);
|
||||
|
||||
@ -95,10 +97,10 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
|
||||
|
||||
const timeStr = useMemo(() => `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}`, [seg.start, seg.end, formatTimecode]);
|
||||
|
||||
function onDoubleClick() {
|
||||
const onDoubleClick = useCallback(() => {
|
||||
if (invertCutSegments) return;
|
||||
jumpSegStart();
|
||||
}
|
||||
jumpSegStart(index);
|
||||
}, [index, invertCutSegments, jumpSegStart]);
|
||||
|
||||
const durationMsFormatted = Math.floor(durationMs);
|
||||
const frameCount = getFrameCount(duration);
|
||||
@ -114,14 +116,18 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
|
||||
|
||||
const tags = useMemo(() => getSegmentTags(seg), [seg]);
|
||||
|
||||
const maybeOnClick = useCallback(() => !invertCutSegments && onClick(index), [index, invertCutSegments, onClick]);
|
||||
|
||||
const motionStyle = useMemo(() => ({ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }), [isActive]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
role="button"
|
||||
onClick={() => !invertCutSegments && onClick(index)}
|
||||
onClick={maybeOnClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
layout
|
||||
style={{ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }}
|
||||
style={motionStyle}
|
||||
initial={{ scaleY: 0 }}
|
||||
animate={{ scaleY: 1, opacity: !selected && !invertCutSegments ? 0.5 : undefined }}
|
||||
exit={{ scaleY: 0 }}
|
||||
@ -171,7 +177,7 @@ const SegmentList = memo(({
|
||||
|
||||
const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
|
||||
|
||||
const sortableList = segments.map((seg) => ({ id: seg.segId, seg }));
|
||||
const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]);
|
||||
|
||||
const setSortableList = useCallback((newList) => {
|
||||
if (isEqual(segments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
|
||||
@ -189,7 +195,7 @@ const SegmentList = memo(({
|
||||
}
|
||||
}
|
||||
|
||||
async function onReorderSegs(index) {
|
||||
const onReorderSegs = useCallback(async (index) => {
|
||||
if (apparentCutSegments.length < 2) return;
|
||||
const { value } = await Swal.fire({
|
||||
title: `${t('Change order of segment')} ${index + 1}`,
|
||||
@ -207,7 +213,7 @@ const SegmentList = memo(({
|
||||
const newOrder = parseInt(value, 10);
|
||||
updateSegOrder(index, newOrder - 1);
|
||||
}
|
||||
}
|
||||
}, [apparentCutSegments.length, t, updateSegOrder]);
|
||||
|
||||
function renderFooter() {
|
||||
const getButtonColor = (seg) => getSegColor(seg).desaturate(0.3).lightness(darkMode ? 45 : 55).string();
|
||||
@ -340,12 +346,12 @@ const SegmentList = memo(({
|
||||
onClick={onSegClick}
|
||||
addSegment={addSegment}
|
||||
onRemoveSelected={onRemoveSelected}
|
||||
onRemovePress={() => removeCutSegment(index)}
|
||||
onReorderPress={() => onReorderSegs(index)}
|
||||
onLabelPress={() => onLabelSegment(index)}
|
||||
jumpSegStart={() => jumpSegStart(index)}
|
||||
jumpSegEnd={() => jumpSegEnd(index)}
|
||||
updateOrder={(dir) => updateSegOrder(index, index + dir)}
|
||||
onRemovePress={removeCutSegment}
|
||||
onReorderPress={onReorderSegs}
|
||||
onLabelPress={onLabelSegment}
|
||||
jumpSegStart={jumpSegStart}
|
||||
jumpSegEnd={jumpSegEnd}
|
||||
updateSegOrder={updateSegOrder}
|
||||
getFrameCount={getFrameCount}
|
||||
formatTimecode={formatTimecode}
|
||||
currentSegIndex={currentSegIndex}
|
||||
|
@ -52,6 +52,11 @@ const CommandedTime = memo(({ commandedTimePercent }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const timelineHeight = 36;
|
||||
|
||||
const timeWrapperStyle = { position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' };
|
||||
const timeStyle = { background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' };
|
||||
|
||||
const Timeline = memo(({
|
||||
durationSafe, startTimeOffset, playerTime, commandedTime, relevantTime,
|
||||
zoom, neighbouringKeyFrames, seekAbs, apparentCutSegments,
|
||||
@ -62,8 +67,6 @@ const Timeline = memo(({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const timelineHeight = 36;
|
||||
|
||||
const { invertCutSegments } = useUserSettings();
|
||||
|
||||
const timelineScrollerRef = useRef();
|
||||
@ -328,8 +331,8 @@ const Timeline = memo(({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
|
||||
<div style={{ background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
<div style={timeWrapperStyle}>
|
||||
<div style={timeStyle}>
|
||||
{formatTimeAndFrames(displayTime)}{isZoomed ? ` ${displayTimePercent}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { IoIosSettings } from 'react-icons/io';
|
||||
import { FaLock, FaUnlock } from 'react-icons/fa';
|
||||
import { IconButton, Button, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
|
||||
import { IconButton, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from './components/Button';
|
||||
|
||||
import ExportModeButton from './components/ExportModeButton';
|
||||
|
||||
@ -11,6 +12,9 @@ import { primaryTextColor, controlsBackground, darkModeTransition } from './colo
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
|
||||
|
||||
const outFmtStyle = { height: 20, maxWidth: 100 };
|
||||
const exportModeStyle = { flexGrow: 0, flexBasis: 140 };
|
||||
|
||||
const TopMenu = memo(({
|
||||
filePath, fileFormat, copyAnyAudioTrack, toggleStripAudio,
|
||||
renderOutFmt, numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings,
|
||||
@ -35,17 +39,20 @@ const TopMenu = memo(({
|
||||
>
|
||||
{filePath && (
|
||||
<>
|
||||
<Button height={20} iconBefore={ListIcon} onClick={withBlur(() => setStreamsSelectorShown(true))}>
|
||||
<Button onClick={withBlur(() => setStreamsSelectorShown(true))}>
|
||||
<ListIcon size="1em" verticalAlign="middle" marginRight=".3em" />
|
||||
{t('Tracks')} ({numStreamsToCopy}/{numStreamsTotal})
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
iconBefore={copyAnyAudioTrack ? VolumeUpIcon : VolumeOffIcon}
|
||||
height={20}
|
||||
title={copyAnyAudioTrack ? t('Keep audio tracks') : t('Discard audio tracks')}
|
||||
onClick={withBlur(toggleStripAudio)}
|
||||
>
|
||||
{copyAnyAudioTrack ? t('Keep audio') : t('Discard audio')}
|
||||
{copyAnyAudioTrack ? (
|
||||
<><VolumeUpIcon size="1em" verticalAlign="middle" marginRight=".3em" />{t('Keep audio')}</>
|
||||
) : (
|
||||
<><VolumeOffIcon size="1em" verticalAlign="middle" marginRight=".3em" />{t('Discard audio')}</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@ -53,35 +60,34 @@ const TopMenu = memo(({
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
|
||||
{showClearWorkingDirButton && (
|
||||
<IconButton
|
||||
intent="danger"
|
||||
icon={CrossIcon}
|
||||
height={20}
|
||||
<CrossIcon
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ width: 20 }}
|
||||
onClick={withBlur(clearOutDir)}
|
||||
title={t('Clear working directory')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
height={20}
|
||||
onClick={withBlur(changeOutDir)}
|
||||
title={customOutDir}
|
||||
paddingLeft={showClearWorkingDirButton ? 4 : undefined}
|
||||
style={{ paddingLeft: showClearWorkingDirButton ? 4 : undefined }}
|
||||
>
|
||||
{customOutDir ? t('Working dir set') : t('Working dir unset')}
|
||||
</Button>
|
||||
|
||||
{filePath && (
|
||||
<>
|
||||
{renderOutFmt({ height: 20, maxWidth: 100 })}
|
||||
{renderOutFmt(outFmtStyle)}
|
||||
|
||||
{!simpleMode && (isCustomFormatSelected || outFormatLocked) && renderFormatLock()}
|
||||
|
||||
<ExportModeButton selectedSegments={selectedSegments} style={{ flexGrow: 0, flexBasis: 140 }} />
|
||||
<ExportModeButton selectedSegments={selectedSegments} style={exportModeStyle} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ verticalAlign: 'middle', marginLeft: 5 }} />
|
||||
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ marginLeft: 5 }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
10
src/components/Button.jsx
Normal file
10
src/components/Button.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import styles from './Button.module.css';
|
||||
|
||||
const Button = memo(({ type = 'button', ...props }) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type
|
||||
<button className={styles.button} type={type} {...props} />
|
||||
));
|
||||
|
||||
export default Button;
|
12
src/components/Button.module.css
Normal file
12
src/components/Button.module.css
Normal file
@ -0,0 +1,12 @@
|
||||
.button {
|
||||
appearance: none;
|
||||
font: inherit;
|
||||
line-height: 140%;
|
||||
font-size: .8em;
|
||||
background-color: var(--gray3);
|
||||
color: var(--gray12);
|
||||
border-radius: .3em;
|
||||
padding: 0 .5em 0 .3em;
|
||||
outline: .05em solid var(--gray8);
|
||||
border: .05em solid var(--gray7);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import dataUriToBuffer from 'data-uri-to-buffer';
|
||||
import pMap from 'p-map';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp, fsOperationWithRetry } from '../util';
|
||||
import { getNumDigits } from '../segments';
|
||||
@ -23,7 +24,7 @@ function getFrameFromVideo(video, format, quality) {
|
||||
}
|
||||
|
||||
export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
|
||||
async function captureFramesRange({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) {
|
||||
const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) => {
|
||||
const getSuffix = (prefix) => `${prefix}.${captureFormat}`;
|
||||
|
||||
if (!outputTimestamps) {
|
||||
@ -67,9 +68,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
|
||||
}, { concurrency: 1 });
|
||||
|
||||
return outPaths[0];
|
||||
}
|
||||
}, [formatTimecode]);
|
||||
|
||||
async function captureFrameFromFfmpeg({ customOutDir, filePath, fromTime, captureFormat, quality }) {
|
||||
const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }) => {
|
||||
const time = formatTimecode({ seconds: fromTime, fileNameFriendly: true });
|
||||
const nameSuffix = `${time}.${captureFormat}`;
|
||||
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
|
||||
@ -77,9 +78,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
|
||||
|
||||
await transferTimestamps({ inPath: filePath, outPath, cutFrom: fromTime, treatOutputFileModifiedTimeAsStart });
|
||||
return outPath;
|
||||
}
|
||||
}, [formatTimecode, treatOutputFileModifiedTimeAsStart]);
|
||||
|
||||
async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, quality }) {
|
||||
const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }) => {
|
||||
const buf = getFrameFromVideo(video, captureFormat, quality);
|
||||
|
||||
const ext = mime.extension(buf.type);
|
||||
@ -90,7 +91,7 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
|
||||
|
||||
await transferTimestamps({ inPath: filePath, outPath, cutFrom: currentTime, treatOutputFileModifiedTimeAsStart });
|
||||
return outPath;
|
||||
}
|
||||
}, [formatTimecode, treatOutputFileModifiedTimeAsStart]);
|
||||
|
||||
return {
|
||||
captureFramesRange,
|
||||
|
29
src/hooks/useWhatChanged.js
Normal file
29
src/hooks/useWhatChanged.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
// https://stackoverflow.com/questions/64997362/how-do-i-see-what-props-have-changed-in-react
|
||||
export default function useWhatChanged(props) {
|
||||
// cache the last set of props
|
||||
const prev = React.useRef(props);
|
||||
|
||||
React.useEffect(() => {
|
||||
// check each prop to see if it has changed
|
||||
const changed = Object.entries(props).reduce((a, [key, prop]) => {
|
||||
if (prev.current[key] === prop) return a;
|
||||
return {
|
||||
...a,
|
||||
[key]: {
|
||||
prev: prev.current[key],
|
||||
next: prop,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
if (Object.keys(changed).length > 0) {
|
||||
console.group('Props That Changed');
|
||||
console.log(changed);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
prev.current = props;
|
||||
}, [props]);
|
||||
}
|
Loading…
Reference in New Issue
Block a user