You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Redux应用useEffect内状态初始化异常问题求助

React Redux 应用状态初始化与 localStorage 同步问题

应用状态结构

全局状态包含两个核心部分:

  • preferences.time:默认值0,存储用户设置的时间选项
  • time.timerId:默认值null,存储定时器ID

Actions 定义

export const timerSet = (payload: number) => ({ type: TIMER_SET, payload });
export const setTime = (payload: number) => ({ type: SET_TIME, payload });

Reducer 实现

export const initialState: State = {
    preferences: {
        time: 0,
    },
    time: {
        timer: 1,
        timerId: null,
    },
};

const timerReducer = (
    state = initialState.time,
    { type, payload }: AnyAction
) => {
    switch (type) {
        case TIMER_SET:
            return { ...state, timer: payload };
        default:
            return state;
    }
};

const preferenceReducer = (
    state = initialState.preferences,
    { type, payload }: AnyAction
) => {
    switch (type) {
        case SET_TIME:
            return {
                ...state,
                time: payload,
            };
        default:
            return state;
    }
};

Header 组件代码

export const Header = () => {
    const {
        preferences: { time },
        time: { timer }
    } = useSelector((state: State) => state);
    const dispatch = useDispatch();
    const [punctuation, setPunctuation] = useState(false);
    const [numbers, setNumbers] = useState(false);
    const [selectedOption, setSelectedOption] = useState(2);
    const [option, setOption] = useLocalStorage("time", time || 60);

    const timeOptions = useMemo(() => [
        { id: 1, optionText: 15 },
        { id: 2, optionText: 30 },
        { id: 3, optionText: 60 },
        { id: 4, optionText: 120 },
    ], []);

    useEffect(() => {
        import(`../data/english.json`).then((words) =>
            dispatch(setWordList(words))
        );
        dispatch(timerSet(option));
        dispatch(setTime(option));
        setSelectedOption(timeOptions.find(opt => opt.optionText === option)?.id || 2);
    }, [dispatch, option, timeOptions]);

    useEffect(() => {
        dispatch(setTime(option));
        dispatch(timerSet(option));
        resetTest()
    }, [dispatch, option])

    return (
        <div className={styles.Container}>
            <div className={styles.NavBar} style={{ width: "44%" }}>
                <h1 className={styles.NavText} onClick={() => setPunctuation(!punctuation)} style={punctuation ? { color: 'var(--main-color)' } : { color: 'var(--sub-color)' }}>@punctuation</h1>
                <h1 className={styles.NavText} onClick={() => setNumbers(!numbers)} style={numbers ? { color: 'var(--main-color)' } : { color: 'var(--sub-color)' }}>#numbers</h1>
                <div className={styles.Divider}></div>
                <h1 className={styles.NavText} style={{ color: 'var(--main-color)' }}>time</h1>
                <div className={styles.Divider}></div>
                {timeOptions.map((option) => (
                    <NavOption
                        key={option.id}
                        optionText={option.optionText}
                        isSelected={option.id === selectedOption}
                        onClick={() => {
                            setSelectedOption(option.id);
                            setOption(option.optionText);
                        }}
                    />
                ))}
            </div>
        </div>
    );
}

resetTest 函数代码

export const resetTest = () => {
    const wordElements = document.getElementsByClassName("word");
    if (wordElements.length > 0) {
        wordElements[0].scrollIntoView({ behavior: "smooth", block: "center" });
    }
    const { dispatch, getState } = store;
    const {
        time: { timerId },
        preferences: { time },
    } = getState();
    document
        .querySelectorAll(".wrong, .right")
        .forEach((el) => el.classList.remove("wrong", "right"));
    if (timerId) {
        clearInterval(timerId);
        dispatch(setTimerId(null));
    }
    import(`../data/english.json`).then((words) =>
        dispatch(setWordList(words.default))
    );
    dispatch(timerSet(time));
};

遇到的问题

  1. 页面初始化时,preferences.time 初始值为0而非localStorage中存储的值
  2. timerId 初始为null而非undefined,影响定时器清理逻辑
  3. 将resetTest()移至dispatch上方后,部分问题解决,但页面刷新时option先显示15,随后才更新为localStorage中的值

完整解决方案

1. 修正Redux初始状态,从localStorage预加载

在创建store时直接从localStorage读取初始值,避免组件初始化时的状态不一致:

const loadInitialState = () => {
    try {
        const savedTime = localStorage.getItem("time");
        return {
            preferences: {
                time: savedTime ? parseInt(savedTime, 10) : 60,
            },
            time: {
                timer: savedTime ? parseInt(savedTime, 10) : 60,
                timerId: undefined,
            },
        };
    } catch (e) {
        return {
            preferences: {
                time: 60,
            },
            time: {
                timer: 60,
                timerId: undefined,
            },
        };
    }
};

export const initialState: State = loadInitialState();

2. 优化Header组件的useLocalStorage与状态同步

useLocalStorage的初始值直接读取localStorage,不依赖Redux状态:

const savedTime = localStorage.getItem("time");
const [option, setOption] = useLocalStorage("time", savedTime ? parseInt(savedTime, 10) : 60);

合并重复的useEffect,避免多次dispatch:

useEffect(() => {
    import(`../data/english.json`).then((words) =>
        dispatch(setWordList(words.default))
    );
    dispatch(setTime(option));
    dispatch(timerSet(option));
    setSelectedOption(timeOptions.find(opt => opt.optionText === option)?.id || 2);
    resetTest(dispatch, useSelector((state: State) => state));
}, [dispatch, option, timeOptions]);

3. 修正resetTest中的定时器逻辑

避免直接依赖store实例,改为传入dispatch和state参数:

export const resetTest = (dispatch: Dispatch, state: State) => {
    const wordElements = document.getElementsByClassName("word");
    if (wordElements.length > 0) {
        wordElements[0].scrollIntoView({ behavior: "smooth", block: "center" });
    }
    const {
        time: { timerId },
        preferences: { time },
    } = state;
    document.querySelectorAll(".wrong, .right").forEach((el) => el.classList.remove("wrong", "right"));
    
    if (timerId !== null && timerId !== undefined) {
        clearInterval(timerId);
        dispatch(setTimerId(undefined));
    }
    import(`../data/english.json`).then((words) =>
        dispatch(setWordList(words.default))
    );
    dispatch(timerSet(time));
};

4. 确保useLocalStorage实现正确

如果是自定义hook,保证挂载时立即读取localStorage:

export const useLocalStorage = <T>(key: string, initialValue: T) => {
    const [storedValue, setStoredValue] = useState<T>(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error(error);
            return initialValue;
        }
    });

    const setValue = (value: T | ((val: T) => T)) => {
        try {
            const valueToStore = value instanceof Function ? value(storedValue) : value;
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(error);
        }
    };

    return [storedValue, setValue] as const;
};

内容的提问来源于stack exchange,提问作者Rohith Nambiar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 22:27:23