Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Declarative data-fetching and caching framework for REST APIs with React

License

NotificationsYou must be signed in to change notification settings

noahgrant/resourcerer

Repository files navigation

Resourcerer Icon

resourcerer

resourcerer is a library for declaratively fetching and caching your application's data. Its powerfuluseResources React hook orwithResources higher-order React component (HOC) allows you to easily construct a component's data flow, including:

  • serial requests
  • prioritized rendering for critical data (enabling less critical or slower requests to not block interactivity)
  • delayed requests
  • prefetching
  • ...and more

Additional features include:

  • fully declarative (no more writing any imperative Fetch API calls)
  • first-class loading and error state support
  • smart client-side caching
  • lazy fetching
  • refetching
  • forced cache invalidation
  • updating a component when a resource updates
  • zero dependencies
  • < 6kB!

Getting started is easy:

  1. Define a model in your application (these are classes descending fromModel orCollection):
// js/models/todos-collection.jsimport{Collection}from'resourcerer';exportdefaultclassTodosCollectionextendsCollection{url(){return'/todos';}}
  1. Create a config file in your application and add your constructor to the ModelMap with a key:
// js/core/resourcerer-config.jsimport{register}from'resourcerer';importTodosCollectionfrom'js/models/todos-collection';// choose any string as its key, which becomes its ResourceKeyregister({todos:TodosCollection});
// in your top level js fileimport'js/core/resourcerer-config';
  1. Use your preferred abstraction (useResources hook orwithResources HOC) to request your models in any component:

    1. useResources

      import{useResources}from'resourcerer';// tell resourcerer which resource you want to fetch in your componentconstgetResources=(props)=>({todos:{}});functionMyComponent(props){const{    isLoading,    hasErrored,    hasLoaded,    todosCollection}=useResources(getResources,props);// when MyComponent is mounted, the todosCollection is fetched and available// as `todosCollection`!return(<divclassName='MyComponent'>{isLoading ?<Loader/> :null}{hasErrored ?<ErrorMessage/> :null}{hasLoaded ?(<ul>{todosCollection.toJSON().map(({id, name})=>(<likey={id}>{name}</li>))}</ul>) :null}</div>);}
    2. withResources

      importReactfrom'react';import{withResources}from'resourcerer';// tell resourcerer which resource you want to fetch in your component@withResources((props)=>({todos:{}}))classMyComponentextendsReact.Component{render(){// when MyComponent is mounted, the todosCollection is fetched and available// as `this.props.todosCollection`!return(<divclassName='MyComponent'>{this.props.isLoading ?<Loader/> :null}{this.props.hasErrored ?<ErrorMessage/> :null}{this.props.hasLoaded ?(<ul>{this.props.todosCollection.map((todoModel)=>(<likey={todoModel.id}>{todoModel.get('name')}</li>))}</ul>) :null}</div>);}}

There's a lot there, so let's unpack that a bit. There's also a lot more that we can do there, so let's also get into that. But first, some logistics:

Contents

  1. Installation
  2. Nomenclature
  3. Tutorial
    1. Intro
    2. Other Props Returned from the Hook/Passed from the HOC (Loading States)
    3. Requesting Prop-driven Data
    4. Changing Props
    5. Common Resource Config Options
      1. params
      2. options
      3. noncritical
      4. force
      5. Custom Resource Names
      6. prefetches
      7. data
      8. lazy
      9. minDuration
      10. dependsOn
      11. provides
    6. Data mutations
    7. Serial Requests
    8. Differences between useResources and withResources
    9. Using resourcerer with TypeScript
    10. Caching Resources with ModelCache
    11. Declarative Cache Keys
    12. Prefetch on Hover
    13. Refetching
    14. Cache Invalidation
    15. Tracking Request Times
  4. Configuring resourcerer
  5. FAQs
  6. Migrating to v2.0

Installation

$ npm i resourcerer oryarn add resourcerer

resourcerer requires on React >= 16.8 but has no external dependencies.

Note: Resourcerer is written in TypeScript and is compiled to ESNext. It does no further transpiling—includingimport/export.If you are using TypeScript yourself, this won't be a problem. If you're not, and you're not babelifying (or similar) yournode_modules folder, you'll need to make an exception for this package, ie:

// webpack.config.js or similarmodule:{rules:[{test:/\.jsx?$/,exclude:/node_modules\/(?!(resourcerer))/,use:{loader:'babel-loader?cacheDirectory=true'}}]}

Nomenclature

  1. Props. Going forward in this tutorial, we'll try to describe behavior of both theuseResources hook and thewithResources HOC at once; we'll also rotate between the two in examples. Note that if we talking about a passed prop of, for exampleisLoading, that that corresponds to anisLoading property returned from the hook and athis.props.isLoading prop passed down from the HOC.

  2. ResourceKeys. These are the keys of the object passed to theregister function in your top-levelresourcerer-config.js file (discussed above in the introduction). The object is of typeRecord<ResourceKeys, new () => Model | new () => Collection>. These keys are passed to the executor functions and are used to tell the hook or HOC which resources to request.

  3. Executor Function. The executor function is a function that both the hook and HOC accept that declaratively describes which resources to request and with what config options. In these docs you'll often see it assigned to a variable calledgetResources. It acceptsprops as arguments and may look like, as we'll explore in an example later:

    constgetResources=(props)=>({user:{path:{userId:props.id}}});

    or

    constgetResources=(props)=>{constnow=Date.now();return{userTodos:{params:{limit:20,end_time:now,start_time:now-props.timeRange,sort_field:props.sortField}}};};

    It returns an object whose keys represent the resources to fetch and whose values areResource Configuration Objects that we'll discuss later (and is highlighted below).

  4. Resource Configuration Object. In the object returned by our executor function, each entry has a key equal to one of theResourceKeys and whose value we will refer to in this document as a Resource Configuration Object, or Resource Config for short. It holds the declarative instructions thatuseResources andwithResources will use to request the resource.

Tutorial

Okay, back to the initial example. Let's take a look at ouruseResources usage in the component:

// `@withResources((props) => ({todos: {}}))`constgetResources=(props)=>({todos:{}});exportdefaultfunctionMyComponent(props){constresources=useResources(getResources,props);// ...}

You see thatuseResources takes an executor function that returns an object. The executor functiontakes a single argument: the current props, which are component props when you usewithResources, but can be anything when you useuseResources. The executor function returns an object whose keys areResourceKeys and whose values are Resource Config objects. Where doResourceKeys come from? From the object passed to theregister method in the config file we added earlier!

// js/core/resourcerer-config.jsimport{register}from'resourcerer';importTodosCollectionfrom'js/models/todos-collection';// after adding this key, `todos` can be used in our executor functions to reference the Todos resource.// The 'todos' string value will also be the default prefix for all todos-related return values.// That's why we have `props.todosCollection`!register({todos:TodosCollection});

(We can also pass custom prefixes for our prop names in a component, butwe'll get to that later.)

Back to the executor function. In the example above, you see it returns an object of{todos: {}}. In general, the object it should return is of type{[key: ResourceKeys]: ResourceConfigObject}, whereResourceConfigObject is a generic map of config options. It can contain as many keys as resources you would like the component to request. In our initial example, the Resource Config Object was empty. Further down, we'll go over the plethora of options and how to use them. For now, let's take a look at some of the resource-related props this simple configuration provides our component.

Other Props Returned from the Hook/Passed from the HOC (Loading States)

Of course, in our initial example, thetodosCollection won’t be populated with data immediately since, after all, the resource has to be fetched from the API. Some of the mostsignificant and most common React UI states we utilize are whether a component’s critical resources have loaded entirely, whether any are still loading, or whether any have errored out. This is how we can appropriately cover our bases—i.e., we can ensure the component shows a loader while the resource is still in route, or if something goes wrong, we can ensure the component will still fail gracefully and not break the layout. To address these concerns, theuseResources hook/withResources HOC gives you several loading state helper props. From our last example:

  • todosLoadingState (can be equal to any of theLoadingStates constants. There will be one for each resource, and the property names will be equal to${resourceKey}LoadingState)
  • hasLoaded {boolean} - all critical resources have successfully completed and are ready to be used by the component
  • isLoading {boolean} - any of the critical resources are still in the process of being fetched
  • hasErrored {boolean} - any of the critical resource requests did not complete successfully

isLoading ,hasLoaded , andhasErrored are not based on individual loading states, but are rather a collective loading state for the aforementioned-critical component resources. In the previous example, the todos resource is the only critical resource, soisLoading /hasLoaded /hasErrored are solely based ontodosLoadingState. But we can also add a non-criticalusers resource, responsible, say, for only display users' names alongside their TODOs—a small piece of the overall component and not worth delaying render over. Here’s how we do that:

constgetResources=(props)=>({todos:{},users:{noncritical:true}});functionMyClassWithTodosAndAUsers(props){constresources=useResources(getResources,props);}

MyClassWithTodosAndAUsers will now receive the following loading-related props, assuming we've registered ausersCollection in our config file:

  • todosLoadingState
  • usersLoadingState
  • isLoading
  • hasLoaded
  • hasErrored

In this case,isLoading , et al, are only representative oftodosLoadingState and completely irrespective ofusersLoadingState . This allow us an incredible amount of flexibility for rendering a component as quickly as possible.

Here’s how might use that to our advantage inMyClassWithTodosAndAUsers :

import{Utils}from'resourcerer';functionMyClassWithTodosAndAUsers(props){const{    isLoading,    hasErrored,    hasLoaded,    todosCollection,    usersCollection,    usersLoadingState}=useResources(getResources,props);vargetUserName=(userId)=>{// usersCollection guaranteed to have returned herevaruser=usersCollection.find(({id})=>id===userId);return(<spanclassName='user-name'>{user&&user.id||'N/A'}</span>);};return(<divclassName='MyClassWithTodosAndUsers'>{isLoading ?<Loader/> :null}{hasLoaded ?(// at this point we are guaranteed all critical resources have returned.// before that, todosCollection is still a Collection instance that can be// mapped over--it's just empty<ul>{todosCollection.map((todoModel)=>(<likey={todoModel.id}>              // pure function that accepts loading states as arguments{Utils.hasLoaded(usersLoadingState) ?getUserName(todoModel.get('userId')) :// if you're anti-loader, you could opt to render nothing and have the// user name simply appear in place after loading<Loadertype="inline"/>}{todoModel.get('name')}</li>)}</ul>) :null}{hasErrored ?<ErrorMessage/> :null}</div>);

Here's a real-life example from theSift Console, where we load a customer's workflows without waiting for the workflow stats resource, which takes much longer. Instead, we gracefully show small loaders where the stats will eventually display, all-the-while keeping our console interactive:

Noncritical Resource Loading

And here's what it looks like when the stats endpoint returns:

Noncritical Resource Returned

There’s one other loading prop offered from the hook/HOC:hasInitiallyLoaded. This can be useful for showing a different UI for components that have already fetched the resource. An example might be a component with filters: when a filter is changed after the initial resource is loaded (thus re-fetching the resource), we may want to show a loader with an overlay over the previous version of the component. See theAdvanced Topics docs for more.

Requesting Prop-driven Data

Let's say we wanted to request not the entire users collection, but just a specific user. Here's our config:

// js/core/resourcerer-config.jsimport{register}from'resourcerer';importTodosCollectionfrom'js/models/todos-collection';importUserModelfrom'js/models/user-model';register({todos:TodosCollection,user:UserModel});

And here's what our model might look like (NOTE: use a Model when you've got one resource instance, and a Collection when you've got a list of that resource):

// js/models/user-model.jsimport{Model}from'resourcerer';exportdefaultclassUserModelextendsModel{url({userId}){// `userId` is passed in here because we have// `path: {userId: props.id}` in the resource config objectreturn`/users/${userId}`;}staticdependencies=['userId'];}

Thedependencies static property is important here, as we'll see in a second; it is a list of properties thatresourcerer will use to generate a cache key for the model. It will look for theuserId property in the following places, in order:

  1. thepath object
  2. thedata object
  3. theparams object

All three of these come from via theResource Configuration Object that is returned from our executor function; it might look like this:

constgetResources=(props)=>({user:{path:{userId:props.id}}})// hookfunctionMyComponent(props){constresources=useResources(getResources,props);// ...}// HOC@withResources(getResources)classMyComponentWithAUserextendsReact.Component{}

Assuming we have aprops.id equal to'noahgrant', this setup will putMyComponentWithAUser in a loading state until/users/noahgrant has returned.

...and here's the best part:

Let's say thatprops.id changes to a different user.MyComponentWithAUser will get putback into a loading state while the new endpoint is fetched,without us having to do anything! This works because our model has dictated that its models should be cached by auserId field, which is passed to it in thepath property.

Changing Props

In general, there are two ways to changeprops.id as in the previous example:

  1. Change the url, which is the top-most state-carrying entity of any application. The url can be changed either by path parameter or query paramter, i.e.example.com/users/noahgrant ->example.com/users/bobdonut, orexample.com/users?id=noahgrant ->example.com/users?id=bobdonut. In this case, each prop change isindexable, which is sometimes desirable, sometimes not.

  2. Change internal application state. For these cases,useResources/withResources make available another handy prop:setResourceState.setResourceState is a function that has the same method signature as theuseState we all know and love. It sets internal hook/HOC state, which is then returned/passed down, respectively, overriding any initial prop, iesetResourceState((state) => ({...state, id: 'bobdonut'})). This isnot indexable.

    Note thatsetResourceState is very useful for thewithResources HOC because it allows you to 'lift' state above the fetching component that otherwise would not be possible. ForuseResources, it is a nice-to-have in some cases, but because you can always define your ownuseState above theuseResources invocation, you may find that you use it less often.

Common Resource Config Options

params

Theparams option is passed directly to thesync method and sent either as stringified query params (GET requests) or as a body (POST/PUT). Its properties are also referenced when generating a cache key if they are listed in a model's staticdependencies property (See thecache key section for more). Let's imagine that we have a lot of users and a lot of todos per user. So many that we only want to fetch the todos over a time range selected from a dropdown, sorted by a field also selected by a dropdown. These are query parameters we'd want to pass in ourparams property:

  @withResources((props)=>{constnow=Date.now();return{userTodos:{params:{limit:20,end_time:now,start_time:now-props.timeRange,sort_field:props.sortField}}};})classUserTodosextendsReact.Component{}

Now, as the prop fields change, the params sent with the request changes as well (provided we set ourdependencies property accordingly):

https://example.com/users/noahgrant/todos?limit=20&end_time=1494611831024&start_time=1492019831024&sort_field=importance

path

As referenced previously, all properties on anpath object will be passed into a model's/collection'surl function. This makes it an ideal place to addpath parameters (contrast this with theparams object, which is the place to add query (GET) or body (POST/PUT/PATCH) parameters). It will also be used in cache key generation if it has any fields specified in the model's staticdependencies property (See thecache key section for more). Continuing with our User Todos example, let's add anpath property:

constgetResources=(props)=>{constnow=Date.now();return{userTodos:{params:{limit:20,end_time:now,start_time:now-props.timeRange,sort_field:props.sortField},path:{userId:props.userId}}};};

Here, this UserTodosCollection instance will get auserId property passed to itsurl function. We'll also want to add the'userId' string to the collection'sstaticdependencies array, because each cached collection should be specific to the user:

// js/models/user_todos_collection.jsexportclassUserTodosCollectionextendsCollection{url({userId}){// userId gets passed in for us to help construct our urlreturn`/users/${userId}/todos`;}staticdependencies=['userId'];};

noncritical

As alluded to in theOther Props section, not all resources used by the component are needed for rendering. By adding anoncritical: true option, we:

  • De-prioritize fetching the resource until after all critical resources have been fetched
  • Remove the resource from consideration within the component-wide loading states (hasLoaded,isLoading,hasErrored), giving us the ability to render without waiting on those resources
  • Can set our own UI logic around displaying noncritical data based on their individual loading states, ieusersLoadingState, which can be passed to the pure helper methods,Utils.hasLoaded,Utils.hasErrored, andUtils.isLoading fromresourcerer.

force

Sometimes you want the latest of a resource, bypassing whatever model has already been cached in your application. To accomplish this, simply pass aforce: true in a resource's config. The force-fetched response will replace any prior model in the cache.

constgetResources=(props)=>({latestStats:{force:true}});functionMyComponentWithLatestStats(props){const{latestStatsModel}=useResources(getResources,props);}

The resource will only get force-requested when the component mounts; theforce flag will get ignored on subsequent updates. If you need to refetch after mounting to get the latest resource, userefetch.

This behavior is similar to the behavior you get withcache invalidation.

Custom Resource Names

Passing aresourceKey: <ResourceKeys> option allows you to pass a custom name as thewithResources key, which will become the base name for component-related props passed down to the component. For example, this configuration:

constgetResources=(props)=>({myRadTodos:{resourceKey:todos});exportdefaultfunctionMyComponentWithTodos{const{    myRadTodosCollection,    myRadTodosLoadingState,    myRadTodosStatus,    ...rest}=useResources(getResources,props);}

would still fetch the todos resource, but the properties returned/props passed to theMyComponentWithTodos instance will bemyRadTodosCollection,myRadTodosLoadingState, andmyRadTodosStatus, etc, as shown. This also allows us to fetch the same resource type multiple times for a single component.

NOTE: when using resourcerer withTypescript (recommended), it will complain about custom resource names, but itwill work. For now, you'll need to// @ts-ignore, but please submit a PR :).

prefetches

This option is an array of props objects that represent what isdifferent from the props in the original resource. For each array entry, a new resource configuration object will be calculated by merging the current props with the new props, and the resulting request is made. In contrast to the original resource, however,no props representing the prefetched requests are returned or passed down to any children (ie, there are no loading state props, no model props, etc). They are simply returned and kept in memory so that whenever they are requested, they are already available.

A great example of this is for pagination. Let's take our previous example and add afrom property to go with ourlimit that is based on the value of apage prop (tracked either by url parameter or bysetResourceState). We want to request the first page but also prefetch the following page because we think the user is likely to click on it:

constgetResources=(props)=>{constnow=Date.now();constREQUESTS_PER_PAGE=20;return{userTodos:{params:{from:props.page*REQUESTS_PER_PAGE,limit:REQUESTS_PER_PAGE,end_time:now,start_time:now-props.timeRange,sort_field:props.sortField},path:{userId:props.userId},// this entry is how we expect the props to change. in this case, we want props.page to be// incremented. the resulting prefetched request will have a `from` value of 20, whereas the// original request will have a `from` value of 0. The `userTodosCollection` returned (hook) or// passed down as props (HOC) will be the latter.prefetches:[{page:props.page+1}]}};};

When the user clicks on a 'next' arrow that updates page state, the collection will already be in the cache, and it will get passed as the newuserTodosCollection. Accordingly, the third page will then get prefetched (props.page equal to 2 andfrom equal to 40). Don't forget to addfrom to thedependencies list!

If you're looking to optimistically prefetch resources when a user hovers, say, over a link, see thePrefetch on Hover section.

data

Pass in a data hash to initialize a Model instance with data before initially fetching. This is passed directly to themodelconstructor method, and is typically much less useful than providing the properties directly to theparams property. One place it might be useful is to seed a model with an id you already have:

getResources=()=>({customer:{data:{id:props.customerId}}});functionMyCustomerComponent(props){const{customerModel}=useResources(getResources,props);// now you can reference the id directly on the modelconsole.log(customerModel.get('id'));// or the id shorthand, customerModel.id}

You can also usedata to take advantage ofre-caching.

Likeparams andpath, thedata object will also be used in cache key generation if it has any fields specified in the model's staticdependencies property (See thecache key section for more).

lazy

Lazy fetching is one of resourcerer's most powerful features, allowing you to get a reference to a model without actually fetching it. If the model is ultimately fetched elsewhere on the page, the component that lazily fetched it will still listen for updates.

A great example of when this would be useful is for search results. Search results are read-only, but if you modify the entity of a result somewhere else in the page, you'd like to see it reflected in your search results. Yet you don't want to fetch the entity details for every search result and spam your API. Enter lazy loading:

// todo_search.jsxgetResources=()=>({todosSearch:{params:someSearchParams}});functionTodoSearch(props){const{todoSearchModel}=useResources(getResources,props);return(<Table><thead><TableHeader>Name</TableHeader><TableHeader>Last Updated</TableHeader></thead><tbody>{todoSearchModel.get('results').map((todo)=><TodoSearchItem{...todo}/>)}</tbody></Table>);}// todo_search_item.jsx// the todoModel is never actually fetched here, it's only listened on, allowing any changes made elsewhere in the page to be reflected here.getResources=()=>({todo:{id:props.id,lazy:true}});functionTodoSearchItem(props){const{todoModel}=useResources(getResources,props);return(<tr><td>{todoModel.get('name')||props.name}</td><td>{todoModel.get('updated_at')||props.updated_at}</td></tr>);

If the todo model has been fetched already, we'll read straight from that. And if it gets updated, this component, via resourcerer, is listening for updates and will re-render to keep our entire UI in sync. Otherwise, we'll just fall back to our read-only search results. WIN!

minDuration

Sometimes requests can betoo fast for certain UIs. In these cases, spinners and other loading states can appear more like a jarring flicker than a helpful status indicator. For these, you can pass aminDuration equal to the minimum number of milliseconds that a request should take. This is great forsave and destroy requests. It will work for fetch requests viauseResources, as well, but beware: if multiple components use the same resource and there are different (or missing) values forminDuration, this will cause a race condition.

dependsOn

See the section onserial requests.

provides

See the section onserial requests.

Data Mutations

So far we've only discussed fetching data. Butresourcerer also makes it very easy to make write requests via theModel andCollection instances that are returned. These classes are enriched data structures that hold our API server data as well as several utilities that help manage the server data in our application. There are three main write operations via these classes:

  1. Model#save

    Use this to create a new resource object (POST) or update an existing one (PUT). Uses the return value of theisNew() method to determine which method to use. If updating, pass a{patch: true} option to use PATCH instead of PUT, which will also send over only the changed attributes instead of the entire resource.

    functionMyComponent(props){const{myModel}=useResources(getResources,props),onSave=()=>myModel.save({foo:'bar'}).then([model])=>// ...).catch(()=>alert('request failed'));return<buttononClick={onSave}>Persist model</button>;}
  2. Model#destroy

    Use this to make a DELETE request at a url with this model's id. Will also remove the model from any collection it is a part of.

    myModel.destroy().catch(()=>alert('Model could not be destroyed));
  3. Collection#create

    If working with a collection instead of a model,.create() adds a new model to the collection and then persists it to the server (viamodel.save()). This is pretty convenient:

    functionTodoDetails(props){const{hasLoaded, todosCollection}=useResources(getResources,props),todoModel=todosCollection.get(props.id),onSaveTodo={// set some loading state...if(!props.id){// create new todo!returntodosCollection.create(attrs).then(([model])=>// ...).catch(()=>alert('create failed'));.then(()=>// remove loading state);}// update existingtodoModel.save(attrs).then(([model])=> ...).catch(()=>alert('update failed'));};if(hasLoaded&&props.id&&!todoModel){return<p>Todonotfound.</p>;}return(// ...<buttononClick={onSaveTodo}>Save</button>);}

Each one of these methods exhibit the following behaviors:

  1. They automatically fire off the appropriate request with the right data and at the right url
  2. They will cause every component registered to that resource to re-render with the updated data, keeping the application in sync
  3. On error, they undo the changes that were done (and their registered components render again).

Note:

  1. All calls resolve an array, which is a tuple of[model, response]. All reject with just the response.
  2. All write calls must have a.catch attached, even if the rejection is swallowed. Omitting one risks an uncaught Promise rejection exception if the request fails.

Serial Requests

In most situations, all resource requests should be parallelized; but that’s not always possible. Every so often, there may be a situation where one request depends on the result of another. For these cases, we have thedependsOn resource config option and theprovides resource config option. These are probably best explained by example, so here is a simplified instance from theSift Console, where we load a queue item that has info about a user, but we can't get further user information until we know what user id belongs to this queue item.

@withResources((props)=>({user:{path:{userId:props.userId},dependsOn:!!props.userId},queueItem:{data:{id:props.itemId},provides:(queueItemModel)=>({userId:queueItemModel.get('userId')})}}))exportdefaultclassQueueItemPageextendsReact.Component{}Inthissimplifiedexample,only`props.itemId`isinitiallypresentattheurl`items/<itemId>`,andsincetheUserModeldependson`props.userId`beingpresent,thatmodelwon’tinitiallygetfetched.OnlytheQueueItemModelgetsfetchedatfirst;ithasthe`provides`option,whichisfunctionthattakesaninstanceofthereturnedmodelorcollectionandreturnsamapof`{[key: string]: any}`.Eachkeyisanewpropname,andeachvaluethenewprop's value.So,inthiscase,thereturned`queueItemModel`instanceispassedasanargument,andwereturnastringthatwillbeassignedto`props.userId`(or,moreaccurately,willbesetasstatevia`setResourceState`asdescribedintheprevioussection).Attthispoint,`props.userId`exists,andtheUserModelwillbefetched.Andwehaveseriallyrequestedourresources!Onethingtonotehereisthatwhilethe`queueItem`resourceisbeingfetched,theuserresourceisina`PENDING`state,whichisaspecialstatethatdoesnotcontributetooverallcomponent`isLoading`/`hasErrored`states(thoughitwillkeepthecomponentfrombeing`hasLoaded`).Atthispoint,the`QueueItemPage`intheexampleaboveisina`LOADING`state(`isLoading === true`)because`QUEUE_ITEM`isloading.Whenitreturnswiththeuserid,the`user`resourceisputintoa`LOADING`state,andthecomponentthenremains`isLoading === true`untilitreturns,afterwhichthecomponenthassuccessfullyloaded.Ifthe`queueItem`resourcehappenedtoerrorforsomereason,the`user`resourcewouldnevergetoutofits`PENDING`state,andthecomponentwouldthentakeonthe`ERROR`state(`hasErrored === true`)of`queueItem`.Formoreon`PENDING`,see[ThoughtsonthePENDINGState](/docs/advanced_topics.md#thoughts-on-the-pending-resource)inthe[AdvancedTopicsdocument](/docs/advanced_topics.md).Finally,notethatthe`provides`functioncanreturnanynumberoffieldswewanttosetasnewpropsforotherresources:```jsconst getResources = (props) => ({  user: {    options: {state: props.activeState, userId: props.userId},    // userModel depends on multiple props from queueItemModel    dependsOn: !!props.userId && props.activeState === "active"  },  queueItem: {    data: {id: props.itemId},    provides: (queueItemModel) => ({userId: queueItemModel.get('userId'), activeState: queueItemModel.get('state')})  }});export default function QueueItemPage(props) {  // activeState and userId are internal state within `useResources`andreturnedconst{    activeState,    userId,    userModel,    queueItemModel}=useResources(getResources,props);}

Differences between useResources and withResources

The hook and HOC largely operate interchangeably, but do note a couple critical differences:

  1. ThewithResources HOC conveniently contains anErrorBoundary with every instance, but such functionalitydoes not yet exist in hooks. This is a definite advantage for the HOC right now, since, if we're already settinghasErrored clauses in our components to prepare for request errors, we can naturally gracefully degrade when an unexpected exception occurs. You'll need to manage this yourself with hooks until the equivalent functionality is released.

  2. The executor function for a hook can be inlined in your component, which puts component props in its closure scope. So be extra careful to avoid this anti-pattern:

    functionMyComponent({start_time, ...props}){const{todosCollection}=useResources((_props)=>({todos:{params:{start_time}}}),props);// ...

    The subtle problem with the above is that thestart_time executor function parameter is relying on a value in the function component closure instead of the_props parameter object; props passed to the executor function can be current or previous but are not the same as what is in the closure, which will always be current. This will lead to confusing bugs, so instead either read directly from the props parameter passed to the executor function:

    functionMyComponent(props){const{todosCollection}=useResources(({start_time})=>({todos:{params:{start_time}}}),props);// ...

    or, even clearer, define your executor function outside of the component scope, as we've done throughout this tutorial (now you know why!):

    constgetResources=({start_time})=>({todos:{params:{start_time}}});functionMyComponent(props){const{todosCollection}=useResources(getResources,props);// ...

Caching Resources with ModelCache

resourcerer handles resource storage and caching, so that when multiple components request the same resource with the same parameters or the same body, they receive the same model in response. If multiple components request a resource still in-flight, only a single request is made, and each component awaits the return of the same resource. Fetched resources are stored in theModelCache. Under most circumstances, you won’t need to interact with directly; but it’s still worth knowing a little bit about what it does.

TheModelCache is a simple module that contains a couple of Maps—one that is the actual cache{[cacheKey: string]: Model | Collection}, and one that is a component manifest, keeping track of all component instances that are using a given resource (unique by cache key). When a component unmounts,resourcerer will unregister the component instance from the component manifest; if a resource no longer has any component instances attached, it gets scheduled for cache removal. The timeout period for cache removal is two minutes by default (but can be changed, seeConfiguring resourcerer, oroverridden on a model-class basis), to allow navigating back and forth between pages without requiring a refetch of all resources. After the timeout, if no other new component instances have requested the resource, it’s removed from theModelCache. Any further requests for that resource then go back through the network.

Again, it’s unlikely that you’ll useModelCache directly while usingresourcerer, but it’s helpful to know a bit about what’s going on behind-the-scenes.

Declarative Cache Keys

As alluded to previously,resourcerer relies on the model classes themselves to tell it how it should be cached. This is accomplished via a staticdependencies array, where each entry can be either:

  1. A string, where each string is the name of a property that the model receives whose value should take part in the cache key. The model can receive this property either from thepath hash, thedata hash, or theparams hash, in that order.

  2. A function, whose return value is an object of keys and values that should both contribute to the cache key.

Let's take a look at theuserTodos resource from above, where we want to request some top number of todos for a user sorted by some value over some time range. The resource declaration might look like this:

constgetResources=(props)=>{constnow=Date.now();return{userTodos:{params:{limit:props.limit,end_time:now,start_time:now-props.timeRange,sort_field:props.sortField},path:{userId:props.userId}}};};

And our corresponding model definition might look like this:

exportclassUserTodosCollectionextendsCollection{url({userId}){return`/users/${userId}/todos`;}// ...staticdependencies=['limit','userId','sort_field',({end_millis, start_millis})=>({range:end_millis-start_millis})];};

We can see thatlimit andsort_field as specified independencies are taken straight from theparams object thatresourcerer transforms into url query parameters.userId is part of the/users/{userId}/todos path, so it can't be part of theparams object, which is why it gets passed in thepath object instead.

The time range is a little tougher to cache, though. We're less interested the spcecificend_time/start_time values to the millisecond—it does us little good to cache an endpoint tied toDate.now() when it will never be the same for the next request. We're much more interested in the difference betweenend_time andstart_time. This is a great use-case for a function entry independencies, which takes theparams object passed an argument. In the case above, the returned object will contribute a key calledrange and a value equal to the time range to the cache key.

The generated cache key would be something likeuserTodos~limit=50_$range=86400000_sort_field=importance_userId=noah. Again, note that:

  • theuserId value is taken from thepath hash
  • thelimit andsort_field values are taken from theparams hash
  • therange value is taken from a function that takesstart_millis/end_millis from theparams hash into account.

Prefetch on Hover

You can useresourcerer's executor function to optimistically prefetch resources when a user hovers over an element. For example, if a user hovers over a link to their TODOS page, you may want to get a head start on fetching theirtodos resource so that perceived loading time goes down or gets eliminated entirely. We can do this with the top-levelprefetch function:

import{prefetch}from'resourcerer';// here's our executor function just as we pass to useResources or withResourcesconstgetTodos=(props)=>{constnow=Date.now();return{userTodos:{params:{limit:props.limit,end_time:now,start_time:now-props.timeRange,sort_field:props.sortField},path:{userId:props.userId}}};};// in your component, call the prefetch method with the executor and an object that matches// what you expect the props to look like when the resources are requested without prefetch.// attach the result to an `onMouseEnter` prop<ahref='/todos'onMouseEnter={prefetch(getTodos,expectedProps)}>TODOS</a>

Note, as mentioned in the comment above, thatexpectedProps should take the form of props expected when the resource is actually needed. For example, maybe we're viewing a list of users, and so there is noprops.userId in the component that usesprefetch. But for the user in the list with id'noahgrant', we would pass it anexpectedProps that includes{userId: 'noahgrant'} because we know that when we click on the link and navigate to that url,props.userId should be equal to'noahgrant'.

Refetching

resourcerer also returns arefetch function that you can use to re-request a resourcethat has already been requested on-demand. A couple examples of where this could come in handy:

  1. A request timed out and you want to give the user the option of retrying.
  2. You have made a change to one resource that may render an auxiliary resource stale, and you want to bring the auxiliary resource up-to-date.

The function takes a single or a list ofResourceKeys. Each entry will get refetched.

functionMyComponent(props){const{todosCollection, refetch}=useResources(({start_time})=>({todos:{params:{start_time}}}),props);// ...return<ButtononClick={()=>refetch("todos")}>Refetch me</Button>;

NOTE:

  • The list returned by the function should only include keys that are currently returned by the executor function. In the example above, returninguserTodos would not fetch anything because it is not part of the current executor function. To conditionally fetch another resource, add it to the executor function withdependsOn.
  • The resource that will be refetched is the version returned by the executor function with the current props. To fetch a different version, use the standard props flow instead of refetching.

Cache Invalidation

In some cases you may want to imperatively remove a resource from the cache. For example, you may make a change to a related resource that renders a resource invalid. For those cases,useResources returns aninvalidate function that takes a single or a list ofResourceKeys:

functionMyComponent(props){const{todosCollection, invalidate}=useResources(({start_time})=>({todos:{params:{start_time}}}),props);// ...return<ButtononClick={()=>invalidate(["todos"])}>Invalidate me</Button>;
  • Unlikerefetching, the ResourceKeys passed toinvalidate do not need to be from those returned by the executor function. They can any resource key.

Tracking Request Times

If you have a metrics aggregator and want to track API request times, you can do this by setting ameasure static property on your model or collection.measure can either be a boolean or a function that returns a boolean. The function takes the resource config object as a parameter:

import{Model}from'resourcerer';classMyMeasuredModelextendsModel{// either a boolean, which will track every request of this model instancestaticmeasure=true;// or a function that returns a boolean, which will track instance requests based on a conditionstaticmeasure=({data={}})=>data.id==='noahgrant';}

When the staticmeasure property is/returns true,resourcerer will record the time it takes for that resource to return and pass the data to thetrack configuration method that you can set up, sending it to your own app data aggregator. This allows you to see the effects of your endpoints from a user’s perspective.

Configuringresourcerer

The same config file used toregister your models also allows you to set custom configuration properties for your own application:

import{ResourcesConfig}from'resourcerer';ResourcesConfig.set(configObj);

ResourcesConfig.set accepts an object with any of the following properties:

  • cacheGracePeriod (number in ms): the length of time a resource will be kept in the cache after being scheduled for removal (see thecaching section for more).Default: 120000 (2 minutes). Note that each model class can provide its own timeout override.

  • errorBoundaryChild (JSX/React.Element): the element or component that should be rendered in the ErrorBoundary included in everywithResources wrapping. By default, a caught error renders this child:

    <divclassName='caught-error'><p>An error occurred.</p></div>
  • log (function): method invoked when an error is caught by the ErrorBoundary. Takes the caught error as an argument. Use this hook to send caught errors to your error monitoring system.Default: noop.

  • prefilter (function): this function takes in the options object passed to the request and should return any new options you want to add. this is a great place to add custom request headers (like auth headers) or do custom error response handling. For example:

    prefilter:(options)=>({error:(response)=>{if(response.status===401){// refresh auth token logic}elseif(response.status===429){// do some rate-limiting retry logic}// catch callbacks still get called after this, so always default to rejectingreturnPromise.reject(response);},headers:{    ...options.headers,Authorization:`Bearer${localStorage.getItem('super-secret-auth-token')}`}})

    Default: the identity function.

  • stringify (function): Use this to pass in a custom or more powerful way to stringify your GET parameters. The default is to useURLSearchParams, but that won't url-encode nested objects or arrays. Override this method if you need support for that, ie:

    import{stringify}from'qs';ResourcesConfig.set({stringify(params,options){// params is the params object to be stringified into query parameters// options is the request options objectreturnstringify(params);}});
  • track (function): method invoked whenameasure property is added to a Model or Collection. Use this hook to send the measured data to your application analytics tracker.Default: noop. The method is invoked with two arguments:

    • the event string,'API Fetch'
    • event data object with the following properties:
      • Resource (string): the name of the resource (taken from the entry inResourceKeys)
      • params (object): params object supplied via the resource's config
      • options (object): options object supplied via the resource's config
      • duration (number): time in milliseconds between request and response

FAQs

  • Why?

    Yeah...isn'tGraphQL the thing to use now? Why bother with a library for REST APIs?

    GraphQL is awesome, but there are many reasons why you might not want to use it. Maybe you don't have the resources to ensure that all of your data can be accessed performantly; in that case, your single/graphql endpoint will only ever be as fast as your slowest data source. Maybe your existing REST API is working well and your eng org isn't going to prioritize any time to move away from it. Etc, etc, etc.resourcerer offers a way for your front-end team to quickly get up and running with declarative data fetching, request flows, and model caching.

  • How is this different from React Query?

    React Query is an awesome popular library that shares some of the same features as resourcerer. But becauseresourcerer isexplicitly for REST APIs and React Query is backend agnostic, we get to abstract out even more. For example, in React Query, you'll need to imperatively fetch your resource in each component:

    // React Query, assuming a made-up category dependency// component 1functionMyComponent({category, ...props}){// define your fetch key and imperatively fetch your resourceconst{data}=useQuery(['somekey',{category}],()=>{returnfetch('/todos',(res)=>res.json())}}// component 2functionMySecondComponent({category, ...props}){// same thing. you'll probably want to abstract these out so that changing it one place changes it everywhereconst{data}=useQuery(['somekey',{category}],()=>{returnfetch('/todos',(res)=>res.json())}}

    With resourcerer, this abstraction is done once in a model--both defining its url as well as how its properties should affect its cache key:

    // resourcerer, with the same category dependency. dependencies are resource-specific,// not component-specific, so they should be defined on the model instead of the component// todos-collection.jsexportdefaultclassTodosCollectionextendsCollection{staticdependencies=['category'];url(){return'/todos';}}// component1functionMyComponent({category, ...props}){const{todosCollection}=useResources(()=>({todos:{path:{category}}));}// component2--identical to the firstfunctionMyComponent({category, ...props}){const{todosCollection}=useResources(()=>({todos:{path:{category}}));}

    The other big difference you might note is the data object in the hook's response. With React Query, you get exactly the JSON returned by the server. With resourcerer, you getModel orCollection instances, which are enriched data representations from which you can also perform write operations that will propagate throughout all other subscribed components—regardless of their location in your application. Need to update a model? Callmodel.set()—any other component that uses that model (or its collection) will automatically update. Need to persist to the server? Callmodel.save() orcollection.add(). Need to remove the model?model.destroy(). Ez-pz.

    Also note that thetodosCollection in both components 1 and 2 in the last example are the same objects.

  • Doesresourcerer support SSR?

    There is no official documentation for its use in server-side rendering at this point. However, because passing models as props directly to a componentbypasses fetching, it is likely thatresourcerer can work nicely with an SSR setup that:

    1. passes instantiated models directly through the app before callingrenderToString
    2. provides those models within a top-level<script> element that adds them directly to theModelCache.
  • Canresourcerer do anything other thanGET requests?

    resourcerer only handles resourcefetching (i.e. callingModel.prototype.fetch). Note that this is not the same as only makingGET requests; pass in amethod: 'POST' property in a resource's config to turn theparams property into a POST body, for example, when making a search request.

    For write operations, use Models'save anddestroy methods directly:

    onClickSaveButton(){this.setState({isSaving:true});// any other mounted component in the application that uses this resource// will get re-rendered with the updated name as soon as this is calledthis.props.userTodoModel.save({name:'Giving This Todo A New Name'}).then(()=>notify('Todo save succeeded!')).catch(()=>notify('Todo save failed :/')).then(()=>this.setState({isSaving:false}));}
  • What about other data sources like websockets?

    resourcerer supports request/response-style semantics only. A similar package for declaratively linking message-pushing to React updates would be awesome—but it is not, at this point, part of this package.

  • How can we test components that useresourcerer?

    See thedoc on testing components for more on that.

  • How big is theresourcerer package?

    Under 10kB gzipped. It has no dependencies.

  • Semver?

    Yes. Releases will adhere tosemver rules.

Migrating to v2.0

See theMigrating to v2.0 doc.


[8]ページ先頭

©2009-2025 Movatter.jp