Mohammed Alhawamdeh
Mohammed Alhawamdeh

Reputation: 47

React doesn't update the sorted items in DOM

so I ran into a problem when I want to sort elements within an array in React.

The problem is not with the sort logic but with React itself, so whenever I want to click a button that says 'sort by difficulty' it doesn't work. However, when I click on a 'Hide' button the same moment the lements get updated in the DOM exactly as I want.

What's causing this issue and how to solve it? Thanks

EDIT : CODE SANDBOX LINK https://codesandbox.io/s/tender-rubin-1nq0r

import React, { useState, useEffect } from 'react';
// import { ListProvider } from '../context/listContext.js';
import useAjax from '../custom-hooks/useAjax.js';
import TodoForm from './form.js';

import './todo.scss';

const ToDo = () => {
  const [_addItem, _toggleComplete, _getTodoItems, _deleteItems, _hideItems, sorted, list] = useAjax()
  useEffect(_getTodoItems, []);

  const [currentPage, setCurrentPage] = useState(1)
  const [todosPerPage, setTodosPerPage] = useState(3)
  const pageNumbers = []
  const indexOfLastTodo = currentPage * todosPerPage;
  const indexOfFirstTodo = indexOfLastTodo - todosPerPage;

  let currentTodos = list.slice(indexOfFirstTodo, indexOfLastTodo)
  for (let i = 1; i <= Math.ceil(list.length / todosPerPage); i++) {
    pageNumbers.push(i);
  }


  return (
    <>
      <header>
        <h2>
          There are {list.filter(item => !item.complete).length} Items To Complete
        </h2>
      </header>

      <section className="todo">

        <div>
          <TodoForm handleSubmit={_addItem} />
        </div>

        <div>
          <ul>
            {currentTodos.map(item => (
              <li
                className={`complete-${item.complete.toString()}`}
                key={item._id}
              >
                <span onClick={() => _toggleComplete(item._id)}>
                  {item.text}
                </span>
                <small>{item.difficulty}</small>
                <button onClick={() => _deleteItems(item)}>X</button>
              </li>
            ))}
            {
              pageNumbers.map(number => {
                return (
                  <button
                    key={number}
                    id={number}
                    onClick={(event) => { setCurrentPage(Number(event.target.id)) }}
                  >
                    {number}
                  </button>
                );
              })
            }
            <button onClick={_hideItems}>hide</button>
            <button onClick={sorted}>Sort By Difficulty</button>
          </ul>
        </div>
      </section>
    </>
  );
};

export default ToDo;



// **************************THe custom Hook file it attached below :******************************************

import { useState } from 'react'
import axios from 'axios'

const todoAPI = 'https://api-js401.herokuapp.com/api/v1/todo'
export default () => {
    const [list, setList] = useState([])
    const _addItem = (item) => {
        item.due = new Date()
        axios.post(todoAPI, JSON.stringify(item), {
            headers: { 'Content-Type': 'application/json' }
        })
            .then(data => {
                setList([...list, data.data])

            })
    }
    const _toggleComplete = id => {
        let item = list.filter(i => i._id === id)[0] || {};
        if (item._id) {
            item.complete = !item.complete;
            let url = `${todoAPI}/${id}`;
            axios.put(url, JSON.stringify(item), {
                headers: { 'Content-Type': 'application/json' }
            })
                .then(data => {
                    setList(list.map((listItem) => listItem._id === item._id ? data.data : listItem))
                })
        }
    }
    const _getTodoItems = async () => {
        let rawData = await axios.get(todoAPI)
        let data = rawData.data.results
        setList(data)
    }
    const _deleteItems = async item => {
        let url = `${todoAPI}/${item._id}`
        let deletedItem = await axios.delete(url)
        setList(list.filter((listItem) => listItem._id === deletedItem.data._id ? '' : listItem))
    }
    const _hideItems = () => {
        setList(list.filter(listItem => (
            listItem.complete === false
        )))
    }
    const sorted = () => {
        setList(list.sort((a, b) => {
            return b.difficulty - a.difficulty
        }))


    }
    return [_addItem, _toggleComplete, _getTodoItems, _deleteItems, _hideItems, sorted, list]
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Upvotes: 2

Views: 373

Answers (1)

Lakshya Thakur
Lakshya Thakur

Reputation: 8316

This is happening because .sort(...) doesn't return a new array but the same one i.e. it's not a new object reference. You have to explicitly do that so that React can know that I have a new object reference that means I should cause a re-render.

In hide implementation, you're using .filter(...) on the list which returns a new array for you so you didn't have to do it explicitly.

Just change your sort implementation inside useAjax hook like so :-

    const sorted = () => {
        setList([...list.sort((a, b) => {
            return b.difficulty - a.difficulty
        })])
    }

... is the spread operator to create a shallow copy of your list.

Forked sandbox - https://codesandbox.io/s/epic-leakey-vr925?file=/src/custom-hooks/useAjax.js

Upvotes: 4

Related Questions