React.js ~useState Antipatterns~
1. Consider grouping related conditions together
Before
const [x, setX] = useState(0); const [y, setY] = useState(0);
const handlePointerMove = (e) => { setX(e.clientX); setY(e.clientY); }; return ( <div onPointerMove={handlePointerMove} style={{ width: "100vw", height: "100vh", }} /> );
After
const [position, setPosition] = useState({ x: 0, y: 0 });
const handlePointerMove = (e) => { setPosition({ x: e.clientX, y: e.clientY, }); }; return (... );
Before
const [userName, setUserName] = useState(""); const [userAge, setUserAge] = useState(0);
After
const [userInfo, setUserInfo] = useState({name:"",age:0});
2. Avoid declaring conflicting states
Before
export default function Form() { const [text, setText] = useState(''); const [isSubmitting, setSubmitting] = useState(false); const [isSubmit, setIsSubmit] = useState(false);
async function handleSubmit(e) { e.preventDefault(); isSubmitting(true); await sendMessage(text); isSubmitting(false); setIsSubmit(true); }
if (isSubmit) { return
I appreciate
}function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
return (
<textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} />
Submit {isSubmitting &&Submitting...
} ); }After
export default function Form() { const [text, setText] = useState(''); const [status, setStatus] = useState('TYPING');
async function handleSubmit(e) { e.preventDefault(); setStatus('SUBMITTING'); await sendMessage(text); setStatus('SUBMITTED'); }
const isSending = status === 'SUBMITTING'; const isSent = status === 'SUBMITTED';
return (... ); }
isSubmit and isSubmitting are conflicting and make it difficult to handle these states as it gets complex.
You have to group them into a single state.
3. Avoid redundant use
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
return (
<>
<label>
First name:
<input
name="firstName"
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
name="lastName"
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
FullName: <span>{fullName}</span>
</p>
</>
);
}
The fullName can be calculated when rendering by firstName + lastName.
const [userName, setUserName] = useState({ firstName: "", lastName: "" });
const fullName = userName.firstName + " " + userName.lastName;
const handleUserNameChange = (e) => {
setUserName({ ...userName, [e.target.name]: e.target.value });
};
And props should not be held in the state.
function Text({ children, color }) {
const [textColor] = useState(color);
return <h1 style={{ color: textColor }}>{children}</h1>;
}
export default function Example() {
const [color, setColor] = useState("red");
return (
<div>
<p>
Select Color
<select value={color} onChange={(e) => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
</p>
<Text color={color}>Color will be changed</Text>
</div>
);
}
The color seems to change at a glance, but doesn't change.
Because useState is initialized when first rendering.
Fixed
function Text({ children, color }) { const textColor = color;
return <h1 style={{ color: textColor }}>{children}; }
4. Avoid declaring the same state multiple times
const initialItems = [
{ id: 1, title: "taskA" },
{ id: 2, title: "taskB" },
{ id: 3, title: "taskC" },
];
export default function TaskList() {
const [tasks, setTasks] = useState(initialItems);
const [selectedTask, setSelectedTask] = useState(tasks[0]);
function handleTaskChange(id, e) {
setTasks(tasks.map((task) => (task.id === id ? { ...task, title: e.target.value } : task)));
setSelectedTask((task) => (task.id === id ? { ...task, title: e.target.value } : task));
}
return (
<>
<h2>Task List</h2>
<ul>
{tasks.map((task, index) => (
<li key={task.id}>
<input
value={task.title}
onChange={(e) => {
handleTaskChange(task.id, e);
}}
/>
<button
onClick={() => {
setSelectedTask(task);
}}
>
Select
</button>
</li>
))}
</ul>
<p>Today's task {selectedTask.title}</p>
</>
);
}
The task and the selectedTask handle the same data.
When you edit the task, you have to update selectedTask.
So, fix the codebase to calculate the selectedTask by id
function TaskList() {
const [tasks, setTasks] = useState(initialItems);
const [selectedTaskId, setSelectedTaskId] = useState(0);
function handleTaskChange(id, e) {
setTasks(tasks.map((task) => (task.id === id ? { ...task, title: e.target.value } : task)));
}
const selectedTask = tasks.find((task) => task.id === selectedTaskId);
return (
<>
<h2>Task List</h2>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<input
value={task.title}
onChange={(e) => {
handleTaskChange(task.id, e);
}}
/>
<button
onClick={() => {
setSelectedTaskId(task.id);
}}
>
Select
</button>
</li>
))}
</ul>
<p>Today's Task {selectedTask.title}</p>
</>
);
}
Discussion in the ATmosphere