react 无法使用 forwardRef 和 useRef 读取 ref 为 null 的属性
我正在使用函数组件。我使用
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]);
}
问题
您不需要任何 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>
);
}
}
-
可以使用函数修改父级
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>;
};