@matheuspuel/query-state
v0.3.0
Published
Type safe, immutable query state representation with loading, success, and failure states.
Readme
QueryState
Type safe, immutable query state representation with loading, success, and failure states.
Useful for defining UI state related to asynchronous operations.
This allows use cases like:
- Show stale data (
success) while refreshing (loading) - Display an error (
failure) alongside stale data (success) - Track loading progress while retaining previous results
State properties
Each QueryState instance has three properties
that can be independently set or cleared:
| Property | Type | Meaning |
| --------- | ---------------------------------------------- | ------------------------------------ |
| loading | { time?: DateTime.Utc, progress: P } \| null | Current loading state. |
| success | { time?: DateTime.Utc, data: A } \| null | Last still valid, successful result. |
| failure | { time?: DateTime.Utc, error: E } \| null | Last still applicable failure. |
Combination of state properties
The possible combinations of state properties allow for a comprehensive representation of an operations' state.
| Loading | Success | Failure | Meaning | | ------- | ------- | ------- | ------------------------------------------------------------ | | - | - | - | The operation has not started. | | ✅ | - | - | In progress, no previous data or errors. | | - | ✅ | - | Successfully completed. | | - | - | ✅ | Failed. | | ✅ | ✅ | - | Refreshing with stale data available. | | ✅ | - | ✅ | Retrying after a previous failure. | | - | ✅ | ✅ | Has stale data, but failed while refreshing. | | ✅ | ✅ | ✅ | Has stale data, failed while refreshing and is now retrying. |
Key behaviors
start()setsloadingand preservessuccessandfailuresucceed()setssuccessand clearsloadingandfailurefail()setsfailure, clearsloading, but preservessuccess
Install
pnpm add @matheuspuel/query-stateUsage
import { QueryState } from '@matheuspuel/query-state'
// Create an empty QueryState
let queryState = QueryState.initial<
// success data type
{ count: number },
// failure error type
'NetworkError' | 'ValidationError'
>()
console.log(queryState)
// QueryState { loading: null, success: null, failure: null }
// Start
queryState = queryState.start()
console.log(queryState)
// QueryState { loading: { progress: undefined }, success: null, failure: null }
// Fail
queryState = queryState.fail({ error: 'NetworkError' })
console.log(queryState)
// QueryState { loading: null, success: null, failure: { error: 'NetworkError' } }
// Retry
queryState = queryState.start()
console.log(queryState)
// QueryState { loading: { progress: undefined }, success: null, failure: { error: 'NetworkError' } }
// Succeed
queryState = queryState.succeed({ data: { count: 1 } })
console.log(queryState)
// QueryState { loading: null, success: { data: { count: 1 } }, failure: null }
// Refresh, but fail and retry again
queryState = queryState.start()
console.log(queryState)
// QueryState { loading: { progress: undefined }, success: { data: { count: 1 } }, failure: null }
queryState = queryState.fail({ error: 'ValidationError' })
console.log(queryState)
// QueryState { loading: null, success: { data: { count: 1 } }, failure: { error: 'ValidationError' } }
queryState = queryState.start()
console.log(queryState)
// QueryState { loading: { progress: undefined }, success: { data: { count: 1 } }, failure: { error: 'ValidationError' } }Tracking progress
You can track the loading progress for showing a progress bar.
import { QueryState } from '@matheuspuel/query-state'
let queryState = QueryState.initial<
string,
Error,
// loading progress type
number
>()
// QueryState { loading: null, success: null, failure: null }
queryState = queryState.makeProgress(0.1)
console.log(queryState)
// QueryState { loading: { progress: 0.1 }, success: null, failure: null }
queryState = queryState.makeProgress(0.2)
console.log(queryState)
// QueryState { loading: { progress: 0.2 }, success: null, failure: null }Or you can use it to describe the current step in progress.
let queryState = QueryState.initial<
string,
Error,
'validating' | 'fetching' | 'calculating'
>().makeProgress('validating')
console.log(queryState)
// QueryState { loading: { progress: 'validating' }, success: null, failure: null }Set times
Set the time the operation started and completed to help determining how old the data is or how long the operation is taking to complete.
import { QueryState } from '@matheuspuel/query-state'
import { DateTime } from 'effect'
let queryState: QueryState<number, Error> = QueryState.initial()
console.log(queryState)
// Start and set time
queryState = queryState.start({
time: DateTime.unsafeMake('2026-01-01T00:00:00Z'),
})
console.log(queryState)
// QueryState {
// loading: { time: DateTime.Utc(2026-01-01T00:00:00.000Z), progress: undefined },
// success: null,
// failure: null
// }
// Succeed and set the result time
queryState = queryState.succeed({
data: 10,
time: DateTime.unsafeMake('2026-01-01T00:00:00Z'),
})
console.log(queryState)
// QueryState {
// loading: null,
// success: { data: 10, time: DateTime.Utc(2026-01-01T00:00:00.000Z) },
// failure: null
// }
// Fail and set the failure time
queryState = queryState.fail({
error: new Error('Something went wrong'),
time: DateTime.unsafeMake('2026-01-01T00:00:00Z'),
})
console.log(queryState)
// QueryState {
// loading: null,
// success: { data: 10, time: DateTime.Utc(2026-01-01T00:00:00.000Z) },
// failure: {
// error: Error: Something went wrong
// at ...
// time: DateTime.Utc(2026-01-01T00:00:00.000Z)
// }
// }Track Effect
Use QueryState.trackEffect to run an Effect while automatically updating the QueryState.
import { QueryState } from '@matheuspuel/query-state'
import { Effect } from 'effect'
const validate = (input: {
name: string
}): Effect.Effect<{ id: number }, 'ValidationError'> =>
Effect.succeed({ id: 3 })
const fetchResult = (data: {
id: number
}): Effect.Effect<{ count: number }, 'NetworkError'> =>
Math.random() > 0.7
? Effect.succeed({ count: 15 })
: Effect.fail('NetworkError')
let queryState = QueryState.initial<
{ count: number },
'NetworkError' | 'ValidationError',
'validating' | 'fetching'
>()
const updateQueryState = (
f: (state: typeof queryState) => typeof queryState,
) => {
queryState = f(queryState)
console.log(queryState)
}
const run = QueryState.trackEffect(
updateQueryState,
(input: { name: string }, update) =>
Effect.gen(function* () {
const data = yield* validate(input)
update(_ => _.makeProgress('fetching'))
const result = yield* fetchResult(data)
return result
}),
{ initialProgress: 'validating' },
)
Effect.gen(function* () {
const result = yield* run({ name: 'Test' }).pipe(Effect.retry({}))
console.log('Final result:', result)
}).pipe(Effect.runSync)
// QueryState { loading: { time: DateTime.Utc(2026-02-11T20:39:27.392Z), progress: 'validating' }, success: null, failure: null }
// QueryState { loading: { time: DateTime.Utc(2026-02-11T20:39:27.392Z), progress: 'fetching' }, success: null, failure: null }
// QueryState { loading: null, success: null, failure: { time: DateTime.Utc(2026-02-11T20:39:27.392Z), error: 'NetworkError' } }
// QueryState { loading: { time: DateTime.Utc(2026-02-11T20:39:27.403Z), progress: 'validating' }, success: null, failure: { time: DateTime.Utc(2026-02-11T20:39:27.392Z), error: 'NetworkError' } }
// QueryState { loading: { time: DateTime.Utc(2026-02-11T20:39:27.403Z), progress: 'fetching' }, success: null, failure: { time: DateTime.Utc(2026-02-11T20:39:27.392Z), error: 'NetworkError' } }
// QueryState { loading: null, success: { time: DateTime.Utc(2026-02-11T20:39:27.403Z), data: { count: 15 } }, failure: null }
// Final result: { count: 15 }