GithubHelp home page GithubHelp logo

uptrend-tech / uptrend-redux-modules Goto Github PK

View Code? Open in Web Editor NEW
5.0 2.0 1.0 212 KB

Collection of modules useful to redux based react apps.

License: MIT License

JavaScript 99.93% Emacs Lisp 0.07%
redux-modules redux-saga react normalizr redux-saga-thunk

uptrend-redux-modules's Introduction

uptrend-redux-modules

Redux Module (redux + redux-saga + redux-saga-thunk) for requesting resources from API and storing response data into entities if provided a normalizr schema.


Build Status Code Coverage version downloads MIT License

All Contributors PRs Welcome Code of Conduct

Watch on GitHub Star on GitHub Tweet

The problem

At Uptrend we enjoy building React applications and have had success using redux + normalizr to manage state and redux-saga + redux-saga-thunk to orchestrate application side effects (i.e. asynchronous things like data fetching). Code is easy to understand and typically works as expected but someone could have a criticism about the amount of ceremony and boilerplate required.

Typically, whenever adding a new entity to an app it required us to write reducers, actions, sagas, schemas, selectors, and container components to get basic CRUD functionality.

This solution

Create a concise and straightforward way to make HTTP requests that normalize response handling including normalization of response data into index entities in the redux store. To get CRUD functionality for a new entity, you add a normalizr schema and use the provided actions and selectors provided by URM (uptrend-redux-modules). URM also provides render prop React components that simplify and reduce the amount of code needed.

Below are code examples to highlight what using URM resource and entities looks like:

URM Resource & Entities Graph

ResourceDetailLoader Component

const OrgDetailAutoLoader = ({orgId}) => (
  <ResourceDetailLoader resource="org" resourceId={orgId} autoLoad>
    {({status, result, onEventLoadResource}) => (
      <div>
        <pre>{'autoLoad={true}'}</pre>

        <button onClick={onEventLoadResource} disabled={status.loading}>
          Load Resource
        </button>

        {status.initial && <span className="label label-default">initial</span>}
        {status.loading && <span className="label label-primary">loading</span>}
        {status.error && <span className="label label-danger">error</span>}
        {status.success && <span className="label label-success">success</span>}

        {status.loading ? (
          <h5>Loading...</h5>
        ) : (
          result && (
            <div>
              <div>
                Org ID: <code>{result.id}</code>
              </div>
              <div>
                Active: <code>{result.active ? 'Yes' : 'No'}</code>
              </div>
            </div>
          )
        )}
      </div>
    )}
  </ResourceDetailLoader>
)

ResourceListLoader Component

const OrgListLoader = () => (
  <ResourceListLoader resource="org">
    {({status, result, onEventLoadResource}) => (
      <div>
        <div>
          <pre>{'autoLoad={false}'}</pre>
          <pre>{JSON.stringify(status, null, 2)}</pre>
        </div>

        <button onClick={onEventLoadResource} disabled={status.loading}>
          Load Resource
        </button>

        {status.initial && <span className="label label-default">initial</span>}
        {status.loading && <span className="label label-primary">loading</span>}
        {status.error && <span className="label label-danger">error</span>}
        {status.success && <span className="label label-success">success</span>}

        {status.loading ? (
          <h5>Loading...</h5>
        ) : (
          result &&
          result.map(org => (
            <div key={org.id}>
              <span>
                Org ID: <code>{org.id}</code>
              </span>
              <span>
                Active: <code>{org.active ? 'Yes' : 'No'}</code>
              </span>
            </div>
          ))
        )}
      </div>
    )}
  </ResourceListLoader>
)

Using Resource Redux-Saga-Thunk Style

Resource actions provide a promise based interface that redux-saga-thunk allows. Below shows how a resource can use without using selectors. This is nice when you need resource data to save locally.

import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'

const mapDispatchToProps = dispatch => ({
  loadGroups: () =>
    dispatch(resourceListReadRequest('group', {active: true}, 'group')),
})

class GroupListContainer extends React.Component {
  state = {
    loading: false,
    groupList: null,
  }

  componentDidMount() {
    this.loadGroups()
  }

  loadGroups() {
    this.setState({loading: true})
    this.props.loadGroups().then(this.handleLoadSuccess, this.handleLoadFail)
  }

  handleLoadFail = error => {
    this.setState({loading: false, error})
  }

  handleLoadSuccess = ({entities}) => {
    this.setState({loading: false, groupList: entities})
  }

  render() {
    const {loading, groupList} = this.state

    if (loading) return <div>Loading...</div>

    return (
      <ul>{groupList.map(group => <li key={group.id}>{group.name}</li>)}</ul>
    )
  }
}

GroupListContainer.propTypes = {
  fetchTripGroupList: PropTypes.func.isRequired,
}

export default connect(null, mapDispatchToProps)(GroupListContainer)

Redux Modules

There

// TODO

Table of Contents

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's dependencies:

yarn add uptrend-redux-modules

Example Project Usage

Below is an example of how one may set it up in a react app using the resource and entities redux-modules.

Do note there are many ways you could organize your project and this example is not strict guidelines by any means.

Resource & Entities

  • src/store/modules/resource/index.js

    // - src/store/modules/resource/index.js
    import {createResource} from 'uptrend-redux-modules'
    
    // createResource(...) => { actions, reducers, sagas, selectors }
    export default createResource()
  • src/store/modules/entities/index.js

    // - src/store/modules/entities/index.js
    import {createEntities} from 'uptrend-redux-modules'
    import schemas from './schemas'
    
    // createEntities(...) => { actions, middleware, reducers, sagas, selectors }
    export default createEntities({schemas})
  • src/store/modules/entities/schema.js

    // - src/store/modules/entities/schemas.js
    import {schema} from 'normalizr'
    
    export const user = new schema.Entity('users')
    export const team = new schema.Entity('teams', {owner: user, members: [user]})
  • src/store/actions.js

    // - src/store/actions.js
    import {actions as entities} from 'src/store/modules/entities';
    import {actions as resource} from 'src/store/modules/resource';
    
    export {
      ...entities,
      ...resource,
    }
  • src/store/middlewares.js

    // - src/store/middlewares.js
    import {middleware as entities} from 'src/store/modules/entities'
    
    export default [
      // redux-modules middlewares
      entities,
    ]
  • src/store/reducers.js

    // - src/store/reducer.js
    import {combineReducers} from 'redux'
    
    import {reducer as entities} from 'src/store/modules/entities'
    import {reducer as resource} from 'src/store/modules/resource'
    
    export default combineReducers({
      entities,
      resource,
    })
  • src/store/sagas.js

    // - src/store/sagas.js
    import {sagas as entities} from 'src/store/modules/entities'
    import {sagas as resource} from 'src/store/modules/resource'
    
    // single entry point to start all Sagas at once
    export default function*(services = {}) {
      try {
        yield all([
          // app specific sagas
          example(services),
    
          // redux-modules sagas
          entities(services),
          resource(services),
        ])
      } catch (error) {
        console.error('ROOT SAGA ERROR!!!', error)
        console.trace()
      }
    }
  • src/store/selectors.js

    // - src/store/selectors.js
    import {selectors as fromEntities} from 'src/store/modules/entities'
    import {selectors as fromResource} from 'src/store/modules/resource'
    
    export {fromEntities, fromResource}

Usage

// TODO

Inspiration

Organizing actions, reducers, selectors, sagas, etc. into a module is based on redux-modules from Diego Haz.

The resource and entities modules specifically are modified version of those found in redux-modules and ARc.js.

Other Solutions

I'm not aware of any, if you are please make a pull request and add it here!

Contributors


Brandon Orther

💻 🚇 ⚠️ 💡

Dylan Foster

🐛 🤔

Thanks goes to these people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT

uptrend-redux-modules's People

Contributors

orther avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

fossabot

uptrend-redux-modules's Issues

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Add Resource Components for Create, Update, Delete

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.

Warn that entities middleware needs to be before redux-saga-thunk

To be able to return the normalized (entities) results in the redux-saga-thunk promise the entities middleware needs to be before the redux-saga-thunk middleware. Otherwise the entities middleware isn't called until after the redux-saga-thunk middle returns results.

Ideally we could add a check that would warn if the middlewares were out of order.

The following code examples are from a project using URM that I updated to get the entities values in promise returned from action request.

Previous (Not Working)

export default [
  ReduxSagaThunk,
  ReduxThunk,
  routerMiddleware(browserHistory),
  // NOTE: ^^-- above reducers must be first and in order

  // redux-modules
  entities,
];

Updated (Working)

export default [
  // redux-modules
  entities,

  // following middleware must be in this order
  ReduxSagaThunk,
  ReduxThunk,
  routerMiddleware(browserHistory),
];

ResourceLoader: More flexible render options and defaults

Feature Request

Is your feature request related to a problem? Please describe.
ResourceLoader shouldn't need to call a render child function. If we want to use the resource loader to load some data on mount and don't care about status of the request or dealing with failing requests we could just do: <ResourceLoader resource="/path" />.

More details to follow when time permits...

Standardize the resource loading status names between helper selectors and ResourceLoader components

Feature Request

Is your feature request related to a problem? Please describe.
The status selectors from the URM resource helper are named differently than the status properties returned from the ResourceLoader components.

The URM helper selectors are using the redux-saga-thunk naming (i.e pending, rejected, fulfilled and done) for status selectors. These match the naming convention of the underlying library used redux-saga-thunk. Here is example code of the status selectors returned from a resource helper.

// NOTE the selector names
const { pending, rejected, fulfilled, done } = resourceListRead('demo').selectors;

The ResourceLoader components return a status object with properties initial, loading, error, and success`. Here is example code showing the status object returned to the render prop a ResourceLoader component:

const DemoListLoader = () => (
  <ResourceListLoader resource="demo" autoLoad>
    // NOTE the status names names
    {({status: { loading, error, success, done }) => (
      <div>Load status example</div>
    )}
  </ResourceListLoader>
);

Describe the solution you'd like
I prefer the naming used in the ResourceLoader components so I suggest we update the resource helpers to return selectors renamed to match. The selectors would be renamed:

Current Renamed
pending loading
rejected error
fulfilled success
done done
// suggested updated selector names
const { loading, error, success, done } = resourceListRead('demo').selectors;

Describe alternatives you've considered
Alternatively we could rename the ResourceLoader components status object. I prefer the ResourceLoader status names to the redux-saga-thunk names. They are more clear imo.

Remove entities from state after successful Resource Delete request

We need a way to remove entities from the state. Currently when you delete a resource it is removed from the resource reducer state on the success action and if there was entities state from the resource it stays unaffected. I'm really undecided on how/when to handle removing data from the entities state currently due to the fact we've traditionally jumped through a bunch of hoops to get alternative behavior so we never ran into an issue where data we needed was missing.

I've decided to start by adding meta.removeEntities = true to the action object
return be the resourceDeleteSuccess action creator. In the entities middleware when that flag is detected and it will check if meta.request.entityType holds a schema (e.g. planAppLocation) and meta.request.needle holds a value. If those values are present a newly added ENTITIES_REMOVE action will be dispatched (in the middleware) and the reducer will remove the entity if it exists.

If we recognize this is too aggressive, we will update how/when the meta.removeEntities is enabled to adjust.

EntityList helper component returns every entity if entityIds undefined

Bug Report

Current Behavior
Passing undefined to entityIds prop of the EntityList helper component returns all entities for that type. This behavior resulted in app crashing in glorious fashion when every component was passed 1500 entities to render.

Input Code

  • REPL or Repo link if applicable:
<EntityList entityIds={undefined} entityType="worksheetQuestion">
  {questionList => {
    // !!!
    // !!! questionList is array of every entity detail in redux store state `entities.worksheetQuestion`
    // !!!
  }}
</<EntityList>

Expected behavior/code
It should return 0 results and possibly warn in dev console?

Environment

  • uptrend-redux-modules version: [e.g. v0.21.0]
  • node version: [e.g. Node v9.11.1]
  • yarn (or npm) version: [e.g. Yarn v1.6.0]

Possible Solution

Update the EntityList component to short circuit if entityIds is not pass array.

Support entity returned from DELETE request when HTTP status is 200

Our company APIs have soft delete functionality on some endpoints. These endpoint receive a DELETE request and on success return status code 200 with the updated entity (e.g. is_deleted: true set on it).

To support this an opt-in feature should expose this behavior.

Resource Helper - Add new selector to return `status` object

Current Usage

Currently the resource helpers return a separate selector for each status of a resource request. To minimize the number of passed in props a single status object prop can be created like so:

const {selectors} = resourceListRead('inspector', 'inspector');

const mapStateToProps = state => ({
  status: {
    pending: selectors.pending(state),
    rejected: selectors.rejected(state),
    fulfilled: selectors.fulfilled(state),
    done: selectors.done(state),
  },
});

Desired Usage

Add a selector statusObj that's returned by the resource helpers that provides the status object:

const {selectors} = resourceListRead('inspector', 'inspector');

const mapStateToProps = state => ({
  status: selectors.statusObj(state),
});

ResourceLoader components error: Invariant Violation: Could not find "store" in the context...

Bug Report

ResourceLoader components throw the following error when react-redux is upgraded to v6.0.1

Invariant Violation: Could not find "store" in the context of "Connect(ResourceLoader)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(ResourceLoader) in connect options.

Environment

  • uptrend-redux-modules version: [e.g. v0.21.0]
  • node version: [e.g. Node v10.15.1]
  • yarn (or npm) version: [e.g. Yarn v1.13.0]

Possible Solution

Wrap the ResourceLoader component in Context and Provider as mentioned in the breaking changes section of react-redux v6.0.0 release notes.

Look at redux-devtools fix commit for reference.

Separate Resource success action payload into explicit fields

Currently the resource success actions have the api response set as the payload. To support more features (pagination, etc) we should store values to specific keys. The entities middleware already adds the entities field to the payload object. It makes sense to add a response and entityType to the payload as well as explicitly setting the data field derived from the api response in the resource sagas.

const payload = { 
  data, // the resource data in `resp` 
        // - currently `resp.data` in most projects)

  entities, // undefined by default, 
            //  - entities middleware populates if normalized
            
  entityType, // same as meta
            
  api: {
    response, // the full response body from api request, 
    // room for more api data if needed in future
  },

  resource: {
    path, // string path to resource (same as meta.resource) 
  }, 
};

While this opens up the possibilities for features such as pagination it also provides clarity when looking at the actions (in redux devtools or even in the reducer).

ResourceLoader appending params={}

Reported by Dylan in Slack

Relevant code or config

<ResourceListLoader
  resource={`org/${orgId}/application/${planAppId}/land`}
  loadOnMount
/>

What you did: Loaded a list with the ResourceLoader without passing params.

What happened:
Actual request made was with params get param: http://localhost:3000/api/v1/org/194/application/123/land?params=%7B%7D

Suggested solution:

  1. Do not add params as a GET param. Instead add items in the params object.
  2. Do not add params if the object is empty.

Simplify app code needed to load and access resource (and entities).

Overview

There is quite a bit of boilerplate and ceremony involved to loading a resource,
checking it's status (pending/failed/done/etc) and then getting the response
using selectors (and pulling from entities if an entityType was set).
Traditionally using the redux-saga-thunk pattern is how we've avoided using
selectors. We can make this lib easier to use (less code, less thinking) and
more robust by providing mechanism(s) that do what the current boilerplate code
does. To support multiple use cases I believe it makes sense to provide these
helpers as action/selectors creators, render prop components (based off
selectors) and possible HoCs (higher order components).

Current Usage Example

Here is an example from one of our projects using resource/entities
redux-modules (not from uptrend-redux-modules but it has same API). Notice how
the request path member/search is used in the status selectors and in resource
getList selector. Also the entity type memberSearch is used in request action
and entities getList selector. Do note request path and entityType always need
to match like this:

import { isPending, hasFailed, isDone } from 'redux-saga-thunk';

import { fromEntities, fromResource } from 'rides/store/selectors';
import { modalShow, resourceListCreateRequest } from 'rides/store/actions';
import { MemberSearchPage } from 'rides/components';

const mapDispatchToProps = {
  search: searchParams =>
    resourceListCreateRequest(`member/search`, searchParams, 'memberSearch'),
  showCreateModal: () => modalShow('create-member'),
};

const mapStateToProps = state => ({
  memberListDone: isDone(state, `member/searchListCreate`),       // <-
  memberListFailed: hasFailed(state, 'member/searchListCreate'),  // <- notice the duplication
  memberListPending: isPending(state, 'member/searchListCreate'), // <-
  memberList: fromEntities.getList(
    state,
    'memberSearch', // <- entityType
    fromResource.getList(state, 'member/search'), 
  ),
});

Desired Usage Example

import {resourceCreate} from 'src/store/helpers'

const search = resourceListCreate(
  'member/search', // <- resource
  'memberSearch',  // <- entityType
);

const mapDispatchToProps = {
  search: params => search.action(params), // = resourceListCreateRequest(`user/member`, data, 'member)
};

const mapStateToProps = state => ({
  memberListDone: search.selectors.done(state),
  memberListFailed: search.selectors.rejected(state),
  memberListPending: search.selectors.pending(state),
  memberList: search.selectors.result(state),
});

What the search object looks like:

const search = {
  // The action will take whatever args the resource request action creator
  // takes besides `resource` and `entityType`. Most of the resource request
  // action creators take a single arg of: (data | needle | params)
  // `resourceUpdateRequest` takes two args: (needle, data)
  action, 
  
  selectors: {
  // redux-saga-thunk selectors
  pending,   // <- loading  (boolean)
  fulfilled, // <- success  (boolean)
  rejected,  // <- error    (boolean)
  done,      // <- finished (boolean)
  
  // returned data
  resource, // <- resource data (when normalized returns entity id(s))
  result,   // <- if the resource was normalized into entities this
            //    holds the list of entities, otherwise returns the
            //    resource data (non-normalized)
  }
}

Action & Selectors Creator

It should be pretty straight forward to create a helper that returns the request
action and selectors for accessing a resource like the above example.

  • src/store/helpers/index.js

    import {createResourceHelpers} from 'uptrend-redux-modules';
    import entities from 'sow/store/modules/entities';
    import resource from 'sow/store/modules/resource';
    
    const {
      // NOTE: one for each resource request action type
      resourceCreate,
      resourceDelete,
      resourceDetailRead, 
      resourceListCreate,
      resourceListRead,
      resourceUpdate, 
    } = createResourceHelpers({entities, resource});
    
    export {
      resourceCreate,
      resourceDelete,
      resourceDetailRead, 
      resourceListCreate,
      resourceListRead,
      resourceUpdate, 
    }
  • src/containers/MemberAddContainer.js

Render Prop Components

TBD - (Mostly copied from AECS)

Implementation Checklist

Upgrade redux-saga-thunk

  • Required for new status selectors (pending, fullfiled, rejected, done)

Helper Creators (one for each action creator below)

  • resourceCreateRequest
    • resourceCreateRequest = (resource, data, entityType) => ({
  • resourceListCreateRequest
    • resourceListCreateRequest = (resource, data, entityType) => ({
  • resourceUpdateRequest
    • resourceUpdateRequest = (resource, needle, data, entityType) => ({
  • resourceListReadRequest
    • resourceListReadRequest = (resource, params, entityType) => ({
  • resourceDeleteRequest
    • resourceDeleteRequest = (resource, needle, entityType) => ({
  • resourceDetailReadRequest
    • resourceDetailReadRequest = (resource, needle, entityType) => ({
  • resourceDeleteRequest
    • resourceDeleteRequest = (resource, needle, entityType) => ({

Add caching to Resource

I haven't fully formalized my thoughts on how to make this work but in general I'd like a way to cache API requests. With that there needs to be caching rules or controls.

Will update this issue with further thoughts in the future.

ResourceLoader return state selector based values

Overview

Currently the ResourceLoader component returns a result property in the object passed to the child render prop for accessing the resource or entities the component loaded. result holds a value returned from the resource request action. This means any updates to the resource or entities redux state wont effect the result value. The problem is that in many (most?) cases after loading a resource you would actually want to render the data from the global state so your view will update if there are any state changes. For example if you load a list of resource entities to render an overview that has button to delete one. A resource delete request will update the state. A situation where you would want to use the actual resource request results is backend paginated search forms.

Current Usage Example

The following is a contrived example with a <DeleteUserButton ... /> that when clicked triggers a resource delete request for that user. The problem arrises when the delete request is successful but the user list doesn't update to not include the deleted user.

// NOTE pretend UserDeleteButton has a container that provides deleteUser
const UserDeleteButton = ({ userId, deleteUser }) =>
  <Button onClick={() => deleteUser(userId)}>Delete User</Button>

const UserList = ({ userList }) =>
  <ul>
    {userList.map(user => (
      <li key={user.id}>
        {user.name}
        <UserDeleteButton userId={user.id} />
      </li>
    ))}
  </ul>;

const UserOverview = () =>
  <ResourceListLoader resource="user" entityType="user" loadOnMount>
    {({ status, result: userList }) =>
      <Block>
        {status.pending && <Spinner />}
        {status.success && <UserList userList={userList} />}
      </Block>}
  </ResourceListLoader>;

Solutions

I have several ideas on how we should solve this but it isn't yet clear which solution is best. Below are a list of ways to solve the problem:

  1. Pass down the entity IDs as a property on the render child arg object and use EntityList component to render the list.
  2. Similar to option 1 but rather than pass the IDs just pass the entity list returned from EntityList
    1. This just renders the EntityList inside the ResourceListLoader to reduce boilerplate.
  3. Update ResourceLoader to use the resourceHelpers to create selectors for the resource being loaded and use selectors to load the value passed as result.

Final Notes

I am currently considering several use cases that aren't well supported by current helpers so there is a possibility a overarching refactor will impact this use case.

Documentation

An easy to access reference manual for this library would be quite valuable for our team. It should be helpful to people new to the library but also document the current best practices for all users to reference.

Publishing

If not too much effort I believe it is worth trying to publish the docs as a website using GitHub Pages. I like the static docs site for JS library Crooks which is generated using the open source tool Electric.

Generated Site https://evilsoft.github.io/crocks/docs/
Source Files https://github.com/evilsoft/crocks/blob/master/docs/README.md

NOTE: Electric seems looks nice but we could also just create a static site using Gatsby, react-static or any of the many other options out there.

Docs Content

I expect the content of the docs to change and grow over time. To start I created the follow outline but actual content, organization, and order of information can change.

Overview of concepts

  • General Abstractions
    • redux-modules
    • redux-saga-thunk
  • Package Structure
    • Modules
    • Helpers
  • Features
    • Resource
    • Entities
  • Installation / Setup
    • Install package
    • Setup in project
      • Overview
      • Example
  • Usage Examples
    • Resource / Entities
      • Helpers (createResourceHelper: resourceListRead, etc.)
      • Redux-Saga-Thunk
      • Resource Loader Components
      • Entities Reader Components

Entity loader/reader React components

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

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

In resource helper components, load new resource when `resource` prop changes

Currently we have a situation where a user could change a route param, e.g. /user/1 to /user/2, and if the component tree above ResourceDetailLoader remains the same, it would cause the component to not unmount.

When using the loadOnMount prop, the resource would not be reloaded when the resource changes, since the component would remain mounted.

We should add a mechanism to load a new resource when the resource prop changes, without needing to trigger it manually, either through a new prop (e.g. loadOnResourceChange) or just watching for existing prop changes if loadOnMount is true.

  • Add componentWillReceiveProps lifecycle that checks nextProps.resource and triggers resource request if autoLoad is true.
  • Rename loadOnMount to autoLoadResource
  • Also check for change to nextProps.resourceId in cWRP lifecycle

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.