DEV Community

Cover image for React.js ~useState Antipatterns~
Ogasawara Kakeru
Ogasawara Kakeru

Posted on

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",
        }}
      />
  );
Enter fullscreen mode Exit fullscreen mode
  • After
 const [position, setPosition] = useState({ x: 0, y: 0 });

 const handlePointerMove = (e) => {
    setPosition({
      x: e.clientX,
      y: e.clientY,
    });
  };
  return (...
  );

Enter fullscreen mode Exit fullscreen mode
  • Before
const [userName, setUserName] = useState("");
const [userAge, setUserAge] = useState(0);
Enter fullscreen mode Exit fullscreen mode
  • After
const [userInfo, setUserInfo] = useState({name:"",age:0});
Enter fullscreen mode Exit fullscreen mode

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 <h1>I appreciate</h1>
  }

  function sendMessage(text) {
    return new Promise(resolve => {
    setTimeout(resolve, 2000);
   });
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Submit
      </button>
      {isSubmitting && <p>Submitting...</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • 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 (...
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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 });
  };

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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}</h1>;
}

Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)