Redux: API data response sorted on click

Viewed 963

I am trying to develop an application, that is showing photos from Unsplash given a keyword. I managed to fetch specific photos using unsplash.js:

actions:

export function fetchPhotos(term) {
  const unsplash = new Unsplash({
    applicationId:
      "id",
    secret: "secret",
    callbackUrl: "callback"
  });

  const response = unsplash.search
    .photos(term, 1, 20)
    .then(toJson)
    .then(json => json);

  return {
    type: FETCH_PHOTOS,
    payload: response
  };
}

export function setCategory(term) {
  return {
    type: SET_CATEGORY,
    categories: [term]
  };
}

export function sortPhotos(attribute) {
  return {
    type: SORT_PHOTOS,
    attribute
  }
}

Component that renders the photos:

import React, { Component } from "react";
import { connect } from "react-redux";

import SinglePhoto from "../components/SinglePhoto";

class PhotoList extends Component {
  renderPhotos() {
    const { photos } = this.props;
    console.log(photos);
    if (!photos) {
      return <p>Loading...</p>;
    }

    return photos.map(photo => {
      const url = photo.urls.full;
      const id = photo.id;
      const alt = photo.description;
      return <SinglePhoto url={url} key={id} alt={alt} />;
    });
  }

  render() {
    return <div>{this.renderPhotos()}</div>;
  }
}

function mapStateToProps(state) {
  return {
    photos: state.photos,
    categories: state.categories
  };
}

export default connect(mapStateToProps)(PhotoList);

And reducers:

import { FETCH_PHOTOS, SORT_PHOTOS } from "../actions/types";
export default function(state = [], action) {
  switch (action.type) {
    case FETCH_PHOTOS:
      return [...action.payload.results];
    case SORT_PHOTOS:
      break;
    default:
      return state;
  }
}

What I am struggling to do is to actually sort the array of data I receive from the API according to a specific term. The response is an array of objects that makes it impossible to call it in an external component I've called Buttons that I have wanted to set the logic in:

class Buttons extends Component {
  render() {
    const { created_at: date } = this.props.photos;
  console.log(this.props);

    return (
      <div className="buttons">
        {/* <button onClick={() => this.props.sortPhotos(date)}>Sort by creation date</button> */}
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    photos: state.photos
  }
}

const mapDispatchToProps = (dispatch) => bindActionCreators({sortPhotos}, dispatch);

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

As I would need to loop over the photos to actually receive their created_at props.

Example API response

I would like to sort them, for example, taking created_at into account. This would be handled by a button click (there would be other buttons for let's say likes amount and so on). I tried to do this in mapStateToProps until the moment I realized it would be impossible to call this with onClick handler.

As I have read this post, I thought it would be a great idea, however, I am not sure, how can I handle this request by an action creator.

Is there any way that I could call sorting function with an onclick handler?

1 Answers

One approach you can take is using a library such as Redux's reduxjs/reselect to compute derived data based on state, in this case sorted items based on some object key and/or direction. Selectors are composable and are usually efficient as they are not recomputed unless one of its arguments changes. This approach is adding properties to the reducer's state for sort key and sort order. As these are updated in the store via actions/reducers, the selector uses state to derive the elements in the resulting sorted order. You can utilize the sorted items in any connected component.

I've tried my best to recreate a complete example including actions, reducers, selectors, and store structure.

Actions - Created actions for setting sort key/direction. My example is using redux-thunk for handling async actions, but that is in no way necessary:

export const SET_SORT = 'SET_SORT';

const setSort = (sortDirection, sortKey) => ({
  type: SET_SORT,
  sortDirection,
  sortKey
});

export const sort = (sortDirection = 'desc', sortKey = 'created_at') => dispatch => {
  dispatch(setSort(sortDirection, sortKey));
  return Promise.resolve();
};

Reducer - Updated initial state to keep track of a sort key and/or sort direction with photo objects being stored in a child property such as items:

const initialState = {
  isFetching: false,
  sortDirection: null,
  sortKey: null,
  items: []
};

const photos = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_PHOTOS:
      return {
        ...state,
        isFetching: true
      };
    case RECEIVE_PHOTOS:
      return {
        ...state,
        isFetching: false,
        items: action.photos
      };
    case SET_SORT:
      return {
        ...state,
        sortKey: action.sortKey,
        sortDirection: action.sortDirection
      };
    default:
      return state;
  }
};

Selector - Using reselect, create selectors that retrieves items/photos, sortOrder, and sortDirection. The sorting logic can obviously be enhanced to handle other keys/conditions/etc:

import { createSelector } from 'reselect';

const getPhotosSelector = state => state.photos.items;
const getSortKeySelector = state => state.photos.sortKey;
const getSortDirectionSelector = state => state.photos.sortDirection;

export const getSortedPhotosSelector = createSelector(
  getPhotosSelector,
  getSortKeySelector,
  getSortDirectionSelector,
  (photos, sortKey, sortDirection) => {
    if (sortKey === 'created_at' && sortDirection === 'asc') {
      return photos.slice().sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
    } else if (sortKey === 'created_at' && sortDirection === 'desc') {
      return photos.slice().sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
    } else {
      return photos;
    }
  }
);

Component - Utilize selector to render items. Trigger dispatch of sort action via button click passing in a sort key and/or sort order. The linked example uses dropdowns in combination with the button click to set sort key/order:

import { getSortedPhotosSelector } from './selectors';

// ...

handleClick() {
  this.props.dispatch(sort('desc', 'created_at'));
}

render() {
   const { sortDirection, sortKey, items } = this.props;

   <ul>
     {items.map(item => <li key={item.id}>{item.created_at}</li>)}
   </ul>

   <button type="button" onClick={this.handleClick}>SORT</button>
}

const mapStateToProps = state => ({
  items: getSortedPhotosSelector(state),
  sortKey: state.photos.sortKey,
  sortDirection: state.photos.sortDirection
});

export default connect(mapStateToProps)(PhotoList);

Here is a StackBlitz, demonstrating the functionality in action. It includes controlled components such as and to trigger dispatch of a sort action.

Hopefully that helps!

Related