From 229b2cb2337ca4123c5317568c31c7cc59debe8d Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 21 Jul 2026 14:03:44 +0200 Subject: [PATCH] cleanup: UNSAFE_componentWillReceiveProps -> modern lifecycles (React 19) The 4 class components on UNSAFE_componentWillReceiveProps converted: - FromNow, FireStats: state is a pure mirror of props -> getDerivedStateFromProps - Fires: split the derived state (getDerivedStateFromProps) from the URL- canonicalization side effect (componentDidUpdate, guarded by prevProps) so the side effect never runs inside the pure gDSFP - SelectionMap: marker is also locally draggable, so gDSFP would clobber a drag -> componentDidUpdate guarded on a real center/distance value change (no clobber, no setState loop), merged with the existing fit() Browser-verified: /fires count, fire detail + FromNow, active-fire URL canonicalizes to /fire/archive/:id, home SelectionMap renders stably (no render loop). No UNSAFE_ warnings remain from our code. REST smoke byte-identical. --- imports/ui/components/FromNow/FromNow.js | 14 ++------- .../components/SelectionMap/SelectionMap.js | 28 ++++++++++-------- imports/ui/pages/Fires/Fires.js | 29 ++++++++++++------- imports/ui/pages/FiresMap/FireStats.js | 23 +++++++-------- 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/imports/ui/components/FromNow/FromNow.js b/imports/ui/components/FromNow/FromNow.js index df6e4c1..68afa23 100644 --- a/imports/ui/components/FromNow/FromNow.js +++ b/imports/ui/components/FromNow/FromNow.js @@ -18,13 +18,9 @@ class FromNow extends Component { }; } - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.when !== nextProps.when) { - // console.log(`Next when ${nextProps.when}`); - this.setState({ - when: nextProps.when - }); - } + // state.when just mirrors props.when (was UNSAFE_componentWillReceiveProps) + static getDerivedStateFromProps(props, state) { + return props.when !== state.when ? { when: props.when } : null; } shouldComponentUpdate(nextProps, nextState) { @@ -36,7 +32,6 @@ class FromNow extends Component { } render() { - console.log(`Render from now ${this.state.when}`); return ( {this.state.when} ); @@ -49,9 +44,6 @@ FromNow.propTypes = { whenDate: PropTypes.instanceOf(Date) }; -FromNow.defaultProps = { -}; - /* * const FromNowContainer = withTracker((props) => { * diff --git a/imports/ui/components/SelectionMap/SelectionMap.js b/imports/ui/components/SelectionMap/SelectionMap.js index bfddc6f..9726a31 100644 --- a/imports/ui/components/SelectionMap/SelectionMap.js +++ b/imports/ui/components/SelectionMap/SelectionMap.js @@ -60,18 +60,22 @@ class SelectionMap extends Component { } } - UNSAFE_componentWillReceiveProps(nextProps) { - const nextCenter = nextProps.center[0] ? nextProps.center : this.state.center; - const nextMarker = nextProps.center[0] ? nextProps.center : this.state.marker; - this.setState({ - center: nextCenter, - marker: nextMarker, - distance: nextProps.distance || this.state.distance - }); - // this.fit(); - } - - componentDidUpdate() { + // Was UNSAFE_componentWillReceiveProps: pull center/distance from props into + // state when they actually change. Done in componentDidUpdate (not + // getDerivedStateFromProps) because the marker is also locally draggable, so + // we must guard on a real value change to avoid clobbering a drag / looping. + componentDidUpdate(prevProps) { + const { center, distance } = this.props; + const centerChanged = center[0] && + (center[0] !== this.state.center[0] || center[1] !== this.state.center[1]); + const distanceChanged = distance && distance !== this.state.distance; + if (centerChanged || distanceChanged) { + this.setState({ + center: center[0] ? center : this.state.center, + marker: center[0] ? center : this.state.marker, + distance: distance || this.state.distance + }); + } this.fit(); } diff --git a/imports/ui/pages/Fires/Fires.js b/imports/ui/pages/Fires/Fires.js index e005134..66b227a 100644 --- a/imports/ui/pages/Fires/Fires.js +++ b/imports/ui/pages/Fires/Fires.js @@ -37,18 +37,25 @@ class Fire extends React.Component { }; } - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.when !== nextProps.when || this.props.loading !== nextProps.loading || this.props.notfound !== nextProps.notfound) { - // console.log(`Next when ${nextProps.when}`); - if (nextProps.fire && (nextProps.alert || nextProps.active || nextProps.fromHash)) { - // change url to archive with new _id - nextProps.history.replace(`/fire/archive/${hexId(nextProps.fire._id)}`); + // state mirrors props purely for shouldComponentUpdate (render reads props + // directly). Was the derived-state half of UNSAFE_componentWillReceiveProps. + static getDerivedStateFromProps(props, state) { + if (props.when !== state.when || props.loading !== state.loading || props.notfound !== state.notfound) { + return { loading: props.loading, notfound: props.notfound, when: props.when }; + } + return null; + } + + componentDidUpdate(prevProps) { + // side effect (must not run in getDerivedStateFromProps): once the fire is + // resolved via alert/active/hash, canonicalize the URL to /fire/archive/:id + if (prevProps.when !== this.props.when || prevProps.loading !== this.props.loading || prevProps.notfound !== this.props.notfound) { + const { + fire, alert, active, fromHash, history + } = this.props; + if (fire && (alert || active || fromHash)) { + history.replace(`/fire/archive/${hexId(fire._id)}`); } - this.setState({ - loading: nextProps.loading, - notfound: nextProps.notfound, - when: nextProps.when - }); } } diff --git a/imports/ui/pages/FiresMap/FireStats.js b/imports/ui/pages/FiresMap/FireStats.js index f849362..f850bbb 100644 --- a/imports/ui/pages/FiresMap/FireStats.js +++ b/imports/ui/pages/FiresMap/FireStats.js @@ -17,17 +17,19 @@ class FireStats extends Component { }; } - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.lastCheck !== nextProps.lastCheck || - this.props.lastFireDetected !== nextProps.lastFireDetected || - this.props.loadingAll !== nextProps.loadingAll + // state mirrors props (was UNSAFE_componentWillReceiveProps) + static getDerivedStateFromProps(props, state) { + if (props.lastCheck !== state.lastCheck || + props.lastFireDetected !== state.lastFireDetected || + props.loadingAll !== state.loadingAll ) { - this.setState({ - lastFireDetected: nextProps.lastFireDetected, - lastCheck: nextProps.lastCheck, - loadingAll: nextProps.loadingAll - }); + return { + lastFireDetected: props.lastFireDetected, + lastCheck: props.lastCheck, + loadingAll: props.loadingAll + }; } + return null; } shouldComponentUpdate(nextProps, nextState) { @@ -54,7 +56,4 @@ FireStats.propTypes = { lastCheck: PropTypes.instanceOf(Date) }; -FireStats.defaultProps = { -}; - export default withTranslation()(FireStats);