2

This is a next.js site, since both my Navbar component and my cart page should have access to my cart's content I created a context for them. If I try to render the page, I get:

Unhandled Runtime Error

TypeError: Cannot read properties of undefined (reading 'key')

obs: The cartContent array exists and has length 1, I can get it by delaying when the data's rendered by using setTimeout, but, can't get it to render right after it's fetched.

I need to make it render after the data from firebase is returned, but always met with the mentioned error.

This is my _app.tsx file

function MyApp({ Component, pageProps }) {
    // set user for context
    const userContext = startContext();

    return (
        <UserContext.Provider value = { userContext }>
            <Navbar />
            <Component {...pageProps} />
            <Toaster />
        </UserContext.Provider>
    );
}

export default MyApp

This file has the startContext function that returns the context so it can be used.

export const startContext = () => {
    const [user] = useAuthState(auth);
    const [cart, setCart] = useState(null);
    const [cartContent, setCartContent] = useState(null);
    useEffect(() => {
        if (!user) {
            setCart(null);
            setCartContent(null);
        }
        else {
            getCart(user, setCart, setCartContent);
        }
    }, [user]);
    return { user, cart, setCart, cartContent, setCartContent };
}

This file contains the getCart function.

export const getCart = async (user, setCart, setCartContent) => {
    if (user) {
        try {
            let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
            if (new_cart) { 
                let new_cartContent = []
                await Object.keys(new_cart).map(async (key) => {
                    new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
                });
                console.log(new_cartContent);
                setCartContent(new_cartContent);
                console.log(new_cartContent);
                setCart(new_cart);
            }
            else {
                setCart(null);
                setCartContent(null);
            }
        } catch (err) {
            console.error(err);
        }
    }
}

This is the cart.tsx webpage. When I load it I get the mentioned error.

export default () => {
    const { user, cart, cartContent } = useContext(UserContext);

    return (
        <AuthCheck>
            <div className="grid grid-cols-1 gap-4">
                {cartContent && cartContent[0].key}
            </div>
        </AuthCheck>
    )
}

I've tried to render the cart's content[0].key in many different ways, but couldn't do it. Always get error as if it were undefined. Doing a setTimeout hack works, but, I really wanted to solve this in a decent manner so it's at least error proof in the sense of not depending on firebase's response time/internet latency.

Edit: Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it will consider the state cartContent as undefined and won't trigger again later.

  • Can you put your code in a sanbox? Because I don't see any error looking at the above code. It looks fine to me. – Vishnu Sajeev Jan 05 '22 at 06:59
  • Does this answer your question? [Using promise function inside Javascript Array map](https://stackoverflow.com/questions/39452083/using-promise-function-inside-javascript-array-map) – juliomalves Jan 07 '22 at 10:54

2 Answers2

1

Try changing

{cartContent && cartContent[0].key}

to

{cartContent?.length > 0 && cartContent[0].key}

Edit:: The actual problem is in getCart function in line

let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();

This is either set to an empty array or an empty object. So try changing your if (new_cart) condition to

if (Object.keys(new_cart).length > 0) {

Now you wont get the undefined error

Vishnu Sajeev
  • 941
  • 5
  • 11
  • I just tried this: ```{cartContent?.length > 0 && cartContent[0].key}``` and ```{cartContent?.length > 0 &&

    test

    }``` And none work, it seems like it renders when the component is undefined and later updates the component to its value (an array with length 1) without rerendering
    – Douwe De Jong Jan 05 '22 at 06:02
  • Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it won't trigger it again later and won't render anything. – Douwe De Jong Jan 05 '22 at 06:05
  • @DouweDeJong Check my edit in the answer – Vishnu Sajeev Jan 05 '22 at 07:14
  • This is still not working. If I console.log(new_cart) after the condition in the getCart function I get the right data (an object with a single key) but it doesn't render the content. As I said, I think it triggers the render before setting the state. – Douwe De Jong Jan 05 '22 at 07:46
1

Since there seemed to be a race condition, I figured the setCartContent was executing before its content was fetched. So I changed in the getCart function the map loop with an async function for a for loop

await Object.keys(new_cart).map(async (key) => {
        new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});

to

for (const key of Object.keys(new_cart)) {
         new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
}

I can't make a map function with await in it without making it asynchronous so I the for loop made it work. Hope someone finds some alternatives to solving this, I could only come up with a for loop so the code is synchronous.