0

I built a react-redux app that fetches a list of Reviews from a Rails API - I really want to add the ability to sort that list of reviews in alphabetically ascending/descending order, but I'm not sure where to start.

Do I need to add how I want to sort this list to the state and then add a new action in order to do this? Or is there a simpler way to just add this to the component that shows the list of Reviews?

I'd love exact code examples if possible because I'm really stumped on how to start this and where I can accomplish this.

./components/reviews/ReviewsList.js

import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'

const ReviewList = props => {
  const reviewCards = props.reviews.length > 0 ?
    props.reviews.map(r => (
    <div>
    <br></br>
      <p key={r.id}><Link className="App-link" to={`/app/v1/reviews/${r.id}`}>{r.attributes.title}</Link>
      </p>
    </div>)) :
    null


  return reviewCards
}
const mapStateToProps = state => {
  return {
    reviews: state.reviews.reviews
  }
}

export default connect(mapStateToProps)(ReviewList)

App.js

import React from 'react';
import './App.css';
import ReviewsContainer from './containers/ReviewsContainer';
import ReviewList from './components/reviews/ReviewList'
import NewReviewForm from './components/reviews/NewReviewForm'
import Review from './components/reviews/Review'
import Header from './components/Header'
import Home from './components/Home'
import Footer from './components/Footer'
import { Switch, Route, withRouter } from 'react-router-dom'
import { connect } from 'react-redux';

class App extends React.Component {

  render(){
    const { reviews } = this.props
  return (
    <div className="App">
      <Header />
      <ReviewsContainer />
      <Switch>
        {/* Route renders appropriate components with the link path */}
      <Route exact path='/' component={Home}></Route> 
      <Route exact path='/app/v1/reviews' component={ReviewList}></Route>
      <Route exact path='/app/v1/reviews/new' component={NewReviewForm}/>
      <Route exact path='/app/v1/reviews/:id' render={props => {
        const review = reviews.find(review => review.id === props.match.params.id )
          return <Review review={review} {...props}/>
        }
        // above renders the specific Review URL to the appropriate selection -
      }/>
      </Switch>
      <br></br>
      <Footer />
    </div>
  );
  }
}

const mapStateToProps = state => {
  return ({
    reviews: state.reviews.reviews
  })
}
export default withRouter(connect(mapStateToProps)(App))

store.js

import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
import reviews from './reducers/reviewReducer'
import newReviewForm from './reducers/newReviewForm'
import thunk from 'redux-thunk'

const reducers = combineReducers({
    reviews,
    newReviewForm
  })
  
  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const store = createStore(reducers, composeEnhancer(applyMiddleware(thunk)))

  export default store

Let me know if you'd like to see other code as well.

2
  • What property of the reviews would you like to sort by? Commented Feb 21, 2021 at 19:05
  • the title - which is what the ReviewsList is currently displaying by Commented Feb 21, 2021 at 19:05

1 Answer 1

2

Based on the wording of your post I am assuming that props.reviews contains the entire list of reviews rather than just the first page and that we will not be displaying a different subset as a result of sorting. If that is the case, then my recommendation is to keep the sorting out of redux and deal with it in the component itself through local state or react router.

Local State

It is up to you what format you want to use for your state, as there are multiple approaches that make sense.

Here we save a string value "asc" or "desc":

const [order, setOrder] = useState("asc");

Here we save a boolean. The order is descending if it's true and ascending if it's false.

const [isDesc, setIsDesc] = useState(false);

You could use a second state in addition to the order if you want to support sorting by properties other than title:

const [sortBy, setSortBy] = useState("title");

Router Vars

It might also make sense to control the sort through react-router with a URL query (something like "/app/v1/reviews/?sort=title&order=asc"). You can see an example of that approach in the react-router docs.

Lodash Sort

Lodash has methods sortBy and reverse that make it super easy to sort your array of review objects.

const sorted = _.sortBy(props.reviews, 'attributes.title');
const reviews = order === "desc" ? _.reverse(sorted) : sorted;

Pure JS Sort

If you don't to rely on a package, you can pass a custom compare function using to the built-in array sort method. Since sort() mutates the array, you'll want to do this on a copy.

You can use localeCompare on the string from attributes.title. Under the hood the comparison uses a number, so we can multiply the results by -1 to reverse the order.

const reviews = [...props.reviews].sort( (a, b) => 
  (order === "desc" ? -1 : 1 ) * a.attributes.title.localeCompare(b.attributes.title)
)

Compared to the lodash version, this is harder to read but just as concise.

Component Code

I rewrote your ReviewList component such that it sorts the results and also includes a dropdown to change the sort order. Here is a working demo (with very silly dummy data).

const ReviewList = (props) => {

  const [order, setOrder] = useState("asc"); // either "asc" or "desc"

  const reviews = [...props.reviews].sort(
    (a, b) =>
      (order === "desc" ? -1 : 1) *
      a.attributes.title.localeCompare(b.attributes.title)
  );

  return (
    <div className="review-list">
      <div className="sort-order">
        <label htmlFor="reviews-sort-order">Order: </label>
        <select
          id="reviews-sort-order"
          value={order}
          onChange={(e) => setOrder(e.currentTarget.value)}
          disabled={reviews.length === 0}
        >
          <option value="asc">A-Z</option>
          <option value="desc">Z-A</option>
        </select>
      </div>
      {reviews.length === 0 ? (
        <div className="no-reviews-found">No Reviews Found</div>
      ) : (
        <ul>
          {reviews.map((r) => (
            <li key={r.id}>
              <Link className="App-link" to={`/app/v1/reviews/${r.id}`}>
                {r.attributes.title}
              </Link>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};
Sign up to request clarification or add additional context in comments.

1 Comment

this is incredible, I figured it would be easiest to just add in that component. thank you so much

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.