After I set the state from the loadable within the App.js file:
import React from 'react';
import { useRecoilState, useSetRecoilState, useRecoilValueLoadable } from 'recoil';
import './App.css';
import { Point } from './components/Point';
import { FocusState } from './context/FocusState';
import { ItemListState } from './context/ItemListState';
import { RootState } from './context/RootState';
import { DataState } from './context/DataState';
function App() {
const setFocusState = useSetRecoilState(FocusState);
const setItemListState = useSetRecoilState(ItemListState);
const [rootState, setRootState] = useRecoilState(RootState);
const dataStateLoadable = useRecoilValueLoadable(DataState);
switch (dataStateLoadable.state) {
case 'hasValue':
let dataState = dataStateLoadable.contents;
let {root, focus, items} = dataState;
setFocusState(focus);
setItemListState(items);
setRootState(root);
return (
<div className="App">
<Point key={rootState} id={rootState} depth={0} />
</div>
)
case 'loading':
return (
<div className="App">
<p>Loading...</p>
</div>
)
case 'hasError':
throw dataStateLoadable.contents;
default:
return (
<div className="App">
<p>Loading...</p>
</div>
)
}
}
export default App;
Calling the setFocusState
function from within the Point
component doesn't seem to work:
export const Point: React.FC<{id: string, depth: number}> = ({id, depth}) => {
const pointRef = useRef<HTMLDivElement>(null);
const [focusState, setFocusState] = useRecoilState(FocusState);
const [itemState, setItemState] = useRecoilState(SingleItemStateFamily(id))
const parentPoint = useRecoilValue(SingleItemStateFamily(itemState.parent));
const grandparentPoint = useRecoilValue(SingleItemStateFamily(parentPoint.parent));
const setCursor = () => {
// mignt be null
const node = pointRef.current;
let range = document.createRange();
let sel = window.getSelection();
if (node !== null && node.childNodes.length > 0 && focusState.id === id) {
console.log(node, node.childNodes, focusState)
// select a range first
range.setStart(node.childNodes[0], focusState.cursorPosition);
range.setEnd(node.childNodes[0], focusState.cursorPosition);
// perform selection
sel?.removeAllRanges();
sel?.addRange(range);
node.focus();
}
}
const handleChange = (evt) => {
let newState = {...itemState};
newState.content = evt.currentTarget.innerHTML;
setItemState(newState);
}
const handleKeyEvent = (evt) => {
switch (evt.key) {
case "ArrowUp":
evt.preventDefault();
console.log("Shift focus to item above", parentPoint.children, itemState.id, parentPoint.children.indexOf(itemState.id));
// if it is the first child of a parent, shift focus to the parent
if (parentPoint.children.indexOf(itemState.id) === 0) {
console.log("Shift focus to parent")
setFocusState({id: parentPoint.id, cursorPosition: focusState.cursorPosition});
console.log(focusState);
}
// else, go to the next highest sibling
// the cursorPosition should be min(focusState.curpos, newPoint.content.length)
else {
console.log("Shift focus to previous sibling")
setFocusState({
id: parentPoint.children[parentPoint.children.indexOf(itemState.id)-1],
cursorPosition: focusState.cursorPosition
});
console.log(focusState);
}
break;
case "ArrowDown":
evt.preventDefault();
console.log("Shift focus to item below", parentPoint.children, itemState.id, parentPoint.children.indexOf(itemState.id));
// if it is the last child of a parent, shift focus to the parent's next sibling
if (parentPoint.children.indexOf(itemState.id) === parentPoint.children.length - 1) {
console.log("Shift focus to parent's next sibling")
setFocusState({
id: grandparentPoint.children[grandparentPoint.children.indexOf(parentPoint.id) + 1],
cursorPosition: focusState.cursorPosition
})
}
// else if it has any children, shift focus to the first child
else if (itemState.children.length > 0) {
console.log("Shift focus to first child")
setFocusState({
id: itemState.children[0],
cursorPosition: focusState.cursorPosition
})
}
// else, go to the next lowest sibling
else {
console.log("Shift focus to next sibling")
setFocusState({
id: parentPoint.children[parentPoint.children.indexOf(itemState.id)+1],
cursorPosition: focusState.cursorPosition
});
}
break;
case "Tab":
evt.preventDefault();
if (evt.shiftKey) {
console.log("Dedent item");
} else {
console.log("Indent item");
}
break;
default:
break;
}
}
const handleBlur = (evt) => {
let sel = window.getSelection();
let offset = sel?.anchorOffset as number;
setFocusState({
id: focusState.id,
cursorPosition: offset
})
}
useEffect(() => {
setCursor();
// eslint-disable-next-line
}, [])
return (
<ul className="point-item">
<li>
<ContentEditable onChange={handleChange}
html={itemState.content}
onKeyDown={handleKeyEvent}
innerRef={pointRef}
onBlur={handleBlur}
/>
</li>
{itemState.children.map((child_id: string) => (
<Point key={child_id} id={child_id} depth={depth+1}/>
))}
</ul>
)
}
When I add console.log(focusState)
to the relevant parts of the switch statement within function handleKeyEvent
, it shows that every time setFocusState
is called from within the Point component, nothing changes and the value of focusState
remains the same value as the initial setting. I am guessing this is why setCursor
doesn't get called via useEffect.
Would anyone be able to advise what is going wrong here? What would need to be changed for
setFocusState
to actually modify the value offocusState
when called from within thePoint
component- For that to result in an actual modification of the cursor position via
setCursor
First of all: if you log
focusState
right after setting it, you won't even log the new value because:the
Point
component is rendered withfocusState
' old value (let's call it the #1 value)you add an effect that calls
setCursor
, the effect has empty array dependencies it gets called but it won't get called againyou set
focusState
(to a #2 value) into the handler. Contextually, you log it hoping it contains the new value but...since you set
focusState
the component re-renders to render with the newfocusState
value (#2)your effect doesn't call
setCursor
because it doesn't depend onfocusState
I think that this is your problem
Did you add the
// eslint-disable-next-line
for the sake of asking this question or because you'd like to avoid callingsetCursor
every time the reference tosetCursor
is new (at every render)? If the latter, please consider refactoring it toand removing the
setCursor
function at all.Please note: this answer could be incomplete, please update the question with a playable CodeSandbox to fix definitely your problem.