home / skills / flpbalada / my-opencode-config / react-use-state

react-use-state skill

/skills/react-use-state

This skill helps you implement and troubleshoot React useState usage, guiding when to use state, avoid pitfalls, and optimize local component state.

npx playbooks add skill flpbalada/my-opencode-config --skill react-use-state

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.6 KB
---
name: react-use-state
description: Guides proper usage of React useState hook. Use this skill when adding state to components, deciding between useState vs alternatives, or troubleshooting state update issues.
---

# React: useState Hook Best Practices

## Core Concept

`useState` is a React Hook that adds a state variable to your component, triggering re-renders when the state changes.

```jsx
const [state, setState] = useState(initialState);
```

## When to Use useState

### Ideal Use Cases

| Use Case | Example |
|----------|---------|
| **Form inputs** | `const [name, setName] = useState('')` |
| **UI state** | `const [isOpen, setIsOpen] = useState(false)` |
| **Simple counters** | `const [count, setCount] = useState(0)` |
| **Local component data** | `const [items, setItems] = useState([])` |

### Use useState When

- State is **local** to the component
- State transitions are **simple** (direct value replacement)
- Changes should **trigger re-renders**
- You need to **persist values** between renders

## When NOT to Use useState

### Use useRef Instead

When you need mutable values that **don't trigger re-renders**:

```jsx
// Interval IDs, DOM references, previous values
const intervalRef = useRef(null);
const inputRef = useRef(null);
```

### Use useReducer Instead

When state logic is **complex**:

```jsx
// Multiple related values, complex transitions
const [state, dispatch] = useReducer(reducer, initialState);
```

Use `useReducer` when:
- State has multiple sub-values
- Next state depends on previous state in complex ways
- You want to centralize state logic

### Avoid Redundant State

If a value can be **computed from props or other state**, don't store it:

```jsx
// BAD: Redundant state
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// GOOD: Compute during render
const fullName = `${firstName} ${lastName}`;

// If expensive, use useMemo
const sortedItems = useMemo(() => 
  items.sort((a, b) => a.name.localeCompare(b.name)), 
  [items]
);
```

### Don't Use for Global/Shared State

For state shared across multiple components:
- **React Context** for moderate sharing
- **External stores** (Zustand, Jotai) for complex apps
- **Server state libraries** (TanStack Query) for async data

## Critical Rules

### 1. Never Mutate State Directly

```jsx
// BAD: Mutation
obj.x = 10;
setObj(obj); // React ignores this!

// GOOD: Create new object
setObj({ ...obj, x: 10 });

// BAD: Array mutation
arr.push(item);
setArr(arr); // React ignores this!

// GOOD: Create new array
setArr([...arr, item]);
```

### 2. State Updates Are Asynchronous

```jsx
function handleClick() {
  setCount(count + 1);
  console.log(count); // Still old value!
  
  // If you need the new value:
  const nextCount = count + 1;
  setCount(nextCount);
  console.log(nextCount); // New value
}
```

### 3. Use Updater Function for Sequential Updates

```jsx
// BAD: Only increments by 1
function handleClick() {
  setCount(count + 1); // 0 + 1 = 1
  setCount(count + 1); // 0 + 1 = 1 (same stale value!)
  setCount(count + 1); // 0 + 1 = 1
}

// GOOD: Increments by 3
function handleClick() {
  setCount(c => c + 1); // 0 -> 1
  setCount(c => c + 1); // 1 -> 2
  setCount(c => c + 1); // 2 -> 3
}
```

### 4. Use Initializer Function for Expensive Initial Values

```jsx
// BAD: createTodos() runs every render
const [todos, setTodos] = useState(createTodos());

// GOOD: createTodos runs only once
const [todos, setTodos] = useState(createTodos);

// Or with arrow function for arguments
const [todos, setTodos] = useState(() => createTodos(userId));
```

### 5. Call Hooks at Top Level Only

```jsx
// BAD: Conditional hook
if (condition) {
  const [state, setState] = useState(0); // Error!
}

// GOOD: Always call, conditionally use
const [state, setState] = useState(0);
if (condition) {
  // use state here
}
```

## Common Patterns

### Resetting State with Key

```jsx
// Parent controls reset via key
<Form key={version} />

// When version changes, Form remounts with fresh state
```

### Storing Functions in State

```jsx
// BAD: Function gets called
const [fn, setFn] = useState(someFunction);

// GOOD: Wrap in arrow function
const [fn, setFn] = useState(() => someFunction);
setFn(() => newFunction);
```

### Updating Objects/Arrays

```jsx
// Object: spread and override
setForm({ ...form, email: newEmail });

// Nested object
setUser({
  ...user,
  address: { ...user.address, city: newCity }
});

// Array: filter, map, spread
setItems(items.filter(i => i.id !== id));        // Remove
setItems([...items, newItem]);                    // Add
setItems(items.map(i => i.id === id ? {...i, done: true} : i)); // Update
```

## Quick Reference

### DO

- Use for simple, local component state
- Create new objects/arrays when updating
- Use updater function when depending on previous state
- Use initializer function for expensive initial values

### DON'T

- Store computed/derived values
- Mutate existing state objects/arrays
- Read state immediately after setting (it's a snapshot)
- Call `setState` unconditionally during render

## Alternative Hooks Comparison

| Hook | Use When |
|------|----------|
| `useState` | Simple state, primitives, basic objects |
| `useReducer` | Complex state logic, multiple sub-values |
| `useRef` | Mutable values without re-renders |
| `useMemo` | Expensive computed values |
| `useContext` | State shared across component tree |

## References

- [React Docs - useState](https://react.dev/reference/react/useState)
- [React Docs - State as a Snapshot](https://react.dev/learn/state-as-a-snapshot)
- [React Docs - Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure)

Overview

This skill guides correct and efficient use of React's useState hook. It explains when useState is the right choice, when to prefer alternatives, and common pitfalls that cause stale updates or ignored changes. Followable rules and patterns help keep state predictable and performant.

How this skill works

The skill inspects component state scenarios and recommends the appropriate approach: useState for simple local state, useReducer for complex transitions, useRef for mutable values that shouldn't trigger renders, and useMemo for expensive derived values. It highlights critical rules like immutability, updater functions for sequential changes, initializer functions for costly defaults, and the top-level hooks rule.

When to use it

  • Local component values that trigger re-renders (form inputs, toggles, counters).
  • Simple state transitions where you replace a value directly.
  • When you need to persist a value across renders without external storage.
  • Not for global/shared state or values derivable from props/state.

Best practices

  • Never mutate state directly; always return new objects/arrays (spread, map, filter).
  • Use the functional updater (setState(prev => next)) when new state depends on previous state.
  • Provide an initializer function to avoid expensive work on every render (useState(() => init())).
  • Call hooks only at the top level of components; never inside conditionals or loops.
  • Compute derived values during render or with useMemo instead of storing redundant state.

Example use cases

  • Form input values: const [name, setName] = useState('') and update onChange.
  • UI toggles and modals: const [isOpen, setIsOpen] = useState(false).
  • Local lists: setItems(items => [...items, newItem]) for adding without mutating.
  • Sequential counters: setCount(c => c + 1) called multiple times to accumulate updates.
  • Expensive default state: const [todos, setTodos] = useState(() => createTodos()).

FAQ

Why didn't React update when I mutated an object and called setState?

React compares references; mutating the same object keeps the reference unchanged. Create a new object/array and pass that to setState so React sees the change.

I called setState then logged the value and it was old. Is that a bug?

No. State updates are asynchronous snapshots. Use the updater function or read the new value from a computed variable you set before calling setState.