Reputation: 47
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
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