开发者问题收集

react 无法使用 forwardRef 和 useRef 读取 ref 为 null 的属性

2022-09-18
2105

我正在使用函数组件。我使用 useRef 来存储 previousValueCount ,因为我不希望它在更新时重新渲染组件。 只要我的 useRef 声明位于我的代码使用该 ref 读取和写入的组件中,这种方法就可以完美运行 。但是,我意识到我可能需要在树的更高层使用它,因此我转向了 props。但是,当我将 useRef 声明移到父级并将 ref 作为 prop 传递时,代码似乎中断了,告诉我它为空,无论初始化值如何,并且它在子级之前都可以工作。据我所知,我无法将 ref 作为 prop 传递,因此我转向了 React.forwardRef 。我尝试了同样的事情,但没有解决方案,并且出现了下面分享的类似错误。我如何将 ref 作为 prop 或向下组件树传递?

APP 组件:

function App() {
  const [itemsLeftCount, setItemsleftCount] = useState(0);

  return (
    <ToDos
      itemsLeftCount={itemsLeftCount}
      setItemsLeftCount={setItemsLeftCount}
    ></ToDos>
  )
}

父组件:

function ToDos(props) {
  const prevItemsLeftCountRef = useRef(0);

  return (
    <ToDoBox
      itemsLeftCount={props.itemsLeftCount}
      setItemsLeftCount={props.setItemsLeftCount}
      ref={prevItemsLeftCountRef}
    ></ToDoBox>

  {

子组件:

const ToDoBox = React.forwardRef((props, ref) => {
  useEffect(() => {
    //LINE 25 Error points to right below comment
    ref.prevItemsLeftCountRef.current = props.itemsLeftCount;
  }, [props.itemsLeftCount]);

  useEffect(() => {
    //ON MOUNT
    props.setItemsLeftCount(ref.prevItemsLeftCountRef.current + 1);

    //ON UNMOUNT
    return function removeToDoFromItemsLeft() {
      //this needs to only run after setitemsleftcount state is for sure done updating
      props.setItemsLeftCount(ref.prevItemsLeftCountRef.current - 1);
    };
  }, []);
})

我收到此错误: 未捕获的 TypeError:无法读取 null 的属性(读取“prevItemsLeftCountRef”) at ToDoBox.js:25:1

@Mustafa Walid

function ToDos(props) {
  const prevItemsLeftCountRef = useRef(0);
  const setPrevItemsLeftCountRef = (val) => {
    prevItemsLeftCountRef.current = val;
  };

 return (
   <ToDoBox
     itemsLeftCount={props.itemsLeftCount}
     setItemsLeftCount={props.setItemsLeftCount}
     prevItemsLeftCountRef={prevItemsLeftCountRef}
     setPrevItemsLeftCountRef={setPrevItemsLeftCountRef}
    ></ToDoBox>
  )
}

function ToDoBox(props) {
  useEffect(() => {
    //itemsLeftCount exists further up tree initialized at 0
    props.setPrevItemsLeftCountRef(props.itemsLeftCount); 
  }, [props.itemsLeftCount]);

}
2个回答

问题

您不需要任何 ref。您应该使用 React 状态。我真的无法确切指出导致双重初始递减的任何一个具体问题,但代码中有很多问题都值得一提。

解决方案

首先,在 ToDoBox 组件中安装 useEffect 钩子。我看到您试图保留和维护 两个 项计数状态值,一个当前值和一个先前值。这完全是多余的,如代码所示,可能会导致状态同步问题。简而言之,您应该在此处使用 功能状态更新 来正确访问先前的项计数状态值以增加或减少它。您可能还需要检查当前的 ToDoBox 组件不是返回的清理函数中的“创建框”,以确保创建待办事项的 ToDoBox 不会意外更新项目计数。

示例:

useEffect(() => {
  // RUNS ON MOUNT
  if (!isCreateBox) {
    setItemsLeftCount(count => count + 1);
  }
  
  return function removeToDoFromItemsLeft() {
    if (!isCreateBox) {
      setItemsLeftCount(count => count - 1);
    }
  };
}, []);

仅此一点就足以阻止项目计数双倍减少的问题。

我注意到的其他问题

状态突变

添加 删除待办事项的函数都在改变状态。这就是为什么您必须在 ToDos 中添加辅助 state 状态以强制应用重新渲染并显示突变的原因。

addToDoToList

此处 updatedArr 是对 dataArr 状态对象/数组的引用,并且该函数将 直接 推送到数组(突变),然后将完全相同的对象/数组引用保存回状态。

function addToDoToList() {
  let updatedArr = dataArr;   // <-- saved reference to state
  updatedArr.push(dataInput); // <-- mutation!!
  setDataArr(updatedArr);     // <-- same reference back into state
}
function handleClickCreateButton() {
  addToDoToList(); // <-- mutates state
  //force a rerender of todos
  setState({});    // <-- force rerender to see mutation
}

handleClickDeleteButton

此处,类似地, updateArr 是对当前状态的引用。 Array.prototype.splice 就地突变数组。再次,变异状态引用被保存回状态,并且需要强制重新渲染。

function handleClickDeleteButton() {
  // if splice from dataArr that renders a component todobox for each piece of data
  // if remove data from dataArr, component removed...
  let updatedArr = dataArr;    // <-- saved reference to state
  updatedArr.splice(index, 1); // <-- mutation!!
  setDataArr(updatedArr);      // <-- same reference back into state

  // then need to rerender todos... I have "decoy state" to help with that
  setState({});
}

奇怪的是,这段代码周围的注释暗示你甚至知道/理解有些事情发生了。

这里的解决方案是 再次 使用功能状态更新从先前状态正确更新 同时返回新的对象/数组引用,以便 React 看到状态已更新并触发重新渲染。我还建议向待办事项添加一个 id GUID,以便更容易识别它们。这比使用数组索引要好。

示例:

import { nanoid } from 'nanoid';

...

function addTodo() {
  // concat to and return a new array reference
  setDataArr(data => data.concat({
    id: nanoid(),
    todo: dataInput,
  }));
}

function removeTodo(id) {
  // filter returns a new array reference
  setDataArr(data => data.filter(todo => todo.id !== id));
}

其他一般设计问题

  • 从根 itemsLeftCount 状态和 setItemsLeftCount 状态更新函数的 Props 钻取一直到叶 ToDoBox 组件。
  • 没有集中控制 dataArr 状态不变量。
  • 使用映射的数组索引作为 React 键。
  • 尝试计算嵌套子项中的“派生”状态,即项目计数。

这是您的代码的精简/最小化工作版本:

应用程序

function App() {
  const [itemsLeftCount, setItemsLeftCount] = useState(0);

  return (
    <div>
      <ToDos setItemsLeftCount={setItemsLeftCount} />
      <div>{itemsLeftCount} items left</div>
    </div>
  );
}

ToDos

import { nanoid } from "nanoid";

function ToDos({ setItemsLeftCount }) {
  const [dataInput, setInputData] = useState("");
  const [dataArr, setDataArr] = useState([]);

  useEffect(() => {
    // Todos updated, update item count in parent
    setItemsLeftCount(dataArr.length);
  }, [dataArr, setItemsLeftCount]);

  function getInputData(event) {
    setInputData(event.target.value);
  }

  function addTodo() {
    setDataArr((data) =>
      data.concat({
        id: nanoid(),
        todo: dataInput
      })
    );
  }

  function removeTodo(id) {
    setDataArr((data) => data.filter((todo) => todo.id !== id));
  }

  return (
    <div>
      <ToDoBox isCreateBox getInputData={getInputData} addTodo={addTodo} />
      {dataArr.map((data) => (
        <ToDoBox key={data.id} data={data} removeTodo={removeTodo} />
      ))}
    </div>
  );
}

ToDoBox

function ToDoBox({ addTodo, data, getInputData, isCreateBox, removeTodo }) {
  if (isCreateBox) {
    return (
      <div>
        <input
          placeholder="Create a new todo..."
          onChange={getInputData}
          tabIndex={-1}
        />
        <span onClick={addTodo}>+</span>
      </div>
    );
  } else {
    return (
      <div>
        <span>{data.todo}</span>{" "}
        <span onClick={() => removeTodo(data.id)}>X</span>
      </div>
    );
  }
}

Edit react-cannot-read-properties-of-null-of-ref-with-forwardref-and-useref

Drew Reese
2022-09-20
  • 可以使用函数修改父级 ref ,然后将此函数传递给其子级。然后子级将使用此函数修改父级的 ref
const MainComponent = () => {
  const myRef = useRef("old Ref");

  const changeRef = (val) => {
    myRef.current = val;
  };

  return <SubComponent changeRef={changeRef} />;
};

const SubComponent = ({ changeRef }) => {
  changeRef("new Ref");

  return <h1>Changed Parent's Ref!</h1>;
};
Mustafa Walid
2022-09-18