I'm new to ReactJS. I have a table with 2 columns. I want to sort the table based on the column header that is clicked on. Here is the code:
import React, { useState, useEffect } from 'react'
import { getUsers } from '../../services/userService'
const Table = () => {
const [users, setUsers] = useState([]);
const [currentUsers, setCurrentUsers] = useState([]);
const [isSorted, setIsSorted] = useState(false);
const [valueHeader, setValueHeader] = useState({title: "",body: ""}); //Value header state
const [sortedUsers, setSortedUsers] = useState([]);
useEffect(async () => {
try {
const response = await getUsers(search);
setUsers(response.data.users);
} catch (error) { }
}, [search]);
const sortFn = (userA, userB) => {
// sort logic here, it can be whatever is needed
// sorting alphabetically by `first_name` in this case
return userA[valueHeader.body].localeCompare(userB[valueHeader.body]) //<== Use value of column header
}
useEffect(() => {
if (isSorted) {
setSortedUsers(currentUsers.slice().sort(sortFn))
} else {
setSortedUsers(currentUsers)
}
}, [isSorted, currentUsers, valueHeader]) //<== add valueHeader to dependency
const toggleSort = (target) => {
setIsSorted(!isSorted)
setValueHeader({
title: target,
body: target == "name" ? "first_name" : "mobile_number"
}) //<=== set state of value header
}
return (
<div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>
<table className='w-full border-separate rounded-md'>
<thead>
<tr className='bg-text-secondary text-white shadow-sm text-center'>
<th className='p-2' onClick={()=>toggleSort("name")}>name</th>
<th className='p-2' onClick={()=>toggleSort("mobile")}>mobile</th>
</tr>
</thead>
<tbody>
{sortedUsers.map((item, index) =>
<tr key={item.id} className={index % 2 === 0 ? 'bg-white shadow-sm text-center' : 'bg-text bg-opacity-5 shadow-sm text-center'}>
<td className='text-text text-sm p-2'>{item.first_name}</td>
<td className='text-text text-sm p-2'>{item.mobile_number}</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
export default Table
My code is working fine. When I click on a column header, the table is sorted in the ascending order. However, when I re-clicked on the column header, the table becomes unsorted. A Cycle like this:
Unsorted === click ====> ascending ...
Is it possible to sort the table in descending order when I re-click on the column header? I mean a cycle like this:
Unsorted === click ====> ascending === click ===> descending ...
Related
I am fetching all docs from firebase, I did it successfully, I can even log it in console.
Mapping through the list is the problem.
The fetch API takes time before it returns the data, while my map function runs
immediately after the components were invoke, at that moment the API didn't return
anything, so map function will have nothing to map.
import { firebaseDB } from "./db";
import { collection, getDocs } from "firebase/firestore";
const responseArr: any[] = [];
// GETS ALL AVAILABLE DOCS IN GOALs COLLECTION
const getAllGoals = async () => {
const response = await getDocs(collection(firebaseDB, "goals"));
response.forEach((doc) => {
responseArr.push(doc.data());
});
};
export { getAllGoals, responseArr };
Now all I want is to wait for response then store it array in in useState().
import React, { useEffect, useState } from "react";
import OnprogressGoals from "./OnprogressGoals";
import GoalsDone from "./GoalsDone";
import { getAllGoals, responseArr } from "../../containers/GetAllGoals";
const GoalList = (props: any) => {
const [refresh, setrefresh] = useState([]);
useEffect(() => {
getAllGoals();
responseArr.map((goal) => {
const goaler: any = [...refresh, goal];
setrefresh(goaler);
});
}, []);
console.log(refresh);
const OnprogressGoalsResponse = responseArr.filter(
(Goal) => Goal.GoalCompletion === "Onprogress"
);
const GoalsDoneResponse = responseArr.filter(
(Goal) => Goal.GoalCompletion === "Done"
);
return (
<div className="h-[85%] overflow-y-scroll scrollbar-thin scrollbar-thumb-gray-900 scrollbar-track-gray-50">
<button onClick={() => alert("I am clicked")}>click me</button>
<table className="w-full h-full">
<>
<tr className="border-b px-5">
<td className="text-sm font-medium py-5 pl-5">
<span>Date created</span>
</td>
<td className="text-sm font-medium py-5">
<span className="border-l pl-3">Goal name</span>
</td>
<td className="text-sm font-medium py-5">
<span className="border-l pl-3">Due date</span>
</td>
<td className="text-sm font-medium py-5 pr-5">
<span className="border-l pl-3">Goal target</span>
</td>
</tr>
<tr>
<td colSpan={4} className="px-5">
<h1 className="py-5 font-semibold text-2xl">On progress</h1>
</td>
</tr>
{/* {console.log(`Goal List - ${props.goalsList}`)} */}
<OnprogressGoals
ShowGoalDetail={props.ShowGoalDetail}
goalsList={OnprogressGoalsResponse}
/>
<tr>
<td colSpan={4} className="px-5">
<h1 className="py-5 font-semibold text-2xl">Goal done</h1>
</td>
</tr>
<GoalsDone
ShowGoalDetail={props.ShowGoalDetail}
goalsList={GoalsDoneResponse}
/>
</>
</table>
</div>
);
};
export default GoalList;
Your dependency array is empty that why useEffect is called only once after the render and your component cannot render the update data in responeArr. So to call it when data is load put your responseArr as dependancy in the useEffect.
Just update your useEffect with the below code
useEffect(() => {
getAllGoals();
responseArr&&responseArr.map((goal) => {
const goaler: any = [...refresh, goal];
setrefresh(goaler);
});
}, [responseArr]);
That's right because the responseArr doesn't waiting for getAllGoals.
Try this:
useEffect(() => {
getDocs(collection(firebaseDB, "goals")).then((response) => {
response.forEach((goal) => {
const goaler: any = [...refresh, goal];
setrefresh(goaler);
}
}
);
}, []);
remove responseArr and use responseArr as useState(), I hope that help you
// const responseArr = []; it not update your component after rendeing component
let ArrEmpWithType : Array<any> = []
const GoalList = (props: any) => {
const [responseArr, setresponseArr] = useState(ArrEmpWithType);
const [refresh, setrefresh] = useState([]);
const getAllGoals = async () => {
const response = await getDocs(collection(firebaseDB, "goals"));
response.forEach((doc) => {
let Mydoc: any = doc.Data();
Mydoc.id = doc.id;
setresponseArr([...responseArr, Mydoc])
});
};
useEffect(() => {getAllGoals(); }, []);
useEffect(() => { }, [responseArr]);// it help rerender after update responseArr
I'm new to both react and Tailwind CSS. I've created a table. Table columns are related (each name in the 1st column has a related mobile number in the 2nd column). I want to add an option on each column of this table, so that when I click on the header of a column, the table rows become sorted (alphabetically or numerically) according to that column. Here is the code:
import React, { useState, useEffect } from 'react'
import { getUsers } from '../../services/userService'
const Table = () => {
const [users, setUsers] = useState([]);
const [currentUsers, setCurrentUsers] = useState([]);
const [search, setSearch] = useState('');
const [isSorted, setIsSorted] = useState(false);
const [sortedUsers, setSortedUsers] = useState([]);
useEffect(async () => {
try {
const response = await getUsers(search);
setUsers(response.data.users);
setPageCount(Math.ceil(response.data.users.length / pageItemCount))
setCurrentUsers(response.data.users.slice(0, pageItemCount))
} catch (error) { }
}, [search]);
const handleChange = (event, value) => {
changePage(value);
}
const sortFn = (userA, userB) => {
// sort logic here, it can be whatever is needed
// sorting alphabetically by `first_name` in this case
return userA.first_name.localeCompare(userB.first_name)
}
const toggleSort = () => {
setIsSorted(!isSorted)
}
// when `currentUsers` changes we want to reset our table
// in order to keep it in sync with actual values
// we're also sorting if we were already sorting
useEffect(() => {
if (isSorted) {
setSortedUsers(currentUsers.slice().sort(sortFn))
} else {
setSortedUsers(currentUsers)
}
}, [isSorted, currentUsers])
return (
<div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>
<table className='w-full border-separate rounded-md'>
<thead>
<tr className='bg-text-secondary text-white shadow-sm text-center'>
<th className='p-2' onClick={(e) => toggleSort()}>name</th>
<th className='p-2' onClick={(e) => toggleSort()}>mobile</th>
</tr>
</thead>
<tbody>
{sortedUsers.map((item, index) =>
<tr key={item.id} className={index % 2 === 0 ? 'bg-white shadow-sm text-center' : 'bg-text bg-opacity-5 shadow-sm text-center'}>
<td className='text-text text-sm p-2'>{item.first_name}</td>
<td className='text-text text-sm p-2'>{item.mobile}</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
export default Table
The code is working fine. The only problem is that the table is only sorted based on the first name regardless of which column header I click on (So when I click on the mobile column header, the table is still sorted based on the first_name). How can I change it, so that the table content become sorted according to the clicked column header?
You should have some state which holds the key which the data is currently sorted by. Whenever a header is clicked update that state to whatever column was clicked.
On every re-render you can then use the key to access the values to sort by.
Please note: This sort() function will just compare strings and numbers and will only compare if the datatype matches.
const users = [
{ fname: "Thomas", lname: "Fox", age: 51 },
{ fname: "John", lname: "Mayor", age: 18 },
{ fname: "Ronny", lname: "Bush", age: 32 },
{ fname: "Aaron", lname: "Schulz", age: 73 },
];
const cols = [
{ key: "fname", text: "First name" },
{ key: "lname", text: "Last name" },
{ key: "age", text: "Age" },
];
const Table = () => {
const [data, setData] = React.useState(users);
const [columns, setColumns] = React.useState(cols);
const [sortedBy, setSortedBy] = React.useState(columns[0].key);
console.log(`Sorting by column ${sortedBy}`);
const sorted = data.sort((a, b) => {
const aVal = a[sortedBy];
const bVal = b[sortedBy];
if (typeof aVal === "number" && typeof bVal === "number") return aVal - bVal;
else if (typeof aVal === "string" && typeof bVal === "string") return aVal.localeCompare(bVal);
return 1;
});
return (
<table>
<thead>
<tr className="">
{columns.map((col) => (
<th onClick={() => setSortedBy(col.key)}>{col.text}</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((item) => (
<tr>
<td>{item.fname}</td>
<td>{item.lname}</td>
<td>{item.age}</td>
</tr>
))}
</tbody>
</table>
);
};
ReactDOM.render(<Table />, document.getElementById("root"));
th {
border: solid thin;
padding: 0.5rem;
}
table {
border-collapse: collapse;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Instead of just the key you could additionally store whether to store in ascending or descending order for example when we click on a column again we toggle the descending order.
This is just a basic example which should give you some idea on how to implement such functionality.
I try to load user history and show it inside a table.
Inside this table should be a button with "show more"
This button is a dropdown toggle button.
Everything working so far but when i click the button that is inside a map function all dropdowns open at the same time.
I know i've to use an index in the map function but I've no clue how i can use this index for my dropdown state.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useSelector } from 'react-redux';
function OrderHistory() {
const [history, setHistory] = useState([]);
const [dropdown, setDropdown] = useState(false);
const auth = useSelector((state) => state.auth);
const token = useSelector((state) => state.token);
const { user } = auth;
useEffect(() => {
const getHistory = async () => {
const res = await axios.get('/user/history', {
headers: { Authorization: token },
userid: user._id,
});
setHistory(res.data);
};
getHistory();
}, [token, setHistory]);
console.log(history);
const clickHandler = (index) => (event) => {
//event.preventDefault();
setDropdown(!dropdown);
console.log('clicked');
};
return (
<div className='history-page h-screen'>
<h2>History</h2>
<h4>You have {history.length} ordered</h4>
<table>
<thead>
<tr>
<th>Bestellnummer</th>
<th>Datum</th>
<th>Summe</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{history.map((items, index) => (
<tr key={items._id}>
<td>
{items._id}
</td>
<td>{new Date(items.createdAt).toLocaleDateString()}</td>
<td>
{(
Math.round(
items.cartItems.reduce((x, item) => x + item.price, 0) * 100
) / 100
).toFixed(2)}
€
</td>
<td>
<button
class='bg-yellow-500 hover:bg-yellow-400 text-white font-bold py-2 px-4 rounded'
key={index}
onClick={clickHandler(index)}
>
Show more
</button>
<div className={dropdown ? '' : 'hidden'}>
<div>{items.firstName}</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default OrderHistory;
Initialize the dropdown state with null.
const [dropdown, setDropdown] = useState(null);
In clickHandler store, i just store the index.
const clickHandler = (index) => {
setDropdown((prev) => {
return prev === index ? null : index;
});
console.log('clicked', index);
};
Now in map function, check where you apply the condition to show the dropdown. I check if the dropdown is equal to that index, then that dropdown will visible otherwise add hidden class.
You can also use _id to show/hide dropdown instead of index
<td>
<button
class='bg-yellow-500 hover:bg-yellow-400 text-white font-bold py-2 px-4 rounded'
key={index}
onClick={() => clickHandler(index)}
>
Show more
</button>
<div
className={dropdown === index ? '' : 'hidden'}
// style={{ display: dropdown === index ? 'block' : 'none' }}
>
<div>{items.firstName}</div>
</div>
</td>
I am building a table using react table package and tailwindcss. But My table header is not properly aligning with the table body. Please see screenshots.
Table Component Code:
import React from "react";
import { useTable } from "react-table";
const Table = ({ columns, data }) => {
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({
columns,
data,
});
return (
<div className="table-fixed border-collapse">
<table {...getTableProps()} className="block text-center">
<thead className="">
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th
className="bg-primary-dark text-white p-4 text-center"
{...column.getHeaderProps()}
>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
<tbody
{...getTableBodyProps()}
className="block overflow-auto h-72 bg-primary-light text-primary-dark "
>
{rows.map((row, i) => {
prepareRow(row);
return (
<tr className="block" {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td className="p-4" {...cell.getCellProps()}>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default Table;
Table Component Container code:
import React, { useMemo } from "react";
import useData from "../../hooks/useData";
import Table from "./Table";
const TableSection = React.memo(({ query }) => {
const { data, runtime, error } = useData(query);
const column =
data.length > 0 &&
Object.keys(data[0]).map((key) => {
return {
Header: data[0][key],
accessor: key,
};
});
const columns = useMemo(() => column, [column]);
const queryData = useMemo(() => data.slice(1), [data]);
return (
<div className="col-start-2 col-end-3 row-start-3 row-end-4 text-white m-6">
<Table columns={columns} data={queryData} />
</div>
);
});
export default TableSection;
i have an eCommerce application i want the quantity of an item in shopping cart** to be increased as user clicks on plus icon , but when the user clicks on the item state changes but the component doesn't re-renders there for not updating the quantity number in shopping cart , but as i refresh the page the number is updated
main code:
const [item, setItems] = useState([]);
useEffect(() => {
setItems(items);
}, []);
const handlePlus = (number, index) => {
let Nnumber = (number += 1);
let changeitems = item;
changeitems[index].qty = Nnumber;
setItems(changeitems);
localStorage.setItem("items", JSON.stringify(changeitems));
};
JSX:
<span onClick={(e) => {
handlePlus(eachItem.qty, index);
}}
>
<i className="bi bi-plus-circle-fill text-success"></i>{" "}
</span>
Complete code
import { Fragment, useState, useEffect } from "react";
const Cartitemscreater = ({ items ,cartUpdater}) => {
const [total, setTotal] = useState(0);
const [item, setItems] = useState([]);
const [available,setAvailable] = useState(true)
useEffect(() => {
setItems(items);
let totalPr = 0;
for (let index = 0; index < items.length; index++) {
let price = parseInt(items[index].productprice);
totalPr += price * items[index].qty;
}
setTotal(totalPr);
}, []);
let changeItems = []
const handlePlus = (number, index) => {
let Nnumber = (number += 1);
changeItems = item;
changeItems[index].qty = Nnumber;
setItems(changeItems);
localStorage.setItem("items", JSON.stringify(changeItems));
};
const handleMinus = (number, index) => {
let Nnumber = (number -= 1);
let changeitems = item;
changeitems[index].qty = Nnumber;
setItems(changeitems);
localStorage.setItem("items", JSON.stringify(changeitems));
};
const handleDelete = (_id,index)=>{
let deleteState = []
for (let index = 0; index < item.length; index++) {
if(item[index]._id!==_id){
deleteState.push(item[index])
}
}
setItems(deleteState)
cartUpdater(deleteState.length)
if(deleteState.length===0){
setAvailable(false)
return localStorage.removeItem("items")
}
localStorage.setItem("items",JSON.stringify(deleteState))
}
return (
<Fragment>
{ available ?<div className="container my-5">
<div className="table-responsive-xl">
<table class="table mt-5 ">
<thead>
<tr>
<th scope="col">Product image</th>
<th scope="col">Product title</th>
<th scope="col">Product price</th>
<th scope="col">Product quantity </th>
</tr>
</thead>
{ item.map((eachItem, index) => {
return (
<tbody key={eachItem._id} className="shadow">
<tr>
<td>
<img
src={`/products/${eachItem._id}/${eachItem.prImage}`}
className="img-fluid imgs "
alt="somethings"
style={{
width: "130px",
height: "80px",
objectFit:"cover"
}}
/>
<p><small className="text-muted">{eachItem.productdescription}</small></p>
<span onClick={()=>{
handleDelete(eachItem._id,index)
}}><i class="bi bi-trash fs-4 text-danger"></i></span>
</td>
<td>{eachItem.producttitle}</td>
<td className="fw-bold">$ {eachItem.productprice}</td>
<td>
<span
onClick={(e) => {
handleMinus(eachItem.qty, index);
}}
>
{" "}
<i className="bi bi-dash-circle-fill text-danger"></i>
</span>
<span className="mx-2"> {eachItem.qty}</span>
<span
onClick={(e) => {
handlePlus(eachItem.qty, index);
}}
>
<i className="bi bi-plus-circle-fill text-success"></i>{" "}
</span>
</td>
</tr>
</tbody>
);
})}
</table>
</div>
</div>: <p className="mt-lg-5 fw-bold text-danger fs-4"> {"Cart is empty "}<i class="bi bi-cart-fill"></i> </p> }
</Fragment>
);
};
export default Cartitemscreater;
The number isn't updating because useState is async. React is not updating instant instead it is just putting the state update into a queue to avoid unnecessary re-renders. If you really need the update you can use this update function.
Example:
const useForceUpdate = () => {
const [value, setValue] = useState(0);
return () => setValue((value) => value + 1);
};
Just call it in the same click event.