DEV Community

Cover image for React + TypeScript: Smarter State Management with useState and readonly
Arka
Arka

Posted on

React + TypeScript: Smarter State Management with useState and readonly

Managing state in React becomes more predictable and error resistant when you combine the power of TypeScript’s type system with immutability features like readonly. In this post, we’ll explore how to strongly type your state with useState(), enforce immutability using readonly, and even go deeper with ReadonlyDeep for complex structures. These patterns not only improve IntelliSense and prevent bugs but also align perfectly with React’s rendering philosophy.


🧠 useState<Type>() in TypeScript

  • Strongly types state variables for better safety and IntelliSense.
  • Prevents accidental type mismatches during state updates.
  • Encourages predictable and maintainable state management.
 const [count, setCount] = useState<number>(0); // Strongly typed as number
Enter fullscreen mode Exit fullscreen mode

🔒 readonly in TypeScript

  • Enforces immutability on objects and arrays.
  • Prevents reassignment or mutation of properties.
  • Useful for protecting state structures from unintended changes.

Individual readonly on each property (manual)


type User = {
  readonly id: number;
  readonly name: string;
};

const [user, setUser] = useState<User>({
  id: 1,
  name: 'Arka',
});

// user.id = 2; ❌ Error: Cannot assign to 'id' because it is a read-only property
Enter fullscreen mode Exit fullscreen mode

Using Readonly directly (automatic)


type User = {
  id: number;
  name: string;
};

const [user, setUser] = useState<Readonly<User>>({
  id: 1,
  name: 'Arka',
});

// user.id = 2; ❌ Error: Cannot assign to 'id' because it is a read-only property

Updating State Safely
const updateName = (newName: string) => {
  setUser({ ...user, name: newName }); // ✅ Immutable update
};

Enter fullscreen mode Exit fullscreen mode

📦 readonly vs ReadonlyDeep

  • readonly only applies to top-level properties.
  • For deeply nested immutability, use utility types like ReadonlyDeep<T> (via libraries or custom types).
  • React state usually benefits from shallow immutability, but deep immutability can help in complex structures.

Install type-fest

npm install type-fest
Enter fullscreen mode Exit fullscreen mode

React + TypeScript Example with ReadonlyDeep


import { useState } from 'react';
import type { ReadonlyDeep } from 'type-fest';

type Address = {
  city: string;
  zip: number;
};

type User = {
  id: number;
  name: string;
  address: Address;
};

function Profile() {
  const [user, setUser] = useState<ReadonlyDeep<User>>({
    id: 1,
    name: 'Arka',
    address: {
      city: 'Bangalore',
      zip: 560001,
    },
  });

  // ❌ These will all error due to deep readonly
  // user.name = 'New Name';
  // user.address.city = 'Mumbai';

  const updateCity = (newCity: string) => {
    setUser({
      ...user,
      address: {
        ...user.address,
        city: newCity,
      },
    });
  };

  return (
<div>
<p>{user.name} lives in {user.address.city}</p>
<button onClick={() => updateCity('Mumbai')}>Move to Mumbai</button>
</div>
  );
}

Enter fullscreen mode Exit fullscreen mode

⚠️ Why readonly is useful for objects and arrays

  • Makes entire objects or arrays immutable, not just single keys.
  • Prevents direct mutation, which can break React’s render cycle.
  • Especially important when passing props or managing shared state.

Why readonly is useful for objects and arrays


const numbers: readonly number[] = [1, 2, 3];

// numbers.push(4); ❌ Error: push does not exist on readonly array
Enter fullscreen mode Exit fullscreen mode

🚫 Avoid Direct Mutation of State

  • Always use the setter function from useState.
  • Direct mutation can lead to stale renders or unexpected behavior.
  • readonly helps enforce this pattern and catch errors early.

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState<string[]>(['Learn TS']);

  const addTodo = (todo: string) => {
    // ❌ Direct mutation (bad)
    // todos.push(todo);

    // ✅ Correct way
    setTodos([...todos, todo]);
  };

  return <button onClick={() => addTodo('Write blog')}>Add Todo</button>;
}

Enter fullscreen mode Exit fullscreen mode

🧩 readonly Arrays in React

  • Prevents use of mutating methods like push, pop, splice, etc.
  • Encourages use of non-mutating patterns like map, filter, concat.
  • Aligns with React’s expectation of immutability for re-renders.

✅ Best Practices

  • Combine useState with proper typing for safer updates.
  • Use readonly in types to protect data structures.
  • Avoid mutation to maintain React’s predictable rendering behavior.

Top comments (0)