3

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?

2
  • On a high level, I would pass a prop from your PhotoList component to your Buttons component. This could be used to sort your photos array before you turn them into React Components. Commented Aug 31, 2018 at 19:08
  • Why can you not call sortPhotos from an onClick? Seems like you are there, just need to implement the sort. Commented Aug 31, 2018 at 19:13

1 Answer 1

3

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!

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for this elaborate explanation. I would have one question though: as I am fetching the data from API, I need to set state with term. I managed to do this by passing term to unsplash function and then call fetchPhotos with dispatch in it, however, event though I can log response in .then(json => console.log(json), I cannot return it as usual and the promise keeps on pending. Is there an option I can resolve this promise?
That's tough to troubleshoot without a working example, but are you using some sort of async middleware such as redux-thunk? Are you returning the entire promise? Have you had a chance to see this? May need to create a separate question to get best results, but I think it's an issue of async action creators.
I do use async middleware, indeed it is redux-thunk. I created a new question for this issue, here, I would be very grateful if you could look into that

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.