Reputation: 45
I'm using function components. I am using useRef
to store a previousValueCount
because I don't want it to rerender the component when it updates. This works perfect as long as my useRef
declaration is in the component that my code reads and writes with that ref in. However, realizing I may need this higher up the tree, I turned to props. However, when I moved the useRef
declaration to the parent and passed the ref as a prop, the code seemed to break, telling me it was null, regardless of the initialization value and it working before in child. To my understanding I could not pass ref as prop so I turned to React.forwardRef
. I tried the same thing with no solution and a similar error shared below. How can I pass a ref as a prop or down component tree?
APP COMPONENT:
function App() {
const [itemsLeftCount, setItemsleftCount] = useState(0);
return (
<ToDos
itemsLeftCount={itemsLeftCount}
setItemsLeftCount={setItemsLeftCount}
></ToDos>
)
}
PARENT COMPONENT:
function ToDos(props) {
const prevItemsLeftCountRef = useRef(0);
return (
<ToDoBox
itemsLeftCount={props.itemsLeftCount}
setItemsLeftCount={props.setItemsLeftCount}
ref={prevItemsLeftCountRef}
></ToDoBox>
{
CHILD COMPONENT:
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);
};
}, []);
})
I receive this error: Uncaught TypeError: Cannot read properties of null (reading '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]);
}
Upvotes: 2
Views: 2435
Reputation: 203572
You don't need a ref for any of this. You should be using React state. I really couldn't exactly nail down any one specific issue that causing the double initial decrement, but there are quite a few issues in the code that they are all worth mentioning.
First, the mounting useEffect
hook in the ToDoBox
component. I see that you were trying to keep and maintain two item count state values, a current value and a previous value. This is completely redundant and as the code shows, can lead to state synchronization issues. Simply stated, you should be using a functional state update right here to correctly access the previous item count state value to increment or decrement it. You may want to also check that the current ToDoBox
component isn't the "create box" in the returned clean up function, to ensure the ToDoBox
that creates todos doesn't accidentally update the item count.
Example:
useEffect(() => {
// RUNS ON MOUNT
if (!isCreateBox) {
setItemsLeftCount(count => count + 1);
}
return function removeToDoFromItemsLeft() {
if (!isCreateBox) {
setItemsLeftCount(count => count - 1);
}
};
}, []);
This alone was enough to stop the item count double-decrement issue.
Both the functions to add and delete todos are both mutating the state. This is why you had to add the auxiliary state
state in ToDos
to force your app to rerender and display the mutations.
addToDoToList
Here updatedArr
is a reference to the dataArr
state object/array, and the function pushes directly into the array (the mutation), and then saves the very same object/array reference back into state.
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
Here, similarly, updateArr
is a reference to the current state. Array.prototype.splice
mutates the array in-place. And again, the mutated state reference is saved back into state and a force rerender is required.
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({});
}
Oddly enough, the comments around this code implies you even know/understand that something is up.
The solution here is to again use functional state updates to correctly update from the previous state and return new object/array references at the same time so React sees that state has updated and triggers the rerender. I suggest also adding an id
GUID to the todos to make identifying them easier. This is better than using the array index.
Examples:
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
state and setItemsLeftCount
state updater function all the way down to the leaf ToDoBox
components.dataArr
state invariant.Here's a boiled-down/minified working version of your code:
App
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>
);
}
}
Upvotes: 3
Reputation: 155
ref
can be modified using a function, this function is then passed down to its child. The child will then modify the parent's ref
using this functionconst 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>;
};
Upvotes: 0