In the previous lesson, we created a basic state machine with simple states and transitions. Now, we will explore two important features of XState: context and side effects. Context allows us to store additional data within a state machine, while side effects enable us to handle external operations, such as making API calls, during state transitions.
In this lesson, you will learn:
In XState, context is used to hold extra information or data that isn't part of the finite states. While states define the condition of the system, context stores data like counters, form values, or any dynamic data that might change over time.
Let's create a state machine that uses context to manage a counter. The counter starts at zero and increments by one every time a button is clicked.
import { createMachine, assign } from 'xstate';
const counterMachine = createMachine({
id: 'counter',
initial: 'active',
context: {
count: 0 // Initial context value
},
states: {
active: {
on: {
INCREMENT: {
actions: 'incrementCount' // Action to increment the count
}
}
}
}
}, {
actions: {
incrementCount: assign({
count: (context) => context.count + 1 // Updates the context count
})
}
});
export default counterMachine;
count: 0
is the initial value).incrementCount
action increments the count.Now, let's integrate this machine into a React component and display the counter.
import React from 'react';
import { useMachine } from '@xstate/react';
import counterMachine from './counterMachine';
const Counter = () => {
const [state, send] = useMachine(counterMachine); // useMachine hook to use the state machine
return (
<div>
<p>Current Count: {state.context.count}</p> {/* Displays the count from context */}
<button onClick={() => send('INCREMENT')}> {/* Sends the INCREMENT event */}
Increment
</button>
</div>
);
};
export default Counter;
state.context
.INCREMENT
event to trigger the state transition and increment the count.In addition to managing state, applications often need to handle side effects. Side effects are external operations such as API requests, logging, or timers that occur during state transitions. XState provides two main ways to handle side effects:
Let's add a side effect that logs a message to the console every time the counter is incremented.
import { createMachine, assign } from 'xstate';
const counterMachine = createMachine({
id: 'counter',
initial: 'active',
context: {
count: 0
},
states: {
active: {
on: {
INCREMENT: {
actions: ['incrementCount', 'logCount'] // Executes both actions on INCREMENT
}
}
}
}
}, {
actions: {
incrementCount: assign({
count: (context) => context.count + 1
}),
logCount: (context) => console.log(`Count is now ${context.count + 1}`) // Logs the new count
}
});
export default counterMachine;
INCREMENT
event occurs. The action runs alongside the incrementCount
action.For asynchronous operations, such as making API calls, XState uses services. Services are functions that run during state transitions and return promises to handle asynchronous actions.
Let’s modify our state machine to simulate fetching data from an API when the counter is incremented. The machine will go into a loading state while the data is fetched.
import { createMachine, assign } from 'xstate';
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: {
data: null,
error: null
},
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
invoke: {
src: 'fetchData', // The service that fetches data
onDone: {
target: 'success',
actions: assign({ data: (context, event) => event.data }) // Stores fetched data in context
},
onError: {
target: 'failure',
actions: assign({ error: (context, event) => event.data }) // Stores error in context
}
}
},
success: {
type: 'final'
},
failure: {
on: {
RETRY: 'loading' // Retry fetching data on error
}
}
}
}, {
services: {
fetchData: () => fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
}
});
export default fetchMachine;
fetchData
is a service that fetches data from an API.fetchData
.context.data
.context.error
.fetchData
are defined.Here’s how to integrate the machine with React to fetch data when a button is clicked.
import React from 'react';
import { useMachine } from '@xstate/react';
import fetchMachine from './fetchMachine';
const FetchData = () => {
const [state, send] = useMachine(fetchMachine);
return (
<div>
{state.matches('idle') && <button onClick={() => send('FETCH')}>Fetch Data</button>}
{state.matches('loading') && <p>Loading...</p>}
{state.matches('success') && <p>Data: {JSON.stringify(state.context.data)}</p>}
{state.matches('failure') && <div>
<p>Error: {state.context.error}</p>
<button onClick={() => send('RETRY')}>Retry</button>
</div>}
</div>
);
};
export default FetchData;
FETCH
event starts the data fetching process.In this lesson, we learned how to use context in XState to manage additional data and how to handle both synchronous and asynchronous side effects. We explored how actions can trigger side effects during state transitions and how services can be used for asynchronous operations like fetching data from an API.
In the next lesson, we’ll explore advanced state management techniques using hierarchical and parallel states in XState.