react-ketting
Version:
Ketting bindings for React
188 lines (154 loc) • 5.01 kB
text/typescript
import { Resource, State as ResourceState } from 'ketting';
import { useState, useEffect } from 'react';
import { ResourceLike } from '../util';
import { useClient } from './use-client';
import { Client } from 'ketting';
import { useResolveResource } from './use-resolve-resource';
type UseReadResourceResponse<T> = {
// True if there is no data yet
loading: boolean;
error: Error | null;
/**
* The ResourceState.
*
* Note that this will be `null` until loading is "false".
*/
resourceState: ResourceState<T>;
/**
* The 'real' resource.
*
* This will be `null` until we have it. It's not typed null because it
* makes it very clumsy to work with the hook.
*/
resource: Resource<T>;
/**
* Change the resource that the hook uses.
*
* A reason you might want to do this is if the resource itself changed
* uris.
*/
setResource(resource: Resource<T>): void;
}
export type UseReadResourceOptions<T> = {
initialState?: ResourceState<T>;
refreshOnStale?: boolean;
/**
* HTTP headers to include if there was no existing cache, and the initial
* GET request must be done to get the state.
*
* These headers are not used on subsequent refreshes/stale cases.
*/
initialGetRequestHeaders?: Record<string, string>;
};
/**
* The useReadResource hook is an internal hook that helps setting up a lot of
* the plumbing for dealing with resources and state.
*
* It's not recommended for external users to use this directly, instead use
* one of the more specialized hooks such as useResource or useCollection.
*
* Example call:
*
* <pre>
* const {
* loading,
* error,
* resourceState,
* } = useResource(resource);
* </pre>
*
* Returned properties:
*
* * loading - will be true as long as the result is still being fetched from
* the server.
* * error - Will be null or an error object.
* * resourceState - A state object. The `.data` property of this object will
* contain the parsed JSON from the server.
*/
export function useReadResource<T>(resourceLike: ResourceLike<T>, options: UseReadResourceOptions<T>): UseReadResourceResponse<T> {
const { resource, setResource } = useResolveResource(resourceLike);
const initialState = options.initialState;
const refreshOnStale = options.refreshOnStale || false;
const client = useClient();
const [resourceState, setResourceState] = useResourceState(resource, initialState, client);
const [loading, setLoading] = useState(resourceState === undefined);
const [error, setError] = useState<null|Error>(null);
useEffect(() => {
// This effect is for setting up the onUpdate event
if (resource === null) {
return;
}
const onUpdate = (newState: ResourceState<T>) => {
setResourceState(newState.clone());
setLoading(false);
};
const onStale = () => {
if (refreshOnStale) {
resource
.refresh()
.catch(err => {
setError(err);
setLoading(false);
});
}
};
resource.on('update', onUpdate);
resource.on('stale', onStale);
return function unmount() {
resource.off('update', onUpdate);
resource.off('stale', onStale);
};
}, [resource]);
useEffect(() => {
// This effect is for fetching the initial ResourceState
if (resource===null) {
// No need to fetch resourceState for these cases.
return;
}
if (resourceState && resourceState.uri === resource.uri) {
// Don't do anything if we already have a resourceState, and the
// resourceState's uri matches what we got.
return;
}
// The 'resource' property has changed, so lets get the new resourceState and data.
const cachedState = resource.client.cache.get(resource.uri);
if (cachedState) {
setResourceState(cachedState);
setLoading(false);
return;
}
setResourceState(undefined);
setLoading(true);
resource.get({ headers: options.initialGetRequestHeaders })
.catch(err => {
setError(err);
setLoading(false);
});
}, [resource]);
const result: UseReadResourceResponse<T> = {
loading,
error,
resourceState: resourceState as ResourceState<T>,
resource: resource as Resource<T>,
setResource,
};
return result;
}
/**
* Internal helper hook to deal with setting up the resource state, and
* populate the cache.
*/
function useResourceState<T>(
resource: Resource<T> | null,
initialData: undefined | ResourceState<T>,
client: Client,
): [ResourceState<T>|undefined, (rs: ResourceState<T>|undefined) => void] {
let data: undefined| ResourceState<T> = undefined;
if (initialData) {
data = initialData;
} else if (resource instanceof Resource) {
data = client.cache.get(resource.uri) || undefined;
}
const [resourceState, setResourceState] = useState<ResourceState<T>| undefined>(data);
return [resourceState, setResourceState];
}