Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Resource Components for Create, Update, Delete #32

Open
orther opened this issue May 20, 2018 · 5 comments
Open

Add Resource Components for Create, Update, Delete #32

orther opened this issue May 20, 2018 · 5 comments
Assignees
Labels
enhancement New feature or request uptrend project need Feature or fix needed for Uptrend project dependent on URM
Projects
Milestone

Comments

@orther
Copy link
Member

orther commented May 20, 2018

Overview

We've found it quite convenient to use ResourceLoader components to setup data loading in our containers. It has become clear that providing the other CRUD operations as child render components would be useful. So I'd like to add the following components:

New Components

NOTE: The following examples are not actually thought through so the items passed to the child render prop will likely be much different.

// --
// -- CreateResource
// --
<CreateResource resource="user" entityType="user" needle={123}>
{({create, onEventCreate, status}) => /* ... */}
</CreateResource> 

// --
// -- UpdateResource
// --
<UpdateResource resource="user" entityType="user" needle={123}>
{({update, onEventUpdate, status}) => /* ... */}
</UpdateResource> 

// --
// -- DeleteResource
// --
<DeleteResource resource="user" entityType="user" needle={123}>
{({delete, onEventDelete, status}) => /* ... */}
</DeleteResource> 

Other Possible Solutions

One thing that we could possibly do to provide this functionality is to provide functions for triggering Create, Update, Delete requests of the resource loaded by the ResourceLoader component. I highly doubt that is a good solution and even if it was it could make sense to build it from these new CrUD components.

@orther orther self-assigned this May 20, 2018
@orther orther added the enhancement New feature or request label May 20, 2018
@orther orther added the uptrend project need Feature or fix needed for Uptrend project dependent on URM label May 20, 2018
@orther orther added this to To do in Work Log via automation May 20, 2018
@orther orther moved this from To do to In progress in Work Log May 20, 2018
@orther
Copy link
Member Author

orther commented May 22, 2018

🚨Name components with Resource prefixing action! 🚨

Rather than <CreateResource ... > it should be <ResourceCreate .../> etc.

Here are updated examples:

// --
// -- ResourceCreate
// --
<ResourceCreate resource="user" entityType="user">
{({create, onEventCreate, status}) => /* ... */}
</ResourceCreate> 

// --
// -- ResourceUpdate
// --
<ResourceUpdate resource="user" entityType="user" needle={123}>
{({update, onEventUpdate, status}) => /* ... */}
</ResourceUpdate> 

// --
// -- ResourceDelete
// --
<ResourceDelete resource="user" entityType="user" needle={123}>
{({delete, onEventDelete, status}) => /* ... */}
</ResourceDelete> 

@orther
Copy link
Member Author

orther commented Jul 1, 2018

Have mostly implemented this but I'd like to get a feature complete for all request methods and options like params/data, headers, etc. Currently we have loadResource and updateResource as well as the request action only (no status/state updates) forms loadResourceRequest and updateResourceRequest. It would be nice to standardized everything.

@orther
Copy link
Member Author

orther commented Jul 10, 2018

I came across react-request and really appreciate a lot about it. The de-duping and caching functionality are exactly what I was hoping to solve through my own investigations for ResourceLoader in the future. In general I think moving the ResourceLoader to be a more standard API & HTTP request interface is a good thing and this library is a great reference.

Besides the functionality I was really struck by the API design (both prop API and the render child result object API). Specifically for this issue the the doFetch function <Fetch /> passes to the render child is inline with what I think the ResourceLoader should do. Rather than passing in a different function for each request method such as loadResource and createResource, we can instead pass a single function such as doRequest that takes an options object that allows you to make any type of request for the resource. We could make the props set on the ResourceLoader be the default for the passed down doRequest but then allow them all to be overwritten with the option object including resource and entityType. That would allow us to easily use a single ResourceLoader to do all CRUD operations and provide tons of flexibility.

A contrived example using a non-existing <Resource /> component that replaces the current <ResourceLoader /> could look like:

<Resource
  // (url) Notice how you don't have to pass as resourceId
  url="/book/15"
  // (method) Defaults to get - Can be: GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE
  method="get"
  // (entityType) Optional triggers normilization
  entityType="book"
  // (lazy) Doesn't load on mount
  lazy
>
  {({ data: book, loading, error, response, status, doRequest }) => {
    <div>
      {loading && 'Loading...'}
      {error && 'There was an error.'}
      {!fetching &&
        !error &&
        response.status === 200 && (
          <div>
            <button onClick={() => doRequest()}>Load Book</button>;
            <button onClick={() => doRequest({ method: 'delete' })}>Delete Book</button>;
            <button
              onClick={() =>
                doRequest({ method: 'put', data: { title: 'Updated Title' } })
              }
            >
              Update Book
            </button>;
            <button onClick={() => doRequest({ method: 'post', data: 'title' })}>
              Delete Book
            </button>;
            <div>
              <h1>Book: {book.title}</h1>
            </div>
          </div>
        )}
    </div>;
  }}
</Resource>

The API section is here: https://github.com/jamesplease/react-request#api

For reference below is the API section copied from react-request README:

Click to expand

Below from react-request README

API

<Fetch />

A component for making a single HTTP request. It accepts every value of init and input
from the
fetch()
API as a prop, in addition to a few other things.

The props that come from the fetch() method are:

  • url
  • method: defaults to "GET"
  • body
  • credentials
  • headers
  • mode
  • cache
  • redirect
  • referrer: defaults to "about:client"
  • referrerPolicy: defaults to ""
  • integrity: defaults to ""
  • keepalive
  • signal

To learn more about the valid options for these props, refer to the
fetch()
documentation.

The following example demonstrates some of the most commonly-used props that come from fetch():

<Fetch
  url="/posts/2"
  method="patch"
  credentials="same-origin"
  headers={{
    'csrf-token': myCsrfToken
  }}
  body={JSON.stringify({ title: 'New post' })}>
  {({ doFetch }) => {
    <button onClick={() => doFetch()}>Update Post</button>;
  }}
</Fetch>

In addition to the fetch() props, there are a number of other useful props.

children

The render prop of this component.
It is called with one argument, result, an object with the following keys:

  • fetching: A Boolean representing whether or not a request is currently in flight for this component
  • failed: A Boolean representing whether or not the request failed for any reason. This includes network
    errors and status codes that are greater than or equal to400.
  • error: An error object representing a network error occurred. Note that HTTP "error" status codes do not
    cause errors; only failed or aborted network requests do. For more, see the
    "Using Fetch" MDN guide.
  • response: An instance of Response. The
    body will already be read, and made
    available to you as response.data.
  • data: The data returned in response. This will be different from response.data if a
    transformData prop was passed to <Fetch/>.
  • doFetch: A function that makes the HTTP request. See notes below.
  • url: The URL that was passed into <Fetch />.
  • requestName: The name of the request (see requestName below)
  • requestKey: The computed request key

There are three common use cases for the doFetch prop:

  • You can use it to "refresh" the data by making a follow-up request for read requests
  • You can use it to retry the request if there is any sort of error
  • You must manually call this method to actually make the request anytime that the lazy prop
    is passed as true.

doFetch accepts one argument: options. Any of the fetch() options, such as url, method, and
body are valid options. You may also specify a new requestKey if you are manually generating your
own keys. This method allows you to customize the request from within the component based on the
component's state.

lazy

Whether or not the request will be called when the component mounts. The default value
is based on the request method that you use.

Method Default value
GET, HEAD, OPTIONS false
POST, PUT, PATCH, DELETE true
<Fetch url="/books" lazy>
  {({ doFetch }) => {
    <button onClick={() => doFetch()}>Load the books</button>;
  }}
</Fetch>
beforeFetch

A function that is called just before a network request is initiated. It is called
with one argument, an object with the following keys:

  • url: The URL of the request
  • init: The second argument passed to global.fetch(), which specifies things
    such as the body, method, and so on
  • requestKey: Either the computed request key, or the value of the
    requestKey prop

This feature is useful for analytics, or syncing response data with a data store such
as Redux.

Note: This function is not called when the component reads from the cache.

afterFetch

A function that is called anytime that a network response is received. It is called
with one argument, an object with the following keys:

  • url: The URL of the request
  • init: The second argument passed to global.fetch(), which specifies things
    such as the body, method, and so on
  • requestKey: Either the computed request key, or the value of the
    requestKey prop
  • response: The response that was received from the HTTP request
  • data: The transformed data from the response. This will be different from
    response.data if a transformData function was passed as a prop to <Fetch/>.
  • error: An error returned from the HTTP request
  • didUnmount: A Boolean representing whether or not the component has unmounted

This can be used for analytics or syncing response data with a data store such
as Redux.

Note: This function is not called when the component reads from the cache.

onResponse

A function that is called every time a response is received, whether that
response is from the cache or from a network request. Receives two arguments:
error and response.

<Fetch
  url="/posts/2"
  onResponse={(error, response) => {
    if (error) {
      console.log('Ruh roh', error);
    } else {
      console.log('Got a response!', response);
    }
  }}>
  {() => {
    <div>Hello</div>;
  }}
</Fetch>
transformData

A function that is called with the data returned from the response. You can use this
hook to transform the data before it is passed into children.

<Fetch
  url="/posts/2"
  transformData={data => data.post>
  {({ fetching, error, response, data }) => {
    <div>
      {fetching && ('Loading...')}
      {error && ('There was an error.')}
      {!fetching && !error && response.status === 200 && (
        <div>
          <h1>{data.title}</h1>
          <div>{data.content}</div>
        </div>
      )}
    </div>
  }}
</Fetch>

Note: transformData does not modify the value of response.data. The transformed data is
made available to you in the render prop argument under the data key.

responseType

The content type of the response body. Defaults to "json" unless the response has a 204 status code,
in which case it will be "text" instead. Valid values are any of the methods on
Body.

Alternatively, you may specify a function that returns a string. The function will be called with one
argument: response. This allows you to dynamically specify the response type based on information
about the response, such as its status code.

// If you have an endpoint that just returns raw text, you could, for instance, convert it into
// an object using `responseType` and `transformData`.
<Fetch
  url="/countries/2"
  responseType="text"
  transformData={countryName => {
    return {
      countryName
    };
  }}>
  {({ data }) => {
    if (data) {
      return <div>{data.countryName}</div>;
    }

    return null;
  }}
</Fetch>

If the response body cannot be parsed as the responseType that you specify, then data will
be set to null.

requestName

A name to give this request, which can help with debugging purposes. The request name is
analogous to a function name in JavaScript. Although we could use anonymous functions
everywhere, we tend to give them names to help humans read and debug the code.

<Fetch url={`/posts/${postId}`} requestName="readPost" />

Note: This feature is analogous to the operation name in GraphQL.

fetchPolicy

This determines how the request interacts with the cache. Valid options are:

  • "cache-first"
  • "cache-and-network"
  • "network-only"
  • "cache-only"

For documentation on what each of these values do, refer to the response caching guide.

The default value of this prop is based on the value of the method prop that you pass to <Fetch/>.

Method Default value
GET, HEAD, OPTIONS "cache-first"
POST, PUT, PATCH, DELETE "network-only"

This prop behaves identically to the Apollo prop
with the same name.

cacheResponse

Whether or not the response will be cached. The default value is based on the value of the method prop that you pass
to <Fetch/>.

Method Default value
GET, HEAD, OPTIONS true
POST, PUT, PATCH, DELETE false

For documentation on this prop, refer to the response caching guide.

dedupe

A Boolean value representing whether or not the request should be
deduplicated.
Defaults to true.

requestKey

A string that is used to control the request deduplication and response caching features. By default,
a key is generated for you. Specifying a custom key is an advanced feature that you may not need.

For more, see the request key
guide.


The rest of the API documentation describes the other named exports from the react-request package.

fetchDedupe( input [, init] [, dedupeOptions] )

This is the fetchDedupe export from the Fetch Dedupe
library. Fetch Dedupe powers the request deduplication in React Request.

If, for whatever reason, you need to make a standalone HTTP request outside of the
<Fetch /> component, then you can use this with confidence that you won't send a
duplicate request.

For more, refer to the documentation of fetch-dedupe.

getRequestKey({ url, method, body, responseType })

Generates a request key. All of the values are optional.

This method comes from fetch-dedupe.

isRequestInFlight( requestKey )

Return a Boolean representing if a request for requestKey is in flight or not.

This method comes from fetch-dedupe.

clearRequestCache()

Wipes the cache of deduped requests. Mostly useful for testing.

This method comes from fetch-dedupe.

Note: this method is not safe to use in application code.

clearResponseCache()

Wipes the cache of cached responses. Mostly useful for testing.

Note: this method is not safe to use in application code.

Acknowledgements

This library was inspired by Apollo. The
library Holen was referenced during the
creation of this library.

GitHub
react-request - Declarative HTTP requests for React

@orther
Copy link
Member Author

orther commented Jul 11, 2018

After some reflection providing a doRequest function that takes a method option is great but I believe it would still be nice to provide separate functions for the different request method types. The <Resource> component would still pass down a doRequest function but also the following (code not tested only example of idea):

const doGet = options => doFetch({...options, method: 'get'});
const doHead = options => doFetch({...options, method: 'head'});
const doOptions = options => doFetch({...options, method: 'options'});

const doPost = options => doFetch({...options, method: 'post'});
const doPut = options => doFetch({...options, method: 'put'});
const doPatch = options => doFetch({...options, method: 'patch'});
const doDelete = options => doFetch({...options, method: 'delete'});

This would result in an updated contrived example:

<Resource
  // (url) Notice how you don't have to pass as resourceId
  url="/book/15"
  // (urlPost) URL used for POST requests; defaults to `url` prop value
  urlPost="/book"
  // (method) Defaults to get - Can be: GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE
  method="get"
  // (entityType) Optional triggers normilization
  entityType="book"
  // (lazy) Doesn't load on mount
  lazy
>
  {({ data: book, loading, error, response, status, doGet, doPost, doPut, doDelete }) => {
    <div>
      {loading && 'Loading...'}
      {error && 'There was an error.'}
      {!fetching &&
        !error &&
        response.status === 200 && (
          <div>
            <button onClick={() => doGet()}>Load Book</button>;
            <button onClick={() => doDelete()}>Delete Book</button>;
            <button
              onClick={() =>
                doRequest({ method: 'put', data: { title: 'Updated Title' } })
              }
            >
              Update Book
            </button>;
            <button onClick={() => doRequest({ method: 'post', data: 'title' })}>
              Delete Book
            </button>;
            <div>
              <h1>Book: {book.title}</h1>
            </div>
          </div>
        )}
    </div>;
  }}
</Resource>

Note about urlPost prop

The urlPost prop is useful for when you are loading a resource detail (e.g. /book/15) and wanting to use it's passed down doPost function for creating a new book by POST'ing to the resource's index endpoint (e.g. /book).

Also note there is an optional url<Method> prop for all method types:
urlDelete, urlGet, urlHead, urlOptions, urlPatch, urlPost, urlPut.

By providing optional url<Method> props to customize the default url used for the do<Method> functions we can support standard REST practices for CRUD operations without the resourceId prop used in the ResourceLoader. This solution is more flexible, easier to understand and assume things about, while still providing concise API. Do note though the real reason we can get rid of resourceId is because we recognize in practice we don't use resources like we use entities.

@orther
Copy link
Member Author

orther commented Jan 30, 2019

Rumor has it React Hooks will arrive this coming Monday Feb 4th. A custom useResource hook would provide a clean simple API compared to Render Props. Throwing out this example of what the hook could look like based on the designs mapped out in this issue intended for a new render prop component.

  const { data, loading, error, response, status, doGet, doPost, doPut } = useResource({
    // (url) Notice how you don't have to pass as resourceId
    url: "/book/15",
    // (urlPost) URL used for POST requests; defaults to `url` prop value
    urlPost: "/book",
    // (method) Defaults to get - Can be: GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE
    method: "get",
    // (entityType) Optional triggers normilization
    entityType: "book",
    // (lazy) Doesn't load on mount
    lazy
  })

  console.log({book: data.book}) // => will hold the book entity that was loaded

@orther orther pinned this issue Jan 30, 2019
@orther orther added this to the React Hooks milestone Jan 31, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request uptrend project need Feature or fix needed for Uptrend project dependent on URM
Projects
Work Log
  
In progress
Development

No branches or pull requests

1 participant