The stopwatch works perfectly with useState Hook, but I need to store the time using global state management. I’m using Zustand in my application and I have had no problems so far storing and updating states, but now I’ve been looking for days for an effective solution to store and update the stopwatch state without re-rendering the component every second, but I have not succeeded in it
I tried implementing useRef, Transient Updates, and the subscribe method according to some research I did, but nothing worked. The component keeps re-rendering every second impacting the optimal timer performance. I’m expecting to store and update the state in Zustand without re-rendering the component every second. Can anyone give me an advice or tip to solve this issue? Thank you in advance
Here is the GameClock1.tsx components:
import React, {useEffect, useRef} from 'react';
import {
StyleSheet,
Text,
View,
ScrollView,
TouchableOpacity,
} from 'react-native';
import moment from 'moment';
import useClockStore from '../../Store/GameClockStore';
interface TimerProps {
interval: number;
style: any;
}
function Timer({interval, style}: TimerProps) {
const pad = (n: number) => (n < 10 ? '0' + n : n);
const duration = moment.duration(interval);
const centiseconds = Math.floor(duration.milliseconds() / 10);
return (
<View style={styles.timerContainer}>
<Text style={style}>{pad(duration.minutes())}:</Text>
<Text style={style}>{pad(duration.seconds())},</Text>
<Text style={style}>{pad(centiseconds)}</Text>
</View>
);
}
interface RoundButtonProps {
title: string;
color: string;
background: string;
onPress?: () => void;
disabled?: boolean;
}
function RoundButton({
title,
color,
background,
onPress,
disabled,
}: RoundButtonProps) {
return (
<TouchableOpacity
onPress={() => !disabled && onPress && onPress()}
style={[styles.button, {backgroundColor: background}]}
activeOpacity={disabled ? 1.0 : 0.7}>
<View style={styles.buttonBorder}>
<Text style={[styles.buttonTitle, {color}]}>{title}</Text>
</View>
</TouchableOpacity>
);
}
interface LapProps {
number: number;
interval: number;
fastest: boolean;
slowest: boolean;
}
function Lap({number, interval, fastest, slowest}: LapProps) {
const lapStyle = [
styles.lapText,
fastest && styles.fastest,
slowest && styles.slowest,
];
return (
<View style={styles.lap}>
<Text style={lapStyle}>Lap {number}</Text>
<Timer style={[lapStyle, styles.lapTimer]} interval={interval} />
</View>
);
}
interface LapsTableProps {
laps: number[];
timer: number;
}
function LapsTable({laps, timer}: LapsTableProps) {
const storeClock = useClockStore();
const finishedLaps = storeClock.state.laps?.slice(1);
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
if (finishedLaps?.length >= 2) {
finishedLaps.forEach(lap => {
if (lap < min) {
min = lap;
}
if (lap > max) {
max = lap;
}
});
}
return (
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{storeClock.state.laps?.map((lap, index) => (
<Lap
number={storeClock.state.laps.length - index}
key={storeClock.state.laps.length - index}
interval={index === 0 ? timer + lap : lap}
fastest={lap === min}
slowest={lap === max}
/>
))}
</ScrollView>
);
}
interface ButtonsRowProps {
children: React.ReactNode;
}
function ButtonsRow({children}: ButtonsRowProps) {
return <View style={styles.buttonsRow}>{children}</View>;
}
function GameClock1() {
const storeClock = useClockStore();
const intervalIdRef = useRef<ReturnType<typeof setInterval> | number | null>(
null,
);
useEffect(() => {
return () => {
clearInterval(intervalIdRef.current as number);
};
}, []);
const start = () => {
const now = new Date().getTime();
storeClock.setState({
start: now,
now,
laps: [0],
});
intervalIdRef.current = setInterval(() => {
storeClock.setState({
now: new Date().getTime(),
});
}, 100);
};
const lap = () => {
const timestamp = new Date().getTime();
const [firstLap, ...other] = storeClock.state.laps;
storeClock.setState({
laps: [
0,
firstLap + storeClock.state.now - storeClock.state.start,
...other,
],
start: timestamp,
now: timestamp,
});
};
const stop = () => {
clearInterval(intervalIdRef.current as number);
const [firstLap, ...other] = storeClock.state.laps;
storeClock.setState({
laps: [
firstLap + storeClock.state.now - storeClock.state.start,
...other,
],
start: 0,
now: 0,
});
};
const reset = () => {
storeClock.setState({
laps: [],
start: 0,
now: 0,
});
};
const resume = () => {
const now = new Date().getTime();
const [...other] = storeClock.state.laps;
storeClock.setState({
start: now,
now,
laps: [...other],
});
intervalIdRef.current = setInterval(() => {
storeClock.setState({
now: new Date().getTime(),
});
}, 100);
};
const timer = storeClock.state.now - storeClock.state.start;
return (
<View style={styles.container}>
<Timer
interval={
storeClock.state.laps?.reduce((total, curr) => total + curr, 0) +
timer
}
style={styles.timer}
/>
{storeClock.state.laps?.length === 0 && (
<ButtonsRow>
<RoundButton
title="Lap"
color="#8B8B90"
background="#151515"
disabled
/>
<RoundButton
title="Start"
color="#50D167"
background="#1B361F"
onPress={start}
/>
</ButtonsRow>
)}
{storeClock.state.start > 0 && (
<ButtonsRow>
<RoundButton
title="Lap"
color="#FFFFFF"
background="#3D3D3D"
onPress={lap}
/>
<RoundButton
title="Stop"
color="#E33935"
background="#3C1715"
onPress={stop}
/>
</ButtonsRow>
)}
{storeClock.state.laps?.length > 0 && storeClock.state.start === 0 && (
<ButtonsRow>
<RoundButton
title="Reset"
color="#FFFFFF"
background="#3D3D3D"
onPress={reset}
/>
<RoundButton
title="Start"
color="#50D167"
background="#1B361F"
onPress={resume}
/>
</ButtonsRow>
)}
<LapsTable laps={storeClock.state.laps} timer={timer} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0D0D0D',
alignItems: 'center',
paddingTop: 130,
paddingHorizontal: 20,
},
timer: {
color: '#FFFFFF',
fontSize: 76,
fontWeight: '200',
width: 110,
},
button: {
width: 80,
height: 80,
borderRadius: 40,
justifyContent: 'center',
alignItems: 'center',
},
buttonTitle: {
fontSize: 18,
},
buttonBorder: {
width: 76,
height: 76,
borderRadius: 38,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
},
buttonsRow: {
flexDirection: 'row',
alignSelf: 'stretch',
justifyContent: 'space-between',
marginTop: 80,
marginBottom: 30,
},
lapText: {
color: '#FFFFFF',
fontSize: 18,
},
lapTimer: {
width: 30,
},
lap: {
flexDirection: 'row',
justifyContent: 'space-between',
borderColor: '#151515',
borderTopWidth: 1,
paddingVertical: 10,
},
scrollView: {
alignSelf: 'stretch',
},
fastest: {
color: '#4BC05F',
},
slowest: {
color: '#CC3531',
},
timerContainer: {
flexDirection: 'row',
},
});
export default GameClock1;`
Here is my Zustand store (GameClockStore.tsx):
import {create} from 'zustand';
export interface ClockState {
state: {
start: number;
now: number;
laps: number[];
};
}
export interface Actions {
setState: (newState: Partial<ClockState['state']>) => void;
}
const useClockStore = create<ClockState & Actions>()(set => ({
state: {
start: 0,
now: 0,
laps: [],
},
setState: newState => set(state => ({state: {...state.state, ...newState}})),
}))
export default useClockStore;