@throned/react-resource-ts v0.1.1
throned ▲ react-resource-ts
Install
npm install --save @throned/react-resource-ts @throned/resource-ts
yarn add @throned/react-resource-ts @throned/resource-ts
@throned/react-resource-ts has @throned/resource-ts in dependencies, so it
will be installed anyway.
Be aware that useResource hook uses AbortController so you may want to add a
polyfill.
Intro
React Resource provides hooks and components for Resource ADT.
useResource hook gives you the ability to wrap your asynchronous operation
into Resource data type and render each possible state of resource (Initial, Loading, Success, Failure components) as well as define your own matchers
(Match).
import * as React from 'react'
import {isLoading} from '@throned/resource-ts'
import {getStateComponents, useResource} from '@throned/react-resource-ts'
type Dog = {message: string}
const loadDog = async (): Promise<Dog> => {
return fetch('https://dog.ceo/api/breeds/image/random').then(res => {
if (!res.ok) {
throw new Error(res.statusText)
}
return res.json()
})
}
const App = () => {
const dog = useResource(loadDog, {defer: true})
const State = getStateComponents(dog.resource)
return (
<React.Fragment>
<State.Match states={['initial', 'loading']}>
<button onClick={dog.run} disabled={isLoading(dog.resource)}>
Show me doggo!
</button>
<State.Loading>
<p>Loading...</p>
</State.Loading>
</State.Match>
<State.Failure render={(error: Error) => <p>Oops! {error.message}</p>} />
<State.Success>
{({message}) => (
<div>
<h1>Nice</h1>
<img src={message} alt="dog" />
<button onClick={dog.run}>Show me more!</button>
</div>
)}
</State.Success>
</React.Fragment>
)
}API
useResource
const {cancel, reset, resource, run} = useResource<Params, Data, Error>(
load,
props,
)load
(params: Params, config?: Config) => Promise<Data>
Function that returns a promise and may fail with Error type (Error type that
provided is useResource type parameters). Accepts optional second parameter of
type Config. Config is { signal: AbortController.signal } that you can
pass into your fetch to enable cancellation.
props
type UseResourceProps<Params, Data, Error> = {
cancellable?: boolean
chain?: Resource<Params, unknown>
defer?: boolean
dependencies?: any[]
onFailure?: (error: Error) => void
onSuccess?: (data: Data) => void
params?: Params
skipLoadingFor?: number
}cancellable
boolean = true
If true will use Abort Controller abort() to cancel previous load on
subsequent load call.
chain
Resource<Params, unknown>
If chain is passed its resource.value will be used instead of params
prop when load is executed by useResource. You can use chain to wait until another resource is in success state to get params from it. When chain is
provided load function will be executed only when resource in chain is in
success.
import {useResource, Success} from '@throned/react-resource-ts'
import {map} from '@throned/resource-ts'
type UserMovie = {movieId: string}
const getUserMovie = () => Promise.resolve({movieId: '1234'})
const getMovie = (id: string) => Promise.resolve({title: 'The Departed'})
const Component = () => {
const user = useResource(getUserMovie)
const movie = useResource(getMovie, {
dependencies: [user.resource],
chain: map(({movieId}: UserMovie) => movieId)(user.resource),
})
return (
<Success of={movie.resource}>{movie => <h1>{movie.title}</h1>}</Success>
)
}defer
boolean = false
If you pass defer: true then load function won't be called automatically. To
run load manually you have to call run.
dependencies
any[] = []
Works the same way as dependencies array in useEffect hook. If value of
dependencies prop is changed between render useResource will execute load
function.
Be aware that if defer: true then load won't be executed.
onFailure
(error: Error) => void
Callback that is called when load rejects.
onSuccess
(data: Data) => void
Callback that is called when load resolves.
params
Params
These params will be provided as a first argument of load function. You can
overwrite them when calling run function by passing new params to it. Will be
ignored by automatic load execution if chain is provided.
skipLoadingFor
number = 150
Number of milliseconds that useResource will wait until changing state to
loading. Pass skipLoadingFor: 0 if you want to see loading state
immediatly after executing load function.
Returns
cancel
() => void
Cancels last running load function by calling abort() on Abort Controller
signal passed within Config. Be aware that it will work only if you handle
signal manually in your load, e.g. passing it into your fetch call.
reset
() => void
Cancles last running load function and sets resource into initial state.
resource
Resource<Data, Error>
Holds the state of load execution. To see what you can do with resource check
@throned/resource-ts
run
(runParams?: Params = props.params) => Promise<Resource<Data, Error>>
Call run to execute load manually. By default it cancels previous load
and uses params passed into props. You can disable auto-cancelling by
passing cancellable: false into props and you can overwrite default params
by providing runParams.
const {run} = useResource(load, {params: 42})
run() // load(42)
run(100) // load(100)bindError
<BindedError>() => useResource<Params, Data, Error = BindedError>
Binds the Error type of useResource. It's handy in cases where you want to
infer Params and Data from load function, but want to provide Error type
explicitly.
class HttpError extends Error {
/* */
}
const load = (): Promise<{username: string}> => {
/* */
}
const useHttp = bindError<HttpError>()
const {resource} = useHttp(load)
resource // Resource<{ username: string }, HttpError>createResource
(load: Load<Params, Data>) => (props: UseResourceProps) => useResource
Binds load function. Use it for creating reusable hook with the same load
function.
createResourceWithError
<BindedError>() => (load: Load<Params, Data>) => (props: UseResourceProps) => useResource<Params, Data, Error = BindedError>
Binds error and load function. Use it for creating reusable hook with the same load
function and explicit Error type.
Components
Use components to render the UI depending on Resource state.
You can provide of prop explicitly for each component.
const user = useResource(loadUser)
<div>
<Match of={user.resource} states={['initial', 'loading']}>
<p>Not ready yet</p>
</Match>
<Failure of={user.resource}>{error => /* */}</Failure>
<Success of={user.resource}>{user => /* */}</Success>
</div>But recommended way is to use getStateComponents to bind of prop.
const user = useResource(loadUser)
const State = getStateComponents(user.resource)
<div>
<State.Match states={['initial', 'loading']}>
<p>Not ready yet</p>
</State.Match>
<State.Failure>{error => /* */}</State.Failure>
<State.Success>{user => /* */}</State.Success>
</div>All components accepts render props and children (valid jsx or render
function). If you provide both children and render function - render will
take precedence.
Initial
type InitialProps = {
children?: ReactNode | (() => ReactNode);
of: AnyResource;
render?: () => ReactNode;
}Loading
type LoadingProps = {
children?: ReactNode | (() => ReactNode);
of: AnyResource;
render?: () => ReactNode;
}Success
type SuccessProps<D> = {
children?: ReactNode | ((value: D) => ReactNode);
of: Resource<D, unknown>;
render?: (value: D) => ReactNode;
}Failure
type FailureProps<E> = {
children?: ReactNode | ((error: E) => ReactNode);
of: Resource<unknown, E>;
render?: (error: E) => ReactNode;
}Match
type MatchProps<D, E> = {
children?: ReactNode | ((resource: Resource<D, E>) => ReactNode);
of: Resource<D, E>;
render?: (resource: Resource<D, E>) => ReactNode;
states: NotEmptyArray<Tag>;
}getStateComponents
(of: Resource<D, E>) => { Initial, Loading, Success, Failure, Match }
Returns state components with binded of prop.
Guides
Todo.