Coder Social home page Coder Social logo

redux-crud-store's Introduction

redux-crud-store - a reusable API for syncing models with a backend

NPM Version NPM Downloads

Jump to What's New

Making a single page application (SPA)? Using a Redux store? Tired of writing the same code for every API endpoint?

This module contains helper functions to make it easier to keep your models in sync with a backend. In particular, it provides these four things:

  1. It handles async for you, using redux-saga. A middleware will watch for your async send actions, and will dispatch the success/error actions when the call is complete.
  2. It implements a default reducer for all of your backend models. This reducer will handle your async send, success, and failure actions, and store them in your redux store in an intelligent way.
  3. You can quickly write action creators for this reducer using the predefined constants and action creators exported by redux-crud-store.
  4. It provides selector functions for your components, which query the store and return a collection of models, a single model, or an object saying "wait for me to load" or "you need to dispatch another fetch action"

How to use it

See docs/API.md for usage.

There are four steps to integrating redux-crud-store into your app:

  1. Set up a redux-saga middleware
  2. Add the reducer to your store
  3. Create action creators for your specific models
  4. Use redux-crud-store's selectors and your action creators in your components

1. Set up a redux-saga middleware

The first step is to import ApiClient and crudSaga from redux-crud-store, which will automate async tasks for you. If your app uses JSON in requests, all you need to do is provide a basePath for the ApiClient, which will be prepended to all of your requests. (See ApiClient.js for more config options). Once you've done that, you can create a redux-saga middleware and add it to your redux store using this code:

import 'babel-polyfill' // needed for IE 11, Edge 12, Safari 9
import createSagaMiddleware from 'redux-saga'

import { createStore, applyMiddleware, compose } from 'redux'
import { crudSaga, ApiClient } from 'redux-crud-store'

const client = new ApiClient({ basePath: 'https://example.com/api/v1' })
const crudMiddleware = createSagaMiddleware()

const createStoreWithMiddleware = compose(
  applyMiddleware(
    crudMiddleware
    // add other middlewares here...
  )
)(createStore)

// assuming rootReducer and initialState are defined elsewhere
const store = createStoreWithMiddleware(rootReducer, initialState)
crudMiddleware.run(crudSaga(client))

The included ApiClient requires fetch API support. If your clients won't support the fetch API, you will need to write your own ApiClient or import a fetch polyfill like whatwg-fetch.

2. Add the reducer to your store

If you like combining your reducers in one file, here's what that file might look like:

import { combineReducers } from 'redux'
import { crudReducer } from 'redux-crud-store'

export default combineReducers({
  models: crudReducer,
  // other reducers go here
})

3. Create action creators for your specific models

Now that the boilerplate is out of the way, you can start being productive with your own API. A given model might use very predictable endpoints, or it might need a lot of logic. You can make your action creators very quickly by basing them off of redux-crud-store's API:

import {
  fetchCollection, fetchRecord, createRecord, updateRecord, deleteRecord
} from 'redux-crud-store'

const MODEL = 'posts'
const PATH = '/posts'

export function fetchPosts(params = {}) {
  return fetchCollection(MODEL, PATH, params)
}

export function fetchPost(id, params = {}) {
  return fetchRecord(MODEL, id, `${PATH}/${id}`, params)
}

export function createPost(data = {}) {
  return createRecord(MODEL, PATH, data)
}

export function updatePost(id, data = {}) {
  return updateRecord(MODEL, id, `${PATH}/${id}`, data)
}

export function deletePost(id) {
  return deleteRecord(MODEL, id, `${PATH}/${id}`)
}

redux-crud-store is based on a RESTful API. If you need support for non-restful endpoints, take a look at the apiCall function in src/actionCreators.js and/or submit a pull request!

4. Use redux-crud-store's selectors and your action creators in your components

A typical component to render page 1 of a collection might look like this:

import React from 'react'
import { connect } from 'react-redux'

import { fetchPosts } from '../../redux/modules/posts'
import { select } from 'redux-crud-store'

class List extends React.Component {
  componentWillMount() {
    const { posts, dispatch } = this.props
    if (posts.needsFetch) {
      dispatch(posts.fetch)
    }
  }

  componentWillReceiveProps(nextProps) {
    const { posts } = nextProps
    const { dispatch } = this.props
    if (posts.needsFetch) {
      dispatch(posts.fetch)
    }
  }

  render() {
    const { posts } = this.props
    if (posts.isLoading) {
      return <div>
        <p>loading...</p>
      </div>
    } else {
      return <div>
        {posts.data.map(post => <li key={post.id}>{post.title}</li>)}
      </div>
    }
  }
}

function mapStateToProps(state, ownProps) {
  return { posts: select(fetchPosts({ page: 1 }), state.models) }
}

export default connect(mapStateToProps)(List)

The select selector function is a convenience wrapper around more specific selectors. You can read more about this function and how it works in the select section of docs/API.md.

Fetching a single record is very similar. A typical component for editing a single record might implement these functions:

import { fetchPost } from '../../redux/modules/posts'
import {
  clearActionStatus, select, selectActionStatus
} from 'redux-crud-store'

....

componentWillMount() {
  const { posts, dispatch } = this.props
  if (posts.needsFetch) {
    dispatch(posts.fetch)
  }
}

componentWillReceiveProps(nextProps) {
  const { posts, status } = nextProps
  const { dispatch } = this.props
  if (posts.needsFetch) {
    dispatch(posts.fetch)
  }
  if (status.isSuccess) {
    dispatch(clearActionStatus('post', 'update'))
  }
}

disableSubmitButton = () => {
  // this function would return true if you should disable the submit
  // button on your form - because you've already sent a PUT request
  return !!this.props.status.pending
}

....

function mapStateToProps(state, ownProps) {
  return {
    post: select(fetchPost(ownProps.id), state.models),
    status: selectActionStatus('posts', state.models, 'update')
  }
}

What does the return value of select() look like?

Select is a helper function to minimize what you need to import into each component. There are simpler selector functions available, documented in docs/API.md.

{
  otherInfo,   # if response was sent in a data envelope, provides the other keys (e.g. pagination data)
  data,        # if isLoading is false, then this will hold either a collection of records, or a single record
  isLoading,   # boolean: false if data is ready and no error occurred while loading data
  needsFetch,  # boolean: true if you still need to dispatch a fetch action (iselect(...).fetch)
  fetch        # action to dispatch, in case `needsFetch` is true
}

Collection caching

redux-crud-store caches collections and records. So if you send a request like GET /posts to your server with the params

{
  page: 2,
  per: 25,
  filter: {
    author_id: 20
  }
}

it will store the ids associated with that particular collection in the store. If you make the same request again in the next 10 minutes, it will simply use the cached result instead.

Further, if you then want to inspect or edit one of the 25 posts returned by that query, it will already be stored in the byId array in the store. Collections simply hold a list of ids pointing to the cached records.

If you ever worry about your cache getting out of sync, it's easy to manually sync to the server from your components.

What's new

  • As of 5.4.0, the code no longer uses immutable.js! This should improve performance and debuggability. Please file an issue if you discover any bugs after this change!

Breaking changes in 5.0.0

  • babel-polyfill is removed. You must import it yourself if you want sagas to work in IE9, Edge 12, or Safari 9 (or other browsers without generator functions).
  • the UPDATE action no longer changes fetchTime on the record. This is probably only a good thing for your app, but it is a change in behaviour.

What's still missing (TODO)

  • allow dispatching multiple actions for API_CALL
  • consider allowing dispatching multiple actions for CREATE/UPDATE/DELETE
  • configurable keys: It would be great to integrate normalizr, so people could specify a response schema and have their data automatically normalized into the store. This would also enable support for nested models for free.
  • it would be great to support nested models in selectors, perhaps using normalizr somehow.
  • tests for every public function
  • tests for every private function too

Brief layout of what state.models should look like

This is a slightly airbrushed representation of what the state.models key in your store might look like:

state.models : {
  posts: {
    collections: [
      {
        params: {
          no_pagination: true
        },
        otherInfo: null,
        ids: [ 15000, 15001, ... ],
        fetchTime: 1325355325,
        error: null
      },
      {
        params: {
          page: 1
        },
        otherInfo: {
          page: {
            self: 1,
            next: 2,
            prev: 0
          }
        },
        ids: [ 15000, 15001, ... ],
        fetchTime: 1325355325,
        error: { status: 500, message: '500 Internal Server error' }
      },
    ],
    byId: {
      15000: { fetchTime: 1325355325,
               error: { type: 403, message: '403 Forbidden' },
               record: { id: 15000, ... } },
      15001: { fetchTime: 1325355325,
               error: null,
               record: { id: 15001, ... } }
    },
    actionStatus: {
      create: { pending: false, id: null, isSuccess: true, payload: null },
      update: { pending: false,
                id: 8,
                isSuccess: false, 
                payload: {
                  message: "Invalid id",
                  errors: { "editor_id": "not an editor" }
                }
              },
      delete: { pending: true, id: 45 }
    }
  },
  comments: {
    // the exact same layout as post...
  },
}

Glossary of frequently used terms:

  • Model is an abstract type like "posts" or "comments"
    • it also refers to an object in state.models
  • Record is a single resource e.g. the post with id=10
  • Collection is a number of records e.g. page 1 of posts
    • state.posts.collections refers to previously executed queries
    • a single collection is made up of params, the returned ids, and then metadata
  • fetch means to go to the server
  • select means to get the existing models from the state and return an object for use in components

License

Copyright 2016 Devin Howard

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

redux-crud-store's People

Contributors

devvmh avatar hallettj avatar julienr2 avatar latal avatar recurser avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-crud-store's Issues

selectActionStatus API

Ever since creating selectNiceActionStatus, I haven't really used anything else. I've been thinking of dropping selectActionStatus. I need to consider the ramifications, and come up with a plan for changing the API.

After briefly reviewing the code just now, I think I'll probably go ahead with this plan....

Probably it would look like this:

3.1.0: deprecate selectActionStatus with a warning that the implementation will change.
4.0.0: replace selectActionStatus with selectNiceActionStatus's implementation. Deprecate selectNiceActionStatus
5.0.0: Remove selectNiceActionStatus as redundant.

Update collection after `createRecord` success ?

Hi @devvmh, thanks for all your hard work and sharing your pattern for handling CRUD operations in Redux. I think it's a very reasonable approach and helps tremendously to reduce boilerplate. Not sure if you're still focused on this project at all, but I'm struggling with a pretty basic concept.

I'm using createRecord to do a POST operation to my RESTful endpoint. I'd expect that after the new record is created, it would be automatically merged back into the collection so that the UI could display the newly created record.

It seems that once I create the new record, the collection is invalidated and needsFetch is being set to true. This makes sense, but now data is empty, even though I can see the data in the store. In my selector (using select or selectCollection) I'm getting back an empty dataset with the needsFetch flag being set to true.

If we know that we need to fetch again to sync our updated model with the server, why isn't this handled automatically? Why is it up for the component to parse this field and trigger a new request? It seems like I'd always want to re-fetch after inserting a new record, or have the new record merged into our collection following a successful POST.

This particular view is the result of several API calls, making it even trickier on where to re-fetch the data if necessary.

Is there a way to handle optimistic updates ? That is, insert a newly created record into a collection so it can be displayed by the UI and then sync later with the backend.

Am I missing something? Been digging through the source and looks like a model will be invalidated because fetchTime is being set to null for a collection given a CREATE_SUCCESS action.

Let me know if you have any insight.

Thank you!

possible breaking changes for 6.x

Collect desired breaking changes that if merged would trigger a version bump to 6.x. More may still be added:

Integrate normalizr

This should, if done right, solve issue #6, #7, and #19 all together. This library originally assumed all FETCH responses came wrapped in a data envelope, but that assumption can't remain. Currently there is a workaround active in the master branch, but normalizr offers a more robust and recognizable solution to this problem.

To implement this, the following changes are needed:

  • Add normalizr as a dependency (probably) or a peerDependency (ideally)
  • Change FETCH action creators to take a Schema object
  • Change the saga to store the Schema object, and then denormalize the data. After that, it can dispatch one action for each collection. There will need to be a new action type (GOT_NORMALIZED_DATA), since FETCH_ONE_SUCCESS would require dispatching potentially a lot of actions, but FETCH_SUCCESS would store collections and parameters, which doesn't make sense.
  • Change the reducer to handle GOT_NORMALIZED_DATA
  • Investigate further to see if it's helpful to store the Schema objects in the store at any point (currently I think this isn't helpful)
  • Scrub references to id, using the schema's idAttribute instead.
  • Investigate FETCH_ONE, CREATE, and UPDATE to see how normalizr will be implemented
  • Investigate DELETE to see if normalizr needs to be implemented.

Handle response envelopes in a smarter way

Once normalizr is in, we'll be in a better spot to handle response envelopes. Currently there are two big issues with how it's handled.

  1. 'data' is assumed as the key holding the data, and if it's not present the payload is assumed to be the records.
  2. The response envelope, if it exists, is stored in the awkwardly named 'otherInfo'.

I would like to do the following:

  • Use normalizr Schema to specify the shape of the response.
  • Specify a clever default that emulates the current behaviour as much as possible.
  • Store the response envelope, if it exists, in record.envelope (instead of record.otherInfo).
  • Deprecate record.otherInfo and remove it from documentation, but leave it as a key until 7.x or even 8.x (or forever, if 6.x is just so stable and great we don't need any more breaking changes).

Store response headers, and make them accessible

I really love http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api, and it inspired the backend that this library was originally written to consume. In this document, he talks about how pagination may someday soon move to live in response headers, rather than in a data envelope. I'd love to support that. It will involve a breaking change to ApiClient.js however *(and by extension, any custom ApiClient implementations that library consumers have written).

  • Update ApiClient.js to return headers as well as response body. If possible, continue to handle responses that don't include the headers. I think the best way will be to return an object with the shape { headers, payload }, and if the return value isn't in that shape, assume it is a 5.x ApiClient that is only passing a payload.
  • Store the headers alongside the envelope in the collections store, and make them accessible in selectors.
  • Consider whether request headers should be stored alongside params in the collections store.
  • Consider how FETCH_ONE, CREATE, UPDATE, and DELETE may/may not need to persist response headers.

For future reference: given a Fetch API response object, you can convert the headers to a plain object like this:

Array.from(response.headers.entries()).reduce((obj, [key, value]) => {
  obj[key] = value
  return obj
}, {})

http response error not handled

Hi !

Diving more into the lib I started to use the apiCall for non-rest endpoints (in my case to handle authentication). First thanks for this function, it allows me to use the lib more globally for almost all my request so far.

Only thing about this apiCall (and I suspect from the other call function), they don't handle http error. In case I receive a 400/403/404/500, the success event is triggered anyway. Diving into the code it seems it is because the apiGeneric function set up a try catch, but doesn't check the status of the response.

Is it wanted ? Why not add a check and trigger a real success event in case their is no error And the http response is <400 ?

I'll fork it and try it too :) Maybe I'll answer myself later !

Thanks in advance for any information !

Examples for create/update/delete?

all the component examples on the readme are for fetching data (and one for status)

it'd be nice to also have an example for something that takes an id/params.

for example, I have this (but I get an error):

export default connect(
  (state, ownProps) => {
    const token = paramFromLocation(ownProps.location, 'token');
    console.log(token);

    return {
      invitation: select(invitationsAPI.update({id: token || ''})),
    };
  }
)(Invitations);

where update is genericly defined as:

    update: ({id, ...data}, params = {}) => updateRecord(
      model, id, `${path}/${id}`, data, params, {/* auth stuff */}
    ),

error:
image
which is kinda weird... PATCH requests are supposed to return the whole resource in their response -- so I'm not sure what's up (the request isn't made though (it hasn't gotten that far))

Bug/expectation misalignment - Currently update requires record to be present locally.

image

Text from other issue: #61 (comment)

My code:

  componentDidMount() {
    const { status, dispatch, location } = this.props;

    if (status.id === null) {
      const token = paramFromLocation(location, 'token');

      dispatch(invitationsAPI.update({id: token}));
    }
  }

where update is genericly defined as:

    update: ({id, ...data}, params = {}) => updateRecord(
      model, id, `${path}/${id}`, data, params, {/* auth stuff */}
    ),

I'm just trying to do an 'accept invite' kind of workflow where the frontend is hit first so feedback can be presented to the user as fast as possible. (opposed to accepting the invite on the server (which would involve non-api server-routing as well)) :-)

Don't set collection to empty array when updating a record.

Whenever updating a record, selectCollection always responds an empty array and it makes my view's empty too. What do you think if you make some changes (the record will have isUpdating or isLoading, ...) so that selectCollection will give back an array

Proposal: Action to reinitialize a store ?

In case we are handling authentication flows, if a user logout and login with other account the "fetch collection" is not called because the request is still cached.

A nice and easy way could be to offer a redux action to reinitialize a model store when logging out. What do you think ?

Way to reinitialize an ApiClient

I have an SPA that implements token based authentication. I need to reinitialize ApiClient with proper auth header after server gives me a token.
Are there any ways to do it?

putting Error objects in the store makes it non-serializable (right?)

I'm fairly certain that doing this will break people's time travel debugging. This probably isn't a serious issue, but I think it's nice to not do that by default.

@hallettj specifically requested this feature so I should leave in a flag or something that allows you to continue to use Error objects in your store.

This will technically be a breaking change if it goes ahead so it can be in 5.0.0

  • verify this actually breaks time travel debugging
  • fix actionTypes.js
  • fix selectors.js
  • check the changes in issue #15 to see how to make this behaviour optional. I think it will just involve a conditional to not use toJS() if the error is an Error object

TypeError: payload.forEach is not a function

I'm getting an error TypeError: payload.forEach is not a function parsing the response from a JSON API endpoint.

Specifically, this response triggers the error. You can run this gist to see the error in action.

Removing the top-level {"data": ...} wrapping (ie. returning an array like this instead of an object) works fine, but I don't have the option to change the data returned by the API endpoint unfortunately. Here's a gist showing it working with the altered data structure.

As far as I can tell I'm complying with the { data: [ ... ] } structure required by redux-crud-store. Any idea what I'm doing wrong here, or how I might be able get it working?

selectRecord don't return isLoading when updating

After update to 5.0.1, selectRecord don't return isLoading field when updating. I think because, in this version we make fetchTime don't change when UPDATE but selectRecord use it to check the record on updating then return isLoading.
Now, my updating icon does not show ๐Ÿ˜ข

Remove explicit dependency on babel-polyfill

I'm also not sure how this would be implemented. I know I still want to be able to use babel-polyfill for maximum browser compatibility, but am open to adding extra code or config on my end to use babel-polyfill. @hallettj could you elaborate?

server-side rendering and sagas issue (originally "React Performance Measurement Error")

Hi,

I've tried to implement redux-crud-store following the ReadMe file. As soon as I call my route in the browers I'll get this messages:

Warning: There is an internal error in the React performance measurement code. Did not expect componentDidMount timer to start while render timer is still in progress for another instance.

Uncaught TypeError: Cannot read property 'getIn' of undefined

Uncaught TypeError: Cannot read property '__reactInternalInstance$w5v7h28v50n414er620uow29' of null

I can't get my head around where the error lies but it does get fired when I call fetchCollection

Landingpage.js

import React, { Component, PropTypes } from 'react';
import styles from './Landingpage.scss'; // eslint-disable-line
import { connect } from 'react-redux';
import { fetchCities } from '../../modules/cities';
import { select } from 'redux-crud-store';

class Landingpage extends Component {
  componentWillMount() {
    const { cities, dispatch } = this.props;
    if (cities.needsFetch) {
      dispatch(cities.fetch);
    }
  }

  componentWillReceiveProps(nextProps) {
    const { cities } = nextProps;
    const { dispatch } = this.props;
    if (cities.needsFetch) {
      dispatch(cities.fetch);
    }
  }

  render() {
    const { cities } = this.props;
    return (
      <div>
        <section
          className={
              `hero hero-position-bottom hero-spacing ${styles['hero-landingpage']}`
          }
        >
          <div className="container">
            <h1 className="h-font text-primary">XXX</h1>
            <h2 className="h-font">XXX</h2>

            <div className={styles['landingpage-form']}>
              <div className={styles['landingpage-form-city']}>
                <select className="c-select">
                  <option value="">Choose your city</option>
                  {cities && cities.map((city) => (
                    <option key={city.slug} value={city.slug}>{city.name}</option>
                  ))}
                </select>
              </div>
              <div className={styles['landingpage-form-action']}>
                <button type="submit" className="btn btn-primary btn-lg">
                  Discover
                </button>
              </div>
              <div className={styles['landingpage-form-action']}>
                <a href="#" className="btn btn-primary-outline btn-lg">
                  Free Sign Up
                </a>
              </div>
            </div>
          </div>
        </section>
      </div>
    );
  }
}

Landingpage.propTypes = {
  cities: PropTypes.object,
  dispatch: PropTypes.func
};

function mapStateToProps(state) {
  return { cities: select(fetchCities(), state.models) };
}

export default connect(mapStateToProps)(Landingpage);

modules/cities.js

import { fetchCollection } from 'redux-crud-store';

const MODEL = 'cities';
const PATH = '/cities';

export function fetchCities(params = {}) {
  return fetchCollection(MODEL, PATH, params);
}

Tests for every public function

Every public function should have tests.

actionTypes.js: no need

actionCreators.js:

  • simple tests to enforce API
  • tests for default method in opts

reducers.js:

  • crudReducer
  • specific tests for sub-reducers?

sagas.js:

  • apiGeneric
  • crudSaga - is there some way to test the branching?
  • specific tests for sub-sagas?

selectors.js:

  • select
  • selectCollection
  • selectRecord
  • selectRecordOrEmptyObject
  • selectActionStatus

Support for JSON:API format json.

I'm having trouble getting this to work with json:api ( jsonapi.org ) format data.
I think it's mostly because of the id being nested in the data object.

// single resource 
{ data: { id: 1 } }

// multiple resources
{ data: [
  { id: 1 }, { id: 2 }
]}

I think redux-crud-store expects the id to be at the top level of the json document.

maybe the adapter pattern from ember-data could help?

Move redux-saga to peerDependencies?

I just noticed that redux-crud-store has redux-saga in dependencies section, but users have to have redux-saga in their project in order to use this package, so why not to put it to peerDependencies saving some bytes in the bundle?

Set headers to request

First thanks for this lib :) Saved me a lot of time !

I'm trying to set up "Authorization" headers ( {headers: {"Authorization": "Token ..."}} ) to a fetch request from an action creator (fetchCollection) but I can't figure out how to do it..

  • If I set it in the params of the fetchCollection, it get parsed an set up as a url params..
  • The opts argument is only used to set the method from what I saw.

In the ApiClient.js it is said:

You can pass in config using action.payload.fetchConfig

But I don't managed to find out how to do this..

Can you give me a little help on this one ?

Built-in ApiClient class using fetch API

It would be great if there was a built-in ApiClient class, so users didn't need to roll their own. It should be an optional import so people can use their own instead.

Configurable response format - don't assume response.data

Currently this library assumes that records/record fetched from server live in response.data. This should be configurable - you should be able to pull from any key.

I need to investigate further how it would work if the data was directly in the response object.

How to keep the new params in render() method on fetch page collection

Nice project, but have a question, here is the code:

import React from 'react'
import { connect } from 'react-redux'

import { fetchPosts } from '../../redux/modules/posts'
import { select } from 'redux-crud-store'

class List extends React.Component {
  componentWillMount() {
    const { posts, dispatch } = this.props
    if (posts.needsFetch) {
      dispatch(posts.fetch)
    }
  }

  componentWillReceiveProps(nextProps) {
    const { posts } = nextProps
    const { dispatch } = this.props
    if (posts.needsFetch) {
      dispatch(posts.fetch)
    }
  }

  onHandlePage = (page) => {
    let action = select(fetchPosts({ page: page  }), models)
    if (action.needsFetch) {
      this.props.dispatch(action.fetch)
    }
  }
  
  render() {
    const { posts } = this.props
    if (posts.isLoading) {
      return <div>
        <p>loading...</p>
      </div>
    } else 
    {
      return <div>
        {posts.data.map(post => <li key={post.id}>{post.title}</li>)}
        <p>
        <button onClick={this.onHandlePage(2)}>2</button>
        <button onClick={this.onHandlePage(3)}>3</button>
        </p>
      </div>
    }
  }
}

function mapStateToProps(state, ownProps) {
  return { posts: select(fetchPosts({ page: 1 }), state.models) }
}

export default connect(mapStateToProps)(List)

when click the button 2 and 3 page step by step. the page index only value 1.
Do i use the error? and How to keep the new params in render() method on fetch page collection?
thanks.

Note: i tried update mapStateToProps method to like this:

function mapStateToProps(state, ownProps) {
  const params = state.getIn(['models', 'posts', 'collections'], fromJS([{params: { page: 1 }}])).last().get('params').toJS() // when the render execution get the new params

  return { posts: select(fetchPosts(params), state.models) }
}

but the way can do next page is fine.
global state look like this:

{
  models: {
    posts: {
      collections: [
        {
          params: ... // page 1
          ....
        },
        {
          params: ...// page 2
        },
        {
          params: ...// page 3
        }
      ],
      byId: {
          ...// here some record
      }
    }
  }
}

but when click pre page can't work. i don't no why, can help me? thanks

5.2.2 - CrudSaga does not appear to be picking up actions

Fetch action fires, but afterwards nothing happens. I see no request in my network tab, and no subsequent actions fire. I am struggling to debug given the lib is transpiled.

redux 3.7
redux-crud-store 5.2.2
redux-saga 0.15.3

Downgrading to 5.2.1 seems to fix it, however.

My Code:

dispatched action

meta: Object,
type: 'redux-crud-store/crud/FETCH',
payload: {
  method: 'get',
  path: '/materials',
  ...
}

store.js

/* global __DEV__ */
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { routerMiddleware } from 'react-router-redux';
import { createLogger } from 'redux-logger';
import { crudSaga, ApiClient } from 'redux-crud-store'
import createHistory from 'history/createBrowserHistory';
import rootReducer from './reducer';

const client = new ApiClient({ basePath: 'http://localhost:8080' });
const crudMiddleware = createSagaMiddleware();

const history = createHistory();
const initialState = {};
const enhancers = [];

const middleware = [
  routerMiddleware(history),
  crudMiddleware
];

// add dev logging/dev tools in dev
if (__DEV__) {
  const __REDUX_DEVTOOLS_EXTENSION__ = window.__REDUX_DEVTOOLS_EXTENSION__;

  if (typeof devToolsExtension === 'function') {
    enhancers.push(__REDUX_DEVTOOLS_EXTENSION__())
  }

  const logger = createLogger({
    stateTransformer: state => ({ ...state, models: state.models.toJSON() })
  });

  middleware.push(logger)
}

const composedEnhancers = compose(
  applyMiddleware(...middleware),
  ...enhancers
);

const store = createStore(
  rootReducer,
  initialState,
  composedEnhancers
);

crudMiddleware.run(crudSaga(client));

export {
  store,
  history,
}

component

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';

import { select } from 'redux-crud-store'

import { fetchMaterials } from '../../redux/modules/materials'

import './MyComponent.css';

class MyComponent extends PureComponent {
  componentWillMount() {
    const { materials, dispatch } = this.props

    if (materials.needsFetch) {
      dispatch(materials.fetch);
    }
  }

  render() {
    return (...jsx stuff)

  }
}

function mapStateToProps(state, ownProps) {
  return { materials: select(fetchMaterials(), state.models) };
}

export default connect(mapStateToProps)(DataTable)

action

const MODEL = 'materials';
const PATH = '/materials';

export function fetchMaterials(params = {}) {
  return fetchCollection(MODEL, PATH, params);
}

Make ImmutableJS optional

This is not high priority.

It would be nice to either

  1. Create the option of ImmutableJS, seamless-immutable, or no immutability.
  2. Migrate from ImmutableJS to seamless-immutable.

Return structure of selectRecord suboptimal

Hi,

is there a reason selectRecord() returns either a Selection or the server response? There could be collisions, if the server response has e.g. the field isLoading or error.

I think the return value could be simplified by not returning sel.data but the whole object. In this case, you always know what you're dealing with.

And last, I don't understand why the error field contains an Error object when in loading state. Shouldn't the error be empty then?

In summary, I'd propose an object structure like this:

return {
    isLoading: boolean,
    needsFetch: boolean,
    error?: Error,
    data?: T
}

More information about errors

When receiving an error like FETCH_ONE_ERROR, the meta data is lacking some information about the error type. For example, the appplication would act differently on an 404 (show a "not found" page), than on a 403 ("redirect to login"). Am I missing some option to achieve that?

Configurable id field

Currently this library assumes all records have a unique id attribute. This should be configurable instead, and shouldn't be assumed to be a number.

Uncaught exception dispatching fetch request

Hi,
I'm sorry to trouble you with this issue, but I've tried everything that I know of to fix it on my own.

I've tried to follow your example code as accurately as possible. I've determined that my remote API call is getting called but I can't get any data to load into the redux store and I'm getting an exception when I call dispatch(events.fetch) in my componentWillMount() callback. The "events" object has the right signature, fetch, isLoading, needsFetch so it looks like your framework is getting injected properly. On thing that doesn't look right is that "isLoading" is true after the call and after I get the following exception. What could I be doing wrong?:

_uncaught at apiGeneric TypeError: payload.map is not a function
at collectionReducer (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:79672:26)
at https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:79708:17
at updateInDeepMap (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:72040:23)
at updateInDeepMap (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:72049:24)
at List.Map.updateIn (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:71347:27)
at List.Map.update (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:71339:15)
at collectionsReducer (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:79707:21)
at https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:79802:17
at updateInDeepMap (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:72040:23)
at updateInDeepMap (https://lightswitch-dashboard-v2-peterbraswell.c9users.io/static/js/bundle.js:72049:24)

Response without data field, how to handle?

Hello!
In my case I have no data field in server response.
This causes an error:

Payload is not an array! Your server response for a FETCH action
          should be in one of the following forms:

          { data: [ ... ] }

          or

          [ ... ]
        
          Here are the contents of your action:

What is the best way to use custom response schema? It seems like I just can override something, but I didn't understood if you have any API for it.

How to use a filter with fetchCollection ?

I use fetchCollection to retrieve data from an API but I need to filter the collection before Redux Crud Store put them in the store, ie before FETCH_SUCCESS reducer is called. How can I do that ?

Error: `model.get(...).toJS is not a function` if API client puts `Error` value in rejected promises

This problem occurs when selecting a single record, using either selectRecord or select, if the library user's API client implementation puts Error values in rejected promises. Using Error values for rejections is a common practice, and is arguably a best practice.

The problem arises because the redux-crud-store reducers attempt to convert error values to Immutable data structures:

    // reducers.js, line 71
    case FETCH_ONE_ERROR:
      return state.setIn([id.toString(), 'fetchTime'], action.meta.fetchTime)
                  .setIn([id.toString(), 'error'], fromJS(action.payload))  // <-- conversion happens here
                  .setIn([id.toString(), 'record'], null)

Selector functions attempt to convert back to a plain Javascript object:

  // selectors.js, line 129
  if (model.get('error') !== null) {
    return {
      isLoading: false,
      needsFetch: false,
      error: model.get('error').toJS()
    }
  }

Immutable's fromJS function does not attempt to create immutable versions of Error values - it just returns the original value. So the later .toJS() invocation fails.

This relates to #9.

Cache period should be configurable

Store data from response of a create?

I'm doing a POST / creating a record, and the response is the full json document representing the newly created record -- it's not in the redux store -- is there something I need to do to put it there?

thanks!

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.