Working with react stale state
So last week I'm working on side project called nocion which is just a clone of notion. In this project I will try to make everything from scratch and simple. And I found some interesting problem here.
Scenario
This time in order to clone on of the most crucial feature in notion is block editor. So everytime we press Enter
key inside block editor it will generate new block editor. The block editor it self will be stored inside array state. So everytime we receive enter event we will update that array.
import React, { useState, useEffect } from "react";
import "./styles.css";
function BlockEditor({ text, addNewBlock }) {
useEffect(() => {
window.addEventListener("keyup", onKeyUp);
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("keydown", onKeyDown);
};
}, []);
function onKeyUp(event) {
if (blockRef.current !== document.activeElement) return;
if (event.key === "Enter") {
addNewBlock({ text: "" });
event.preventDefault();
}
}
function onKeyDown(event) {
if (blockRef.current !== document.activeElement) return;
if (event.key === "Enter") {
event.preventDefault();
}
}
return (
<h2 suppressContentEditableWarning={true} contentEditable={true}>
{text}
</h2>
);
}
export default function App() {
const [blocks, appendBlock] = useState([
{
text: "Let's edit this page 🚀",
index: 0
}
]);
function addNewBlock(block) {
const newBlock = [...blocks, { ...block, id: blocks.length + 1 }];
console.log(newBlock);
appendBlock(newBlock);
}
return (
<div className="App">
<h1>Callback State</h1>
{React.Children.toArray(
blocks.map(({ text }) => (
<BlockEditor text={text} addNewBlock={addNewBlock} />
))
)}
</div>
);
}
If you see this code is look okay and even in console log we can see the correct data logged we expected to have new line added to the addNewBlock
function. But the dom is not updated.
Stale Value
This is because we are working with the stale values. In short stale value is the value that we use is out of date, it mean we still use the previous value. Let see this example components.
function AsyncCount() {
const [count, setCount] = useState(0);
function incrementAsync() {
setTimeout(function delay() {
setCount(count + 1);
}, 1000);
}
return (
<div>
{count}
<button onClick={incrementAsync}> Increment</button>
</div>
);
}
This component will update the count value every one second after we click. It will working fine if we increment the value on linear time or we process it in low traffic. But if we run increment the value 10 times under one second we will not receive count
value to be 10. To solve that issue we can make sure we work with the previous value of count
state. The solution will be a simple like this.
setTimeout(function delay() {
setCount((count) => count + 1);
}, 1000);
Solution
So let's get back with the blocks editor. How to fix the issue? It easy just update the array blocks
using corrent previous value.
function addNewBlock(block) {
appendBlock((prevBlock) =>
prevBlock.concat([{ ...block, id: blocks.length + 1 }])
);
}
Reference
If you need more deep explanation read here.
- dmitripavlutin - React Hooks Stale Closures
- kentcdodds - useState lazy initialization
- stackoverflow - stale state
Demo Project
If you like to try the project just play with this example demo project.
Interesting about the nocion
project visit here.