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;
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
handleJournalCreateAndUpdatefunction. the solution might help you understand better:You must pass a
dependencies list(just like the 2nd argument ofuseEffect) to theuseBlockNoteto re-run its inner-useEffect, which will update the reference of thehandleJournalCreateAndUpdatefunction. So, you can access the latest value of theuserIDin your function.however, a better solution and practice is to use
useCallbackwhich will make your code cleaner and more optimized in the future: