useSelector hook subscribes to specific derived state from an actor, only triggering re-renders when the selected value changes. This enables fine-grained reactivity and performance optimization.
Type Signature
function useSelector<
TActor extends Pick<AnyActorRef, 'subscribe' | 'getSnapshot'> | undefined,
T
>(
actor: TActor,
selector: (snapshot: SnapshotFrom<TActor>) => T,
compare?: (a: T, b: T) => boolean
): T
Parameters
actor- The actor reference to subscribe to (orundefined)selector- Function that derives a value from the actor’s snapshotcompare(optional) - Custom equality function to determine if the selected value changed (defaults to===)
Return Value
Returns the selected value from the actor’s current snapshot.Basic Usage
Select Context Value
import { useActorRef, useSelector } from '@xstate/react';
import { createMachine, assign } from 'xstate';
const counterMachine = createMachine({
context: { count: 0, lastUpdate: null },
on: {
INCREMENT: {
actions: assign({
count: ({ context }) => context.count + 1,
lastUpdate: () => Date.now()
})
}
}
});
function Counter() {
const actorRef = useActorRef(counterMachine);
// Only re-renders when count changes, not when lastUpdate changes
const count = useSelector(actorRef, (state) => state.context.count);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => actorRef.send({ type: 'INCREMENT' })}>+</button>
</div>
);
}
Select Multiple Values
import { useActorRef, useSelector } from '@xstate/react';
import { createMachine, assign } from 'xstate';
const userMachine = createMachine({
context: {
name: 'Alice',
email: 'alice@example.com',
preferences: { theme: 'dark' }
},
on: {
UPDATE_NAME: {
actions: assign({
name: ({ event }) => event.name
})
},
UPDATE_EMAIL: {
actions: assign({
email: ({ event }) => event.email
})
}
}
});
function UserProfile() {
const userRef = useActorRef(userMachine);
return (
<div>
<UserName actorRef={userRef} />
<UserEmail actorRef={userRef} />
</div>
);
}
// Only re-renders when name changes
function UserName({ actorRef }) {
const name = useSelector(actorRef, (state) => state.context.name);
return <div>Name: {name}</div>;
}
// Only re-renders when email changes
function UserEmail({ actorRef }) {
const email = useSelector(actorRef, (state) => state.context.email);
return <div>Email: {email}</div>;
}
Advanced Usage
Custom Comparison Function
import { useActorRef, useSelector } from '@xstate/react';
import { createMachine, assign } from 'xstate';
const todoMachine = createMachine({
context: {
todos: [] as Array<{ id: number; text: string; completed: boolean }>
},
on: {
ADD_TODO: {
actions: assign({
todos: ({ context, event }) => [
...context.todos,
{ id: Date.now(), text: event.text, completed: false }
]
})
},
TOGGLE_TODO: {
actions: assign({
todos: ({ context, event }) =>
context.todos.map(todo =>
todo.id === event.id
? { ...todo, completed: !todo.completed }
: todo
)
})
}
}
});
// Shallow array comparison
function shallowArrayCompare(a, b) {
if (a.length !== b.length) return false;
return a.every((item, index) => item === b[index]);
}
function TodoList() {
const todoRef = useActorRef(todoMachine);
// Only re-renders when the array reference or items change
const todos = useSelector(
todoRef,
(state) => state.context.todos,
shallowArrayCompare
);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoRef.send({
type: 'TOGGLE_TODO',
id: todo.id
})}
/>
{todo.text}
</li>
))}
</ul>
);
}
Deep Equality Comparison
import { useActorRef, useSelector } from '@xstate/react';
import { createMachine, assign } from 'xstate';
import isEqual from 'lodash/isEqual';
const formMachine = createMachine({
context: {
form: {
user: { name: '', email: '' },
preferences: { notifications: true }
}
},
on: {
UPDATE: {
actions: assign({
form: ({ context, event }) => ({
...context.form,
...event.updates
})
})
}
}
});
function UserPreferences() {
const formRef = useActorRef(formMachine);
// Only re-renders when preferences object deeply changes
const preferences = useSelector(
formRef,
(state) => state.context.form.preferences,
isEqual // Deep equality check
);
return (
<div>
<label>
<input
type="checkbox"
checked={preferences.notifications}
onChange={(e) => formRef.send({
type: 'UPDATE',
updates: {
preferences: {
notifications: e.target.checked
}
}
})}
/>
Enable notifications
</label>
</div>
);
}
Derived State Computation
import { useActorRef, useSelector } from '@xstate/react';
import { createMachine } from 'xstate';
const cartMachine = createMachine({
context: {
items: [
{ id: 1, name: 'Product 1', price: 10, quantity: 2 },
{ id: 2, name: 'Product 2', price: 20, quantity: 1 }
]
},
on: {
UPDATE_QUANTITY: {
actions: assign({
items: ({ context, event }) =>
context.items.map(item =>
item.id === event.id
? { ...item, quantity: event.quantity }
: item
)
})
}
}
});
function Cart() {
const cartRef = useActorRef(cartMachine);
return (
<div>
<CartItems actorRef={cartRef} />
<CartTotal actorRef={cartRef} />
</div>
);
}
function CartItems({ actorRef }) {
const items = useSelector(actorRef, (state) => state.context.items);
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - ${item.price} x {item.quantity}
</li>
))}
</ul>
);
}
function CartTotal({ actorRef }) {
// Computed value - only recalculates when total changes
const total = useSelector(
actorRef,
(state) => state.context.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
)
);
return <div>Total: ${total}</div>;
}
Conditional Selection
import { useActorRef, useSelector } from '@xstate/react';
import { createMachine } from 'xstate';
const dataMachine = createMachine({
initial: 'loading',
context: { data: null, error: null },
states: {
loading: {
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output })
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error })
}
}
},
success: {},
failure: {}
}
});
function DataDisplay() {
const dataRef = useActorRef(dataMachine);
const isLoading = useSelector(
dataRef,
(state) => state.matches('loading')
);
const data = useSelector(
dataRef,
(state) => state.context.data
);
const error = useSelector(
dataRef,
(state) => state.context.error
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Data: {JSON.stringify(data)}</div>;
}
With Actor Context
import { createActorContext } from '@xstate/react';
import { createMachine, assign } from 'xstate';
const appMachine = createMachine({
context: {
user: null,
theme: 'light',
notifications: []
},
initial: 'idle',
states: {
idle: {}
},
on: {
SET_THEME: {
actions: assign({
theme: ({ event }) => event.theme
})
},
ADD_NOTIFICATION: {
actions: assign({
notifications: ({ context, event }) => [
...context.notifications,
event.notification
]
})
}
}
});
const AppContext = createActorContext(appMachine);
function App() {
return (
<AppContext.Provider>
<Header />
<Notifications />
<ThemeSwitcher />
</AppContext.Provider>
);
}
// Only re-renders when theme changes
function Header() {
const theme = AppContext.useSelector((state) => state.context.theme);
return <header className={theme}>Header</header>;
}
// Only re-renders when notifications change
function Notifications() {
const notifications = AppContext.useSelector(
(state) => state.context.notifications
);
return (
<div>
{notifications.map((notif, i) => (
<div key={i}>{notif}</div>
))}
</div>
);
}
function ThemeSwitcher() {
const actorRef = AppContext.useActorRef();
const theme = AppContext.useSelector((state) => state.context.theme);
return (
<button onClick={() => actorRef.send({
type: 'SET_THEME',
theme: theme === 'light' ? 'dark' : 'light'
})}>
Toggle Theme
</button>
);
}
Handling Undefined Actors
import { useSelector } from '@xstate/react';
function OptionalDisplay({ actorRef }) {
// Returns undefined if actor is undefined
const value = useSelector(
actorRef,
(state) => state?.context.value ?? 'No data'
);
return <div>{value}</div>;
}
Error Handling
The hook automatically throws errors when the snapshot status is ‘error’:import { useActorRef, useSelector } from '@xstate/react';
import { ErrorBoundary } from 'react-error-boundary';
function DataComponent() {
const dataRef = useActorRef(dataMachine);
// Throws if snapshot.status === 'error'
const data = useSelector(dataRef, (state) => state.context.data);
return <div>{JSON.stringify(data)}</div>;
}
function App() {
return (
<ErrorBoundary fallback={<div>Error occurred</div>}>
<DataComponent />
</ErrorBoundary>
);
}
Performance Comparison
import { useActor } from '@xstate/react';
function Component() {
const [state] = useActor(machine);
// Re-renders whenever ANY context property changes
return <div>{state.context.count}</div>;
}
Common Selectors
// Select state value
const stateValue = useSelector(actorRef, (state) => state.value);
// Check if in state
const isActive = useSelector(actorRef, (state) => state.matches('active'));
// Select context property
const count = useSelector(actorRef, (state) => state.context.count);
// Select nested property
const userName = useSelector(actorRef, (state) => state.context.user.name);
// Derive computed value
const total = useSelector(
actorRef,
(state) => state.context.items.reduce((sum, item) => sum + item.price, 0)
);
// Select from tags
const hasError = useSelector(
actorRef,
(state) => state.hasTag('error')
);
// Check if state can transition
const canSubmit = useSelector(
actorRef,
(state) => state.can({ type: 'SUBMIT' })
);
Important Notes
- The component only re-renders when the selected value changes according to the comparison function
- Default comparison uses strict equality (
===) - Use custom comparison for objects/arrays to avoid unnecessary re-renders
- The selector function should be pure and deterministic
- If the actor is
undefined, the selector receivesundefinedas the snapshot - Automatically throws errors for snapshots with status ‘error’
See Also
- useActor - Create and subscribe to an actor
- useActorRef - Create an actor without subscribing
- createActorContext - Share actors via context