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! 🙏
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;
};
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>
);
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 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]);