如何使用 Zustand 存储新的 Date().getTime() 方法并更新计时器而不每秒重新渲染组件?

问题描述 投票:0回答:1

秒表与 useState Hook 完美配合,但我需要使用全局状态管理来存储时间。我在我的应用程序中使用 Zustand,到目前为止,存储和更新状态没有任何问题,但现在我一直在寻找一种有效的解决方案来存储和更新秒表状态,而无需每秒重新渲染组件,但我还没有成功。

根据我所做的一些研究,我尝试实现 useRef、Transient Updates 和 subscribe 方法,但没有任何效果。该组件每秒都会重新渲染,从而影响最佳计时器性能。我希望在 Zustand 中存储和更新状态,而无需每秒重新渲染组件。谁能给我任何建议或提示来解决这个问题?预先感谢。

这是 GameClock1.tsx 组件:

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;`

这是我的 Zustand 商店 (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;
typescript react-native zustand
1个回答
0
投票

要使用 Zustand 存储新的 Date().getTime() 方法并更新计时器而不每秒重新渲染组件,您可以利用 Zustand 中间件进行瞬时更新。这是实现这一目标的简化方法:

修改您的 Zustand 商店:

从 'zustand' 导入 { create };

interface ClockState {
  start: number;
  now: number;
  laps: number[];
  setStart: (start: number) => void;
  setNow: (now: number) => void;
  addLap: (lap: number) => void;
  reset: () => void;
}

const useClockStore = create<ClockState>((set) => ({
  start: 0,
  now: 0,
  laps: [],
  setStart: (start) => set({ start }),
  setNow: (now) => set({ now }),
  addLap: (lap) => set((state) => ({ laps: [lap, ...state.laps] })),
  reset: () => set({ start: 0, now: 0, laps: [] }),
}));

export default useClockStore;

更新组件中的定时器逻辑

使用 useRef 和 useEffect 来管理计时器逻辑,而不会导致每次更新时重新渲染。

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.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.laps.map((lap, index) => (
        <Lap
          number={storeClock.laps.length - index}
          key={storeClock.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 { start, now, laps, setStart, setNow, addLap, reset } = useClockStore();
  const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);

  useEffect(() => {
    return () => {
      if (intervalIdRef.current) {
        clearInterval(intervalIdRef.current);
      }
    };
  }, []);

  const startTimer = () => {
    const nowTime = new Date().getTime();
    setStart(nowTime);
    setNow(nowTime);
    intervalIdRef.current = setInterval(() => {
      setNow(new Date().getTime());
    }, 100);
  };

  const lap = () => {
    const timestamp = new Date().getTime();
    addLap(now - start);
    setStart(timestamp);
    setNow(timestamp);
  };

  const stop = () => {
    if (intervalIdRef.current) {
      clearInterval(intervalIdRef.current);
    }
    addLap(now - start);
    setStart(0);
    setNow(0);
  };

  const resetTimer = () => {
    reset();
  };

  const resume = () => {
    const nowTime = new Date().getTime();
    setStart(nowTime);
    setNow(nowTime);
    intervalIdRef.current = setInterval(() => {
      setNow(new Date().getTime());
    }, 100);
  };

  const timer = now - start;

  return (
    <View style={styles.container}>
      <Timer
        interval={laps.reduce((total, curr) => total + curr, 0) + timer}
        style={styles.timer}
      />
      {laps.length === 0 && (
        <ButtonsRow>
          <RoundButton
            title="Lap"
            color="#8B8B90"
            background="#151515"
            disabled
          />
          <RoundButton
            title="Start"
            color="#50D167"
            background="#1B361F"
            onPress={startTimer}
          />
        </ButtonsRow>
      )}
      {start > 0 && (
        <ButtonsRow>
          <RoundButton
            title="Lap"
            color="#FFFFFF"
            background="#3D3D3D"
            onPress={lap}
          />
          <RoundButton
            title="Stop"
            color="#E33935"
            background="#3C1715"
            onPress={stop}
          />
        </ButtonsRow>
      )}
      {laps.length > 0 && start === 0 && (
        <ButtonsRow>
          <RoundButton
            title="Reset"
            color="#FFFFFF"
            background="#3D3D3D"
            onPress={resetTimer}
          />
          <RoundButton
            title="Start"
            color="#50D167"
            background="#1B361F"
            onPress={resume}
          />
        </ButtonsRow>
      )}
      <LapsTable laps={laps} timer={timer} />
    </View>
  );
}
© www.soinside.com 2019 - 2024. All rights reserved.