Skip to content

Repository files navigation

Redux JSON API Module

Redux reducer, actions, action creators, and selectors for interacting with JSON API.

Changelog

v2.2.0

  • Added replace option for fetchRecords, fetchRecord, and saveRecord. When { replace: true }, response records fully overwrite their existing versions in the store instead of being deep-merged. Default behavior is unchanged.
  • Fixed saveRecord TypeScript definition to match implementation signature.

v2.1.1

  • Added comprehensive test suite using Vitest (76 tests across normalize, selectors, and reducer/action creators).

v2.1.0

  • Replaced abandoned json-api-normalizer dependency with an inline implementation, removing transitive dependencies on core-js and lodash that contained security vulnerabilities.
  • Updated all dependencies to latest versions for security patches.
  • Updated TypeScript compilation target from ES5 to ES2015.

Install

Add it to your reducers.

// reducer.js

import { combineReducers } from 'redux';
import api from 'redux-json-api-module';

combineReducers({
  // all reducers
  ...
  api,
  ...
})

Define your API endpoints with axios middle ware

// middleware.js

import { applyMiddleware } from 'redux';
import axios from 'axios';
import axiosMiddleware from 'redux-axios-middleware';

const client = axios.create({
  baseURL: 'https://app.com/api/v1',
  responseType: 'json',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
});

applyMiddleware(
  //all middlewares
  ...
  axiosMiddleware(client), //second parameter options can optionally contain onSuccess, onError, onComplete, successSuffix, errorSuffix
  ...
)

NOTE: If you need to add auth headers or take action on api call request/response checkout redux-axios-middleware interceptors

Options

Replace Mode

By default, records from API responses are deep-merged into the store. This means attributes not present in the response will be preserved from the existing record.

If you want response records to fully replace their existing versions (e.g., when the server returns complete records and you want to discard stale local attributes), pass { replace: true }:

// Deep merge (default) -- existing attributes not in response are preserved
fetchRecords('articles', { page: 1 });

// Replace -- each returned record completely overwrites its existing version
fetchRecords('articles', { page: 1 }, { replace: true });

// Also works with fetchRecord and saveRecord
fetchRecord('articles', 5, { include: 'author' }, { replace: true });
saveRecord(record, { replace: true });

Records of the same type that are NOT in the response are always preserved regardless of the replace setting.

Usage

In our container component we call fetchRecords() to load the data we need. Notice we use the returned promise from fetchRecords() to toggle a loading state.

When fetchRecords() returns with an error we record the ids, this is a good idea for performance reasons. Storing the returned id's means we don't need to sort and filter the raw store data whenever we make changes.

// RecordList.js

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { fetchRecords } from 'redux-json-api-module';

class RecordList extends PureComponent {
  state = {
    ids: [],
    loading: false,
  };
  
  componentWillMount() {
    const { fetchRecords } = this.props;
    
    this.setState({loading: true});
    
    // fetch the 10 most recently updated records with user
    fetchRecords('items', { 
      include: 'user',
      filter: { scope: 'active' },
      sort: '-updated_at',
      page: { size: 10,  }
    }).then((resp) => {
      if (!resp.error) {
        this.setState({ids: resp.payload.data.data.map(r => r.id)})
      }
    }).finally(() => this.setState({loading: false}))
  }
  
  render() {
    const { ids, loading } = this.state;
    
    return (
      loading ? (
        <div className="loader" />
      ) : (
        <ul>
          {ids.map(id => <RecordItem key={id} itemId={id} />)}
        </ul>
      )
    )
  }
}

const mapDispatchToProps = {
  fetchRecords,
};

export default connect(null, mapDispatchToProps)(RecordList)

In our child component we use getRecord() and getRelationship() to load the item and related user records. Then, we pass in only the attributes we need. Passing only needed attributes keeps the mapStateToProps flat and simple, reducing needless rendering

// RecordList.js

import React from 'react';
import { connect } from 'react-redux';
import { getRecord } from 'redux-json-api-module';

const RecordItem = ({ itemTitle, userName }) => (
  <li>{`${itemTitle} by ${userName}`}</li>
);

const mapStateToProps = (state, props) => {
  const item = getRecord(state.api, { type: 'items', id: props.id });
  const user = getRelationship(state.api, item.relationships.user);
  
  return {
    itemTitle: item.attributes.title,
    userName: user.attributes.name,
  }
};

export default connect(mapStateToProps)(RecordList)

About

Redux reducer, actions, and selectors for interacting with JSON API

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages