javascriptreactjsreact-contexttiptap

Context Value Defined in Component, but Returns Undefined in a Function Within the Same Component


Context Variable Issue

Hello everyone! 👋 I'm currently facing an issue with the userID context variable.

Problem Description:

The userID is initially set within the UserContextProvider component in UserContext.jsx. However, when I print the userID value right above the handleJournalCreateAndUpdate function in Editor.jsx, it displays the expected value. Strangely, within the handleJournalCreateAndUpdate function, the userID is coming up as undefined.

Observation:

I've noticed that making any changes to the jsx file containing the Editor component causes the userID value to suddenly become available within the handleJournalCreateAndUpdate function. This behavior raises questions about the timing or order of execution.

I would greatly appreciate any insights or assistance in understanding why the userID variable behaves this way within the mentioned context. Thank you! 🙏

Current Setup:

UserContext.jsx

import React, { createContext, useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import Cookies from 'js-cookie';
import { getUserProfile } from '../services/authService';

// Create the context with an initial value of null
const UserContext = createContext();

// Create a provider component to wrap your app with
export const UserContextProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [userID, setUserID] = useState(null);
    const navigate = useNavigate();
    const location = useLocation();

    const isCurrentPathAuthPage = ['/register', '/login'].includes(
        location.pathname
    );

    const fetchUserProfileAndUpdate = async () => {
        if (!user) {
            const token = Cookies.get('AuthToken');
            if (!token) {
                if (!isCurrentPathAuthPage) {
                    navigate('/login');
                }

                return;
            }

            try {
                const response = await getUserProfile(token);
                setUser(response.user);
                setUserID(response.user._id);
            } catch (error) {
                if (!isCurrentPathAuthPage) {
                    navigate('/login');
                }
            }
        }
    };

    useEffect(() => {
        if (!user) {
            fetchUserProfileAndUpdate();
        }
    }, [user, location.pathname]);

    return (
        <UserContext.Provider value={{ user, userID }}>
            {children}
        </UserContext.Provider>
    );
};

// Create a custom hook to easily access the context value
export const useUser = () => {
    const context = React.useContext(UserContext);

    if (!context) {
        throw new Error('useUser must be used within a UserContextProvider');
    }

    return context;
};

Main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './styles/preflight.css';
import './styles/index.css';
import { ThemeProvider } from './contexts/ThemeContext';
import { MenuProvider } from './contexts/NavDrawerContext.jsx';
import { FocusModeProvider } from './contexts/FocusModeContext.jsx';
import { BrowserRouter } from 'react-router-dom';
import { UserContextProvider } from './contexts/UserContext.jsx';

ThemeProvider;
ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <ThemeProvider>
            <BrowserRouter>
                <UserContextProvider>
                    <MenuProvider>
                        <FocusModeProvider>
                            <App />
                        </FocusModeProvider>
                    </MenuProvider>
                </UserContextProvider>
            </BrowserRouter>
        </ThemeProvider>
    </React.StrictMode>
);

Editor.jsx

import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
    BlockNoteView,
    useBlockNote,
    getDefaultReactSlashMenuItems,
} from '@blocknote/react';
import '@blocknote/core/style.css';
import EditorMenu from './EditorMenu';
import Layout from '../../containers/Layout';

import PromptDisplayCard from './PromptDisplayCard';
import { useTheme } from '../../contexts/ThemeContext';
import { useUser } from '../../contexts/UserContext';
import {
    createJournalEntry,
    getJournalEntryById,
    updateJournalEntry,
} from '../../services/journalEntryService';

//  Variable to keep track of whether a journal entry has been created to prevent multiple entries due to async nature of the JS
let isJournalEntryCreated =
    localStorage.getItem('isJournalEntryCreated') === 'true';

// Main TextEditor component
const Editor = () => {
    const { userID } = useUser();
    const [content, setContent] = useState([]);
    const [selectedBlocks, setSelectedBlocks] = useState([]);
    const [isSelectionActive, setIsSelectionActive] = useState(false);
    const [isFocused, setIsFocused] = useState(false);
    const [isPromptDisplayVisible, setIsPromptDisplayVisible] = useState(false);
    const [currentMood, setCurrentMood] = useState('happy');
    const [isFocusModeOn, setIsFocusModeOn] = useState(false);
    const [journalID, setJournalID] = useState(() =>
        localStorage.getItem('journalID')
    );
    const [isTextEditorMenuCollapsed, setIsTextEditorMenuCollapsed] =
        useState(false);
    const containerRef = useRef(null);
    // Function to scroll to the bottom of the editor
    const scrollToBottom = () => {
        if (containerRef.current) {
            containerRef.current.scrollTop = containerRef.current.scrollHeight;
        }
    };

    useEffect(() => {
        if (journalID) {
            getJournalEntryById(journalID).then((response) => {
                if (response) {
                    // setContent(response.content);
                    // setCurrentMood(response.mood);
                }
            });
        }
    }, []);

    console.log(userID); // has value
    async function handleJournalCreateAndUpdate(editor) {
        console.log('userID', userID?.length); // Undefined


        // Update the content state and scroll to the bottom of the editor
        const newContent = editor.topLevelBlocks;
        captureSelectedBlocks(editor);
        setContent(newContent);
        scrollToBottom();

        if (
            !isJournalEntryCreated &&
            newContent?.length &&
            userID?.length &&
            !journalID
        ) {
            try {
                if (!userID.length) {
                    console.error('User ID not available');
                    return;
                }
                isJournalEntryCreated = true;
                localStorage.setItem('isJournalEntryCreated', true);
                const response = await createJournalEntry(
                    userID,
                    newContent,
                    'happy'
                );
                console.log('response', response._id);
                setJournalID(response._id);
                localStorage.setItem('journalID', journalID);
            } catch (error) {
                console.error('Failed to create journal entry:', error);
                isJournalEntryCreated = false;
                localStorage.setItem('isJournalEntryCreated', false);
            }
        } else if (
            isJournalEntryCreated &&
            newContent?.length &&
            journalID?.length
        ) {
            try {
                if (!journalID.length) {
                    console.error('Journal ID not available');
                    return;
                }
                await updateJournalEntry(journalID, 'content', newContent);
            } catch (error) {
                console.error('Failed to update journal entry:', error);
            }
        }
    }

    const { blockNoteTheme, toggleDarkMode } = useTheme();

    // Customize the slash menu items
    const customizeMenuItems = () => {
        const removeHintsAndShortcuts = (item) => {
            const newItem = { ...item };
            delete newItem.hint;
            delete newItem.shortcut;
            if (newItem.group === 'Basic blocks') {
                newItem.group = 'Basic';
            }
            return newItem;
        };

        const defaultMenuItems = [...getDefaultReactSlashMenuItems()];
        const customizedMenuItems = defaultMenuItems.map(
            removeHintsAndShortcuts
        );
        return customizedMenuItems;
    };

    //  Initialize the editor instance
    const editor = useBlockNote({
        initialContent: content,
        slashMenuItems: customizeMenuItems(),
        onEditorReady: () => {
            editor.focus('end');
            scrollToBottom();
        },

        onEditorContentChange: handleJournalCreateAndUpdate,

        onTextCursorPositionChange: (editor) => {
            captureSelectedBlocks(editor);
        },
    });

    // Function to capture selected blocks
    const captureSelectedBlocks = useCallback(
        (editor) => {
            const currentSelectedBlocks = editor.getSelection()?.blocks;
            const currentActiveBlock = editor.getTextCursorPosition().block;

            if (currentSelectedBlocks) {
                setSelectedBlocks(currentSelectedBlocks);
                setIsSelectionActive(true);
            } else {
                isSelectionActive && setIsSelectionActive(false);
                setSelectedBlocks([currentActiveBlock]);
            }
        },
        [selectedBlocks, isSelectionActive]
    );

    // Renders the editor instance using a React component.
    return (
        <Layout
            currentMood={currentMood}
            setCurrentMood={setCurrentMood}
            isFocusModeOn={isFocusModeOn}
            setIsFocusModeOn={setIsFocusModeOn}
            setIsPromptDisplayVisible={setIsPromptDisplayVisible}
            setIsTextEditorMenuCollapsed={setIsTextEditorMenuCollapsed}
            toggleDarkMode={toggleDarkMode}
            showFocusModeAndMoodDropdown={true}
        >
            //Rest of the code
        </Layout>
    );
};

export default Editor;


Solution

  • After a little bit of research in the source code of the BlockNote, I found out that the problem is that you are missing updating the reference of the handleJournalCreateAndUpdate function. the solution might help you understand better:

    You must pass a dependencies list (just like the 2nd argument of useEffect) to the useBlockNote to re-run its inner-useEffect, which will update the reference of the handleJournalCreateAndUpdate function. So, you can access the latest value of the userID in your function.

    const editor = useBlockNote({
        ...
    }, [userID]);
    

    however, a better solution and practice is to use useCallback which will make your code cleaner and more optimized in the future:

    import {useCallback} from "react"
    
    ...
    
    const handleJournalCreateAndUpdate = useCallback(() => {
        ...
    }, [userID, /* and other dependencies or states */])
    
    const editor = useBlockNote({
        ...,
        onEditorContentChange: handleJournalCreateAndUpdate,
        ...
    }, [handleJournalCreateAndUpdate]);