useState(new Map()) is not working, but object does [duplicate]
Asked Answered
M

2

6

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]} />}
        </>
    );
};
Middleweight answered 28/4, 2020 at 20:58 Comment(0)
E
11

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]]));
Eisenstark answered 28/4, 2020 at 21:2 Comment(2)
I was actually under the impression that calling setState will automatically trigger a re-render, I didn't know that the reference also has to be different. Today I learned, your code works perfectly now! Is this also the reason that If I use a class variant and update the state like so, it works fine? because a new object is returned, so diff ref? this.setState(({repos})=> ({ repos: repos.set(selectedLanguage, data) }));Middleweight
@kevkev By default, class component state doesn't memoize state changes based on references like the newer functional component useState() hooks do. "Why" the class components were designed that way was driven by backwards compatibility. Hooks are a newer API that didn't need to have the same backwards compatibility requirements, so they were able to enforce this more optimized behavior by default.Eisenstark
A
0
New Map() is not working in `useStae(new Map())` Hook. you have to use simple Object with `useState({})`

I think time complexity will not affect much with this using object instead Map(). and for large data you can use redux.

Anthropophagite answered 31/7 at 5:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.