Contents
August 1, 2024 · View on GitHub
- Thoughts on the PENDING Resource
- Implicit dependent resources
- Unfetched Resources
- Loading Overlays
- Recaching newly-saved models
Thoughts on the PENDING Resource
Using dependsOn in simple cases like the one highlighted in the README is pretty straightforward and very powerful. But PENDING resources bring additional complexities to your resource logic, some of which are enumerated here:
-
PENDINGcritical resources don’t contribute toisLoading/hasErroredstates, but will keep your component from reaching ahasLoadedstate. Semantically, this makes sense, becausehasLoadedshould only be true when all critical resources have loaded, regardless of when a resource’s request is made. -
When a
PENDINGresource request is not in flight, its model prop will still be the empty model instance whose properties are frozen (as happens when the resource is in the other three possible loading states). This is to more predictably handle our resources in our components. We don’t need to be defensive with syntax like:todosCollection && todosCollection.toJSON(); // unnecessaryFurthermore, if you've defined additional instance methods on your model, they will be present without being defensive:
todosCollection.myInstanceMethod(); // guaranteed not to error regardless of loading state -
When a previously-
PENDINGbut currentlyLOADEDresource has its dependent prop removed, it goes back to aPENDINGstate (recall that if the dependent prop is changed, it gets put back into aLOADINGstate while the new resource is fetched). This puts us in an interesting state:-
hasInitiallyLoadedwill remain true, as expected. But thePENDINGresource’s prop—assuming the resource’sdependencieslist includes the dependent prop—will now return to the empty model/collection, so any child component that may remain in place afterhasInitiallyLoadedmay need to keep that in mind (ifshouldComponentUpdatereturns false when the new resource fetches, then this shouldn’t matter—see the next point). -
Recall that we can provide the dependent prop in one of two ways:
- We can include it in another resource’s
providesproperty, in which case the dependent prop gets set as state within resourcerer.- We can also set state in our client component and pass it to the executor function, but that requires an extra render cycle.
- We can modify the url in the component’s
componentDidUpdate/useEffect(either url path or query parameter), which will filter the prop down.
When we provide using method (a), the dependent prop can be changed but not removed. When we provide using method (b), the dependent prop can be changed or removed.
As an example of how we might be able to remove a dependent prop via method (b), consider someone navigating to a
/todosurl that auto-navigates to the first todo item and displays its details. ThetodoItemdetails resource depends on atodoIdprop, which it gets in auseEffectvia changing the url once thetodosresource loads. So now we’re at/todos/todo1234. But if the user clicks the back button, we’ll be back at/todoswith a cachedtodosresource andPENDINGtodoItemresource, and all three loading states set tofalse. (Yes, this is a bit contrived because you should actually replace the history entry in this case, but hopefully it helps to illuminate the issue.)So if we remove the dependent prop, we enter a state where
isLoading,hasLoaded, andhasErroredare all false. And since we have to wait for auseEffectto re-auto-update the url with the dependent prop, a lifecycle passes with this state, and there’s really nothing we can do about it.And again—
hasInitiallyLoadedis still true and thetodoItemModelmodel prop is empty, which can cause layout issues if you use, for example, an overlaid loader over a previously-rendered component. For this reason, if using classes/withResources, such a previously-rendered component should usenextProps.hasLoadedinstead of!nextProps.isLoadingin itsshouldComponentUpdate:// overlay-wrapped component, where a loader will show over previously-rendered children, // which we want to then not update. but this component is also a child of a `withResources` // component that has a dependent resource shouldComponentUpdate(nextProps) { // using `return !nextProps.isLoading;` would update the component in the above // scenario, even though `nextProps.myDependentModel` would be empty return nextProps.hasLoaded; } // this assumes that the parent is handling the `hasErrored` state. if it is not, then // you may need to instead use: shouldComponentUpdate(nextProps) { return !(nextProps.isLoading || areAnyPending(nextProps.myDependentLoadingState)); }If using
useResources, we'll want to do the equivalent in our memo:const MemoizedComponent = memo(<Component />, (prevProps, nextProps) => !nextProps.hasLoaded); function Parent() { return ( <div> {isLoading ? <OverlayLoader /> : null} {hasInitiallyLoaded ? <MemoizedComponent /> : null} </div> ); } - We can include it in another resource’s
-
In the case that the model's
dependenciesdoes not include the dependent prop (ie, the prop is used solely for triggering the resource request and doesn't factor into the request data), the model will still exist in the cache when the dependent prop is removed. In this case, the loading state is still returned to PENDING, but the existing model will also be present in the return value.
-
Implicit dependent resources
Another way to effectively have a dependent resource is to use a conditional in your getResources method:
const getResources = (props) => ({
todoos: {},
...(props.todoId ? {todoItem: {data: {id: props.todoId}} : {})
});
In general, using dependsOn is much more preferable, both in terms of semantics and functionality. The key difference here is that the dependent resource does not get put into a PENDING state, and hasLoaded depends on an unpredictable number of resources—for example, in the above scenario, what happens if props.todoId never arrives? Using dependsOn, hasLoaded would not be true, but using the conditional, it would be. This means that with the conditional, you can’t freely make assumptions behind the hasLoaded flag:
{hasLoaded ? (
// with the conditional, you don't know which resources are available. with
// `dependsOn`, you do
) : null}
That doesn’t mean that the conditional can’t be useful—it’s just that its use should be relegated to components that have two discrete forms—one in which the dependent prop is always present, and one in which the dependent prop is never present. If you’re unsure whether a prop might exist, notably because it comes from a providing resource, you should use dependsOn. A good example of when to use a conditional is in this fake component that sometimes fetches a user model and sometimes fetches an order model depending on the presence of an orderId prop:
const getResources = ({userId, orderId}) => ({
...userId ? {user: {path: {userId}} : {},
...!userId && orderId ? {
order: {
noncritical: true,
data: {id: orderId}
}
} : {}
});
In this case, when the component is used as an order component (denoted by the presence of the orderId prop), we fetch the order resource. Otherwise, we don’t.
For all other uses of dependent resources, we should use dependsOn.
Loading Overlays
You probably want to show loaders when you are moving between one model and the next. Because your resources are held as state, both exist in this intermediate state.
Both the hook and the HOC provide a hasInitiallyLoaded prop that is useful here. Here's an example of what it might look like (shown over one of Sift's Insights Charts):

Use it like:
import {useResources} from 'resourcerer';
const getResources = (props) => ({todos: {}});
export default function UserTodos(props) {
const {isLoading, hasInitiallyLoaded, todosCollection} = useResources(getResources, props);
return (
<div className='MyComponent'>
// it's up to you to make this an OverlayLoader or InlineLoader, if you so choose.
{isLoading ? <Loader /> : null}
// this will render once first loaded and remain rendered with a previous
// model even while in a loading state. when a new resource request returns,
// this will render with the updated model and the loader will be removed.
{hasInitiallyLoaded ? (
<ul>
{todosCollection.map((todoModel) => (
<li key={todoModel.id}>{todoModel.get('name')}</li>
))}
</ul>
) : null}
</div>
);
}
Recaching newly-saved models
A common pattern when creating a new instance of a resource is to keep it in state until the user decides to explicitly save it. For example, when setting up a new
TODO, we might fill out a form with its name, all kept in state, at a deep-linkable RESTful url that might be /todos/new. After the user saves the TODO, we get a
server-provided id property and we might navigate to /todos/12345, where 12345 is the new id. This presents an inconvenience: at /new we want to read from
state, but at /{id} we want to read from the saved model (this is compounded if the UI allows the user to edit in-place).
To accomodate this scenario, resourcerer provides a means of re-caching a model instance the first time it receives an id. Thus you can use it with fetch: false like
this:
import {useResources} from 'resourcerer';
const getResources = (props) => ({
todo: {
data: {id: props.id},
fetch: !!props.id
}
});
export default function UserTodo(props) {
const {todoModel} = useResources(getResources, props),
onChange = (evt) => todoModel.set('name', evt.target.value),
onSubmit = todoModel.save()
.then(([model]) => !props.id ? navigate(`/todos/${model.id}`) : null)
.catch(() => notify('An error occurred');
return (
<form onSubmit={onSubmit}>
<label>
TODO:
<input name='name' onChange={onChange} value={todoModel.get('name')} />
</label>
<button>Save</button>
</form>
);
}
What's so nice about this example is that there's no balancing between React state and model state; in either case you read from
the model requested from resourcerer. When you load /todos/new, nothing is fetched and the model is created first client-side;
when you load /todos/{todosId}, the todos resource is first fetched. Both cases, however, are treated identically. And because of
resourcerer's recaching, when you save for the first time and navigate from /todos/new to /todos/{todosId}, the model is
taken from its 'new' cache key and placed in its 'id' cache key, obviating the need to re-request the resource and moving seamlessly
from one to the other.
NOTE: this is only for models requested individually and not as part of a larger collection. When adding a new model to a collection
that is fetched by resourcerer, you can accomplish the above without recaching.