Overview

useStateTogether is the core hook of React Together. It creates state that is automatically synchronized across all users in your session. It works exactly like React’s useState, but any state changes are instantly shared with all connected users.

Perfect for: Shared counters, form data, game state, collaborative editing, or any state that should be the same for all users.

Basic Usage

import { useStateTogether } from 'react-together'

function SharedCounter() {
  const [count, setCount] = useStateTogether('counter', 0)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  )
}

Signature

useStateTogether<T>(
  key: string,
  initialValue: T,
  options?: UseStateTogetherOptions
): [T, Dispatch<SetStateAction<T>>]

Parameters

key
string
required

Unique identifier for this shared state. All users with the same key will share the same state.

initialValue
T
required

Initial value when the state is first created. This only applies when no user has set this state yet.

options
UseStateTogetherOptions

Configuration options for the shared state behavior

Options

options.resetOnDisconnect
boolean
default:"false"

Whether to reset to initial value when all users disconnect

options.throttleDelay
number
default:"100"

Milliseconds to throttle state updates (prevents excessive network traffic)

Return Value

Returns a tuple identical to useState:

[0]
T

The current state value, synchronized across all users

[1]
Dispatch<SetStateAction<T>>

Function to update the state. Accepts new value or updater function.

Examples

Simple Counter

function Counter() {
  const [count, setCount] = useStateTogether('counter', 0)
  
  return (
    <div>
      <h2>Shared Counter: {count}</h2>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

Shared Text Input

function SharedTextEditor() {
  const [text, setText] = useStateTogether('document', '')
  
  return (
    <div>
      <h3>Collaborative Document</h3>
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Start typing... others will see your changes in real-time!"
        rows={10}
        cols={50}
      />
      <p>Characters: {text.length}</p>
    </div>
  )
}

Complex Object State

interface UserPreferences {
  theme: 'light' | 'dark'
  fontSize: number
  showNotifications: boolean
}

function SharedSettings() {
  const [preferences, setPreferences] = useStateTogether<UserPreferences>(
    'preferences',
    {
      theme: 'light',
      fontSize: 14,
      showNotifications: true
    }
  )
  
  const updateTheme = (theme: 'light' | 'dark') => {
    setPreferences(prev => ({ ...prev, theme }))
  }
  
  const updateFontSize = (fontSize: number) => {
    setPreferences(prev => ({ ...prev, fontSize }))
  }
  
  return (
    <div style={{ 
      backgroundColor: preferences.theme === 'dark' ? '#333' : '#fff',
      color: preferences.theme === 'dark' ? '#fff' : '#000',
      fontSize: preferences.fontSize
    }}>
      <h3>Shared Settings</h3>
      
      <div>
        <label>Theme:</label>
        <select 
          value={preferences.theme} 
          onChange={(e) => updateTheme(e.target.value as 'light' | 'dark')}
        >
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </div>
      
      <div>
        <label>Font Size:</label>
        <input
          type="range"
          min="10"
          max="24"
          value={preferences.fontSize}
          onChange={(e) => updateFontSize(parseInt(e.target.value))}
        />
        <span>{preferences.fontSize}px</span>
      </div>
      
      <div>
        <label>
          <input
            type="checkbox"
            checked={preferences.showNotifications}
            onChange={(e) => setPreferences(prev => ({ 
              ...prev, 
              showNotifications: e.target.checked 
            }))}
          />
          Show Notifications
        </label>
      </div>
    </div>
  )
}

Array State Management

interface TodoItem {
  id: string
  text: string
  completed: boolean
  createdBy: string
}

function CollaborativeTodoList() {
  const [todos, setTodos] = useStateTogether<TodoItem[]>('todos', [])
  const [newTodo, setNewTodo] = useState('')
  
  const addTodo = () => {
    if (newTodo.trim()) {
      const todo: TodoItem = {
        id: crypto.randomUUID(),
        text: newTodo.trim(),
        completed: false,
        createdBy: 'current-user' // In real app, get from user context
      }
      setTodos(prevTodos => [...prevTodos, todo])
      setNewTodo('')
    }
  }
  
  const toggleTodo = (id: string) => {
    setTodos(prevTodos => 
      prevTodos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }
  
  const deleteTodo = (id: string) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id))
  }
  
  return (
    <div>
      <h3>Collaborative Todo List</h3>
      
      <div>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="Add a new todo..."
        />
        <button onClick={addTodo}>Add</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ 
              textDecoration: todo.completed ? 'line-through' : 'none' 
            }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
      
      <p>Total: {todos.length} | Completed: {todos.filter(t => t.completed).length}</p>
    </div>
  )
}

Advanced Usage

With Custom Options

function PersistentCounter() {
  const [count, setCount] = useStateTogether('persistent-counter', 0, {
    resetOnDisconnect: false,  // Keep state when users leave
    throttleDelay: 200         // Reduce update frequency
  })
  
  return (
    <div>
      <p>Persistent Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

Multiple Shared States

function GameState() {
  const [score, setScore] = useStateTogether('game-score', 0)
  const [level, setLevel] = useStateTogether('game-level', 1)
  const [players, setPlayers] = useStateTogether<string[]>('players', [])
  
  const levelUp = () => {
    if (score >= level * 100) {
      setLevel(l => l + 1)
      setScore(0) // Reset score for new level
    }
  }
  
  return (
    <div>
      <h3>Game Status</h3>
      <p>Level: {level}</p>
      <p>Score: {score} / {level * 100}</p>
      <p>Players: {players.length}</p>
      
      <button onClick={() => setScore(s => s + 10)}>
        Score Points (+10)
      </button>
      
      <button onClick={levelUp} disabled={score < level * 100}>
        Level Up
      </button>
    </div>
  )
}

Functional Updates

function StateUpdater() {
  const [data, setData] = useStateTogether('complex-data', { 
    users: [], 
    lastUpdate: Date.now() 
  })
  
  // Always use functional updates for complex state
  const addUser = (username: string) => {
    setData(prevData => ({
      ...prevData,
      users: [...prevData.users, username],
      lastUpdate: Date.now()
    }))
  }
  
  const removeUser = (username: string) => {
    setData(prevData => ({
      ...prevData,
      users: prevData.users.filter(u => u !== username),
      lastUpdate: Date.now()
    }))
  }
  
  return (
    <div>
      <h3>User Management</h3>
      <p>Last Updated: {new Date(data.lastUpdate).toLocaleTimeString()}</p>
      <ul>
        {data.users.map(user => (
          <li key={user}>
            {user}
            <button onClick={() => removeUser(user)}>Remove</button>
          </li>
        ))}
      </ul>
      <button onClick={() => addUser(`User${Date.now()}`)}>
        Add Random User
      </button>
    </div>
  )
}

Best Practices

Use Unique Keys

Choose descriptive, unique keys for each shared state

// Good
const [gameScore, setGameScore] = useStateTogether('game-score', 0)
const [playerName, setPlayerName] = useStateTogether('player-name', '')

// Avoid
const [data1, setData1] = useStateTogether('data', 0)
const [data2, setData2] = useStateTogether('data', '') // Same key!

Use Functional Updates

Always use functional updates for complex state

// Good - functional update
setTodos(prev => [...prev, newTodo])

// Risky - might overwrite concurrent changes
setTodos([...todos, newTodo])

Consider Performance

Use throttling for frequently updated state

const [mousePos, setMousePos] = useStateTogether('mouse', { x: 0, y: 0 }, {
  throttleDelay: 50 // Update max 20 times per second
})

Initialize Properly

Provide sensible initial values

// Good - proper defaults
const [config, setConfig] = useStateTogether('config', {
  theme: 'light',
  language: 'en'
})

// Avoid - undefined state
const [config, setConfig] = useStateTogether('config', undefined)

Common Patterns

Reset Functionality

function ResettableCounter() {
  const [count, setCount] = useStateTogether('counter', 0)
  const initialValue = 0
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(initialValue)}>Reset</button>
    </div>
  )
}

Conditional Updates

function ConditionalUpdates() {
  const [value, setValue] = useStateTogether('value', 0)
  
  const incrementIfValid = () => {
    setValue(prev => {
      // Only update if certain conditions are met
      if (prev < 100) {
        return prev + 1
      }
      return prev // No change
    })
  }
  
  return (
    <div>
      <p>Value: {value}</p>
      <button onClick={incrementIfValid} disabled={value >= 100}>
        Increment (max 100)
      </button>
    </div>
  )
}

TypeScript Support

useStateTogether is fully typed and provides excellent TypeScript support:

// Type is inferred from initial value
const [count, setCount] = useStateTogether('counter', 0) // number

// Explicit typing
const [user, setUser] = useStateTogether<User | null>('current-user', null)

// Complex types
interface GameState {
  level: number
  score: number
  players: Player[]
}

const [game, setGame] = useStateTogether<GameState>('game', {
  level: 1,
  score: 0,
  players: []
})

Troubleshooting

useStateTogether is the foundation of React Together. Once you understand how it works, you can build any kind of collaborative feature by combining it with other hooks and components!