首页 > 解决方案 > useState(new Map()) is not working, but object does

问题描述

I honestly have no idea what is going on here. I have this code, on first render it should fetch popular repos and set them to the repos state, which should cause a re-render and paint the new repos on the DOM. The reason I use Map/obj is because I'm caching the repos to avoid re-fetch. The code doesn't work as expected, it's not setting any new state, and I can verify it in the react dev tools. For some reason if I click around on Components in the devtools, the state updates(?!), but the DOM is still not painted (stuck on Loading), which is a very strange behavior.

export default () => {
    const [selectedLanguage, setSelectedLanguage] = useState('All');
    const [error, setError] = useState(null);
    const [repos, setRepos] = useState(() => new Map());

    useEffect(() => {
        if (repos.has(selectedLanguage)) return;
        (async () => {
            try {
                const data = await fetchPopularRepos(selectedLanguage);
                setRepos(repos.set(selectedLanguage, data));
            } catch (err) {
                console.warn('Error fetching... ', err);
                setError(err.message);
            }
        })();
    }, [selectedLanguage, repos]);

    const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);

    const isLoading = () => !repos.has(selectedLanguage) && !error;

    return (
        <>
            <LanguagesNav
                selected={selectedLanguage}
                updateLanguage={updateLanguage}
            />
            {isLoading() && <Loading text="Fetching repos" />}
            {error && <p className="center-text error">{error}</p>}
            {repos.has(selectedLanguage)
                && <ReposGrid repos={repos.get(selectedLanguage)} />}
        </>
    );
};

However, if I change up the code to use object instead of a Map, it works as expected. What am I missing here? For example, this works (using obj instead of a Map)

const Popular = () => {
    const [selectedLanguage, setSelectedLanguage] = useState('All');
    const [error, setError] = useState(null);
    const [repos, setRepos] = useState({});

    useEffect(() => {
        if (repos[selectedLanguage]) return;
        (async () => {
            try {
                const data = await fetchPopularRepos(selectedLanguage);
                setRepos(prev => ({ ...prev, [selectedLanguage]: data }));
            } catch (err) {
                console.warn('Error fetching... ', err);
                setError(err.message);
            }
        })();
    }, [selectedLanguage, repos]);

    const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);

    const isLoading = () => !repos[selectedLanguage] && !error;

    return (
        <>
            <LanguagesNav
                selected={selectedLanguage}
                updateLanguage={updateLanguage}
            />
            {isLoading() && <Loading text="Fetching repos" />}
            {error && <p className="center-text error">{error}</p>}
            {repos[selectedLanguage]
                && <ReposGrid repos={repos[selectedLanguage]} />}
        </>
    );
};

标签: reactjsreact-hooks

解决方案


repos.set() mutates the current instance and returns it. Since setRepos() sees the same reference, it doesn't trigger a re-render.

Instead of

setRepos(repos.set(selectedLanguage, data));

you can use:

setRepos(prev => new Map([...prev, [selectedLanguage, data]]));

推荐阅读