James Harrison
James Harrison

Reputation: 323

React/Recoil: once state is set in the main app, cannot be set again inside a component?

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

  1. setFocusState to actually modify the value of focusState when called from within the Point component
  2. For that to result in an actual modification of the cursor position via setCursor

Upvotes: 0

Views: 1131

Answers (1)

NoriSte
NoriSte

Reputation: 3709

First of all: if you log focusState right after setting it, you won't even log the new value because:

  1. the Point component is rendered with focusState' old value (let's call it the #1 value)

  2. you add an effect that calls setCursor, the effect has empty array dependencies it gets called but it won't get called again

  3. you set focusState (to a #2 value) into the handler. Contextually, you log it hoping it contains the new value but...

  4. since you set focusState the component re-renders to render with the new focusState value (#2)

  5. your effect doesn't call setCursor because it doesn't depend on focusState

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 calling setCursor every time the reference to setCursor is new (at every render)? If the latter, please consider refactoring it to

useEffect(() => {
  // 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();
  }
}, [id, focusState])

and 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.

Upvotes: 1

Related Questions