首页 > 解决方案 > React/Recoil:一旦在主应用程序中设置状态,就不能在组件内再次设置?

问题描述

从 App.js 文件中的可加载项设置状态后:

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;

setFocusState从组件内调用函数Point似乎不起作用:

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>
    )
}

当我console.log(focusState)在 function 中添加 switch 语句的相关部分时handleKeyEvent,它表明每次setFocusState从 Point 组件中调用时,没有任何变化,并且值focusState保持与初始设置相同。我猜这就是为什么setCursor不通过useEffect调用的原因。

有人可以建议这里出了什么问题吗?需要改变什么

  1. setFocusState实际修改从组件focusState内调用时的值Point
  2. 为此,通过实际修改光标位置setCursor

标签: javascriptreactjstypescriptrecoiljs

解决方案


首先:如果您focusState在设置后立即记录,您甚至不会记录新值,因为:

  1. 组件以' 旧值Point呈现focusState(我们称其为 #1 值)

  2. 您添加一个调用setCursor的效果,该效果具有被调用但不会再次调用的空数组依赖项

  3. 您将focusState(设置为#2 值)设置为处理程序。在上下文中,您记录它希望它包含新值,但是......

  4. 因为您将focusState组件重新渲染设置为使用新focusState值进行渲染(#2)

  5. 你的效果不会调用setCursor,因为它不依赖于focusState

我认为这是你的问题

// eslint-disable-next-line是为了问这个问题而添加的,还是因为你想避免setCursor每次引用setCursor都是新的(在每次渲染时)都调用?如果是后者,请考虑将其重构为

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])

并完全删除该setCursor功能。


请注意:此答案可能不完整,请使用可播放的 CodeSandbox 更新问题以明确解决您的问题。


推荐阅读