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

Entity loader/reader React components #19

Closed
4 tasks done
orther opened this issue Apr 26, 2018 · 2 comments
Closed
4 tasks done

Entity loader/reader React components #19

orther opened this issue Apr 26, 2018 · 2 comments

Comments

@orther
Copy link
Member

orther commented Apr 26, 2018

Overview

Loading and reading entity data doesn't need to be handled by specific containers for every component that needs data. Render Prop components for loading and accessing entity data are a power solution which allows you to bypass writing all that boilerplate.

Current Usage Example

Loading and accessing an entity currently requires you create a container component that you wrap the component(s) that need to access the entities.

Click to Show Example(s)

Container

// src/components/MemberListContainer
import { connect } from 'react-redux';
import {resourceListRead} from 'src/store/helpers'
import MemberList from 'src/components/MemberList'

const {action, selectors} = resourceListRead('member/all', 'member');

class MemberListContainer extends React.Component {
  componentDidMount() {
    this.loadMembers();
  }
  
  render() {
    return <MemberList props={...this.props} />;
  }
};

const mapDispatchToProps = {
  loadMembers: action,
};

const mapStateToProps = state => ({
  loading: selectors.pending(state),
  error: selectors.rejected(state),
  done: selectors.done(state),
  memberList: selectors.result(state),
});

export default connect(mapStateToProps, mapDispatchToProps)(MemberListContainer);

Component

// src/components/MemberList
import React from 'react';

const MemberList = ({loading, error, memberList}) => {
  if (error) return <p>Error!</p>;
  
  if (loading || !memberList || memberList.length < 1) {
    return <p>Loading...</p>;
  }

  return (
    <ul>
      {memberList.map((member) => <li key={member.id}>{member.name}</li>)}
    </ul>
  );
}

export default MemberList;

Desired Usage & Components

The need for a container component is removed all together when using the following two components to load data.

ResourceListLoader

This component loads the resource and returns the results. If the entityType
prop is provided the resource is normalized into entities and the entities are
returned.

// src/components/MemberList
import React from 'react';
import {ResourceListLoader} from 'src/store/helpers'

const MemberList = () => (
  <ResourceListLoader
    resource={'member/all'}
    entityType="member" // <- Optional
    renderInitial={() => <p>Not Loaded</p>} // <- Show before data loading triggered
    renderLoading={() => <p>Loading...</p>}
    renderError={(/*error*/) => <p>Error!</p>}
    renderSuccess={memberList => (
      {memberList.map((member) => <li key={member.id}>{member.name}</li>)}
    )}
    loadOnMount={true} // <- if not set data doesn't load until `loadData` triggered
  >
    {(View, loadData) => (
      <div>
        <p>
          <button onClick={loadData}>Reload Data</button>
        </p>
        {View}
      </div>
    )}
  </ResourceListLoader>
);

ResourceDetailLoader

This component loads a resource detail (single item) and returns the results. If
the entityType prop is provided the resource is normalized into an entity and
the entity is returned.

// src/components/MemberList
import React from 'react';
import {ResourceDetailLoader} from 'src/store/helpers'

const MemberDetail = () => (
  <ResourceDetailLoader
    resource={'member/123'}
    entityType="member" // <- Optional
    renderInitial={() => <p>Not Loaded</p>} // <- Shown before data loading triggered
    renderLoading={() => <p>Loading...</p>}
    renderError={(/*error*/) => <p>Error!</p>}
    renderSuccess={member => <h1 key={member.id}>{member.name}</h1>)}
    loadOnMount={true} // <- if not set data doesn't load until `loadData` triggered
  >
    {(View, loadData) => (
      <div>
        <p>
          <button onClick={loadData}>Reload Data</button>
        </p>
        {View}
      </div>
    )}
  </ResourceListLoader>
);

EntityDetail

This component pulls an entity detail (single item) from the entities state.
Note that this component does NOT load data like the loaders above.

<EntityDetail entityType="member" entityId={123}>
  {member => <h1 key={member.id}>{member.name}</h1>}
</EntityDetail>

EntityList

This component pulls an entity list (multiple items) from the entities state.
Note that this component does NOT load data like the loaders above.

<EntityList entityType="member" entityIds={[1,2,3]}>
  {memberList => <p key={member.id}>{member.name}</p>)}
</EntityDetail>

Implementation Checklist

Components

Load & Return Data (resource request)

  • ResourceDetailLoader
  • ResourceListLoader

Read Data (data pulled from entities state)

  • EntityDetail
  • EntityList
@orther
Copy link
Member Author

orther commented Apr 28, 2018

Unified single loader component with optional render props.

<ResourceLoader
  resource={'member/all'}

  resourceId="123" // <- not defined = resource LIST
                   //    prop passed = resource DETAIL

  entityType="member" // <- not defined = not normalized (not entity) value set results normalized
                      //    prop passed = response normalized into entities

  // all render render props optional and default to return `null`
  renderInitial={() => <p>Not Loaded</p>}
  renderLoading={() => <p>Loading...</p>}
  renderError={(/*error*/) => <p>Error!</p>}
  renderSuccess={memberList => memberList.map((member) => (
    <li key={member.id}>{member.name}</li>
  ))}

  loadOnMount // <- when if not set data doesn't load until one of the load
              //    functions `loadResource`/`loadResourceData` are called.
              //
>
  {({data: memberList, loadResource, loadResourceData, status, statusView}) => (
    <div>
      {/* `loadData` is a function
        * object provides boolean flags useful for conditionally
        * rendering content.
        */}
      <div>
        <button onClick={requestResource}>(Re)load Data</button>
      </div>

      {/* `status` object provides boolean flags useful for conditionally
        * rendering content.
        */}
      {status.initial && <p>No Request Made Yet</p>}
      {status.error && <p>Error!</p>}
      {status.pending && <p>Loading...</p>}
      {status.success && memberList.map((member) => (
        <ul>
          <li key={member.id}>{member.name}</li>
        </ul>
      ))}

      {/* `statusView` is populated by the render prop matching the current
        * `status`.
        *
        *    renderInitial ~> status.initial === true
        *    renderLoading ~> status.loading === true
        *    renderError   ~> status.error   === true
        *    renderSuccess ~> status.success === true
        */}
      <ul>
        {statusView}
      </ul>
    </div>
  )}
</ResourceLoader>

@orther
Copy link
Member Author

orther commented Apr 30, 2018

Closed by #21

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant