I've got a table, and each row (DifferentialRow) of the table queries for its own data with react-query's useQuery hook.
I want the table's rows to be sorted by a given field value (calculated within DifferentialRow), and also only show the top 10.
<Table.Body>
{filteredVariables!.map((variable, i) => {
return variable.show ? (
<DifferentialRow
key={i}
....
variable={variable.variable}
setFilteredVariables={setFilteredVariables}
/>
) : null;
})}
</Table.Body>
So when a (DifferentialRow) row has retrieved its data and calculated the sort value, I update the parent filteredVariables object with the new row value, sort, and then set show = true for the top 10 using setFilteredVariables which is passed into DifferentialRow (all shown below).
const diffQuery = useQuery(["differential", {startDate, variable}],fetchDifferentialData);
...
if (diffQuery.isSuccess && diffQuery.data) {
setSortValue(calcSortValue(diffQuery.data.data));
}
html rows here
...
function calcSortValue(resultData: any[]) {
// once we've got a result, and we have calculated the diff we need
// to set the filteredVariables object that keeps track of cumulative data to only show top x
try {
let sortValue = resultData[0].dataValue - resultData[numberOfDays - 1].dataValue;
setFilteredVariables((prev: { variable: string; diff: number; show: boolean }[]) => {
let newResults = [...prev, { diff: sortValue, variable, show: undefined }];
newResults.sort((a, b) => {
return Math.abs(b.diff || 0) - Math.abs(a.diff || 0);
});
let inTopTen = newResults
.slice(0, 10)
.map((co) => co.variable)
.includes(variable);
let finalResults: CompareObject[];
if (inTopTen) {
finalResults = newResults.map((nr) => {
return nr.variable === variable? { ...nr, show: true }: nr;
});
} else {
finalResults = newResults.map((nr) => {
return nr.variable === variable? { ...nr, show: false }: nr;
});
}
return finalResults;
});
return diff;
} catch (error) {
return 0;
}
}
This is all creating circular re-rendering, and I can't figure out how to get around it.
Okay, so my solution was to completely remove the DistributionRow component and query for the data in the parent (table) component using useQueries (each query representing a row). Then I do all the sorting and slicing on the result from useQueries.
const diffResults = useQueries(...)
return diffResults.some((dr) => dr.isSuccess) ? .... <Table.Body>
{diffResults
.filter((dr) => dr.isSuccess)
.map((dr: any) => {
let dateSorted = dr.data.data.sort(function (a: any, b: any) {
return new Date(b.runDate).getTime() - new Date(a.runDate).getTime();
});
let diff = Math.round(calcDiff(dateSorted) * 10) / 10;
let fullResult = {
results: dr.data,
diff,
};
return fullResult;
})
.sort((a, b) => {
return Math.abs(b.diff || 0) - Math.abs(a.diff || 0);
})
.slice(0, 10)
.map(({ diff, results }, i) => {
return (
<Table.Row key={i} data-testid="table-row">
<Table.Cell>{results.variable}</Table.Cell>
{results.data.map((d: any, i: number) =>
new Date(d.runDate) > endDate ? (
<Table.Cell key={i}>
{isNaN(d?.dataValue) ? null : Math.round(d?.dataValue * 10) / 10}
</Table.Cell>
) : null
)}
{compareColumn ? <Table.Cell>{diff}</Table.Cell> : null}
</Table.Row>
);
})}
</Table.Body>
Related
I have a sample list of which is an array of objects with three fields in the App component. The list is passed as a prop to an "booksearch" component which handles the logic to search and filter the list based on search text. below is the JSX which renders the book. I am doubting issue is with the "matcheBook" method.
<div className="output-container">
{this.props.books
.filter((e) => this.matchesBook(e))
.map((b) => (
<div className="output-card" key={b.title}>
{Object.entries(b).map(([k, v]) => (
<div key={v} className="out-section">
<span className="heading">
<b>{k}</b>
</span>
<span className="details">{v}</span>
</div>
))}
</div>
))}
</div>
method for handling the search text
handleChange(evt, name) {
let searchText = evt.target.value;
this.setState((state) => ({
...state,
fields: {
...state.fields,
[name]: searchText
}
}));
}
filtering logic
matchesBook(book) {
const { fields } = this.state;
return Object.entries(book).some(
([k, v]) =>
!fields[k] ||
v.toString()
.toLowerCase()
.includes(fields[k].toString().trim().toLowerCase())
);
}
State shape
this.state = {
fields: initialFields
};
"initialFields" comes from below
const fieldsArray = ["author", "title", "year"];
const initialFields = fieldsArray.reduce(
(a, e) => ({
...a,
[e]: ""
}),
{}
);
codesandbox
I've never tried some() function, but doing the search function code on my own way I modified your matchesBook function into this one:
matchesBook(book) {
const { fields } = this.state;
let matching = 0;
for (let i = 0; i < Object.entries(fields).length; i++) {
if (Object.entries(fields)[i][1] === "") {
matching++;
} else {
if(String(Object.entries(book)[i][1]).toLowerCase().includes(String(Object.entries(fields)[i][1]).toLowerCase())){
matching++;
}
}
}
return matching === Object.entries(fields).length;
}
Try it, it'll work!
According to MDN some() method executes the callbackFn function once for each element present in the array until it finds the one where callbackFn returns a truthy value (a value that becomes true when converted to a Boolean). If such an element is found, some() immediately returns true.
In your case !fields[k] always return true when its value is "" and so it never have to compare succeeding values in the array which contains the field we are checking, in this case year. Please see example below.
const book = {
author: "Charles Dickens",
title: "The Pickwick Papers",
year: "1837"
};
const fields = {author: "", title: "", year: "lookinforthisstringinbook"};
const checkValue = Object.entries(book).some(
([k, v]) =>
!fields[k] || // --> always returns true
v.toString()
.toLowerCase()
.includes(fields[k].toString().trim().toLowerCase())
);
console.log(checkValue)
Here is a working solution where I ran a Array.prototype.every on values of the search object and Array.prototype.some on the book object
matchesBook(book) {
const { fields } = this.state;
return Object.values(fields)
.every(v => v === "") ||
Object.entries(book)
.some(([k, v]) =>
fields[k] !== "" && v.toString()
.toLowerCase()
.includes(fields[k].trim().toLowerCase()));
}
I want to skip all null value from array in React
This is a part of my code.
I've added filter before map function but it looks not working.
componentDidMount(){
client.getEntries({content_type:'formRequest'}).then(response => {
this.setState({qoutes: response.items});
}).catch(e => {
console.log(e);
});
}
render(){
const user = "name";
const qoutes = this.state.qoutes
.filter(i => i !== null)
.map((qoute, i) =>
user === qoute.fields.user || user === qoute.fields.company ?
<QoutesListItem id={i} key={i} qoute={qoute} />
: null)
return(<div>{qoutes}<div>)
I tried to make function like this, but it does not work as well
const qoutes = this.state.qoutes.filter((qoutes)=>{
if(qoutes !== null ) {
return qoutes;
}}).map((qoute, i) =>
How can I avoid null value and get available data (in my case only three objects)
Filter the results again to remove the null elements from the second map:
render(){
const user = "name";
const qoutes = this.state.qoutes
.filter(q => !!q)
.map((qoute, i) =>
user === qoute.fields.user || user === qoute.fields.company ?
<QoutesListItem id={i} key={i} qoute={qoute} />
:
null)
.filter(q => !!q) // Filter falsy values
return(<div>{qoutes}<div>)
Do both conditional checks in a single reduce pass. If the quote is truthy and the fields has a match user or company for user, push the JSX into result array.
const qoutes = this.state.qoutes.reduce(
(quotes, quote, i) => {
if (
quote &&
quote.fields &&
(user === quote.fields.user || user === quote.fields.company)
) {
quotes.push(<QoutesListItem id={i} key={i} qoute={quote} />);
}
return quotes;
},
[] // initial empty quotes result array
);
const quotes = [null, null, null, {
fields: {
user: 'name'
}
}, null, {
fields: {
company: 'name'
}
}];
const user = "name";
const quotesRes = quotes.reduce(
(quotes, quote, i) => {
if (
quote &&
quote.fields &&
(user === quote.fields.user || user === quote.fields.company)
) {
quotes.push(`<QoutesListItem id={${i}} key={${i}} qoute={${quote.fields.user || quote.fields.company}} />`);
}
return quotes;
}, [] // initial empty quotes result array
);
console.log(quotesRes);
FYI, anytime you see map/filter or filter/map should be a good indication to use a reduce function.
There is one very simple application that displays a list table. And there is sorting by asc and desc when you click by the title and dispaly near title word asc and desc.
If you want, you can see how it works and all code in sandbox:
https://codesandbox.io/s/mystifying-raman-btwxc
But two lines who are responsible for sorting by asc and desc written with library Lodash. But I dont need Lodash in my app.
In method onSort and componentDidMount I comment two lines where used Lodash:
class App extends React.Component {
state = {
isLoading: true,
data: [],
sort: "asc",
sortField: "id"
};
onSort = sortField => {
const clonedData = this.state.data.concat();
const sortType = this.state.sort === "asc" ? "desc" : "asc";
const orderedData = _.orderBy(clonedData, sortField, sortType); //used Lodash
this.setState({
data: orderedData,
sort: sortType,
sortField: sortField
});
};
async componentDidMount() {
const response = await fetch("/data/today.json");
const data = await response.json();
this.setState({
isLoading: false,
data: _.orderBy(data, this.state.sortField, this.state.sort) //used Lodash
});
}
render() {
return (
<div className="conatainer">
{this.state.isLoading ? (
<Loader />
) : (
<Table
data={this.state.data}
onSort={this.onSort}
sort={this.state.sort}
sortField={this.state.sortField}
/>
)}
</div>
);
}
}
How to remake this two lines, that sorting by asc and desc were implemented only on the react(without lodash)?
Lodash is a small and amazing library and provides much more than just sorting. But anyways...
React is nothing but JavaScript and that has sorting capability. You need compare of sort function in-built. Below is sort method written in JavaScript (ES6).
function sort(collection, field, direction) {
collection.sort((a, b) => {
if(field) {
a = a[field];
b = b[field];
}
if(direction === 'asc') return (a[field] > b[field]) ? 1 : -1;
else return (a < b) ? 1 : -1;
});
return collection;
}
Usage from your code snippet.
this.sort(clonedData, sortField, sortType);
Hope it helps.
Here's a one-line solution:
const sortBy = (k, d = 1) => (a, b) => (a[k] > b[k] ? 1 * d : b[k] > a[k] ? -1 * d : 0);
And here's how to use it:
const fruits = [
{ name: "banana", amount: 20 },
{ name: "apple", amount: 41 },
{ name: "kiwi", amount: 21 },
{ name: "dragonfruit", amount: 13 },
];
const sortBy = (k, d = 1) => (a, b) => (a[k] > b[k] ? 1 * d : b[k] > a[k] ? -1 * d : 0);
console.log(JSON.stringify([...fruits]));
console.log(JSON.stringify([...fruits].sort(sortBy("name"))));
console.log(JSON.stringify([...fruits].sort(sortBy("name", -1))));
And here's a fork of your CodeSandbox with the implementation working.
Hope this helps.
Cheers! 🍻
I have a problem with sorting state.
I would like to set column sorting as ascending every time when I change the selection of my column and change state as "asc" then "desc" etc.
When I clicking on the same column. Method getHeaderName get actually selected header name and it works properly. Method sortedMethod
just have sorting mechanism(works good) and set state of column name previously selected and actually selected:
private sortedMethod(columnName: HeaderNameDictionary) {
this.sortedMechanism(columnName);
this.setState({ previousColumn: this.state.sortedColumnName })
this.setState({ sortedColumnName : columnName})
}
I think that the problem is with the method "setStateSelectedColumn". In this method, I check the state of current and previous column names and depend on it I set "isAscending" state. But it does not work well.
private setStateSelectedColumn(columnName: HeaderNameDictionary) {
if ((this.state.previousColumn !== this.state.sortedColumnName )) {
this.setState({ isAscending: true })
} else {
this.setState({ isAscending: !this.state.isAscending })
}
}
When I change column and click every time on another column it works good and
change state as isAsc: true,
but when I :
change column - isAsc: true,
Click one more time on this column - isAsc: true,
Click one more time on this column - is Asc: false,
Click one more time on this column - is Asc: true,
Click one more time on this column - is Asc: false,
Second clicking on the same column is problematic and does not change the state.
<th>
Product Code
<span
className={this.getHeaderName(HeaderNameDictionary.PRODUCT_CODE)}
onClick={() => {
this.setState({}, () => {
this.setStateSelectedColumn(HeaderNameDictionary.PRODUCT_CODE);
});
this.sortedMethod(HeaderNameDictionary.PRODUCT_CODE);
}}
/>
</th>;
Do you have any idea how to fix this? Thanks!
It's better to use the setState overload that takes a function instead of the object.
Here is a simple example.
class Sample extends React.Component {
constructor() {
super();
this.initData = [
{name: 'John', family: 'Doe'},
{name: 'Jane', family: 'Hardy'}
];
this.state = {
sortField: 'name',
sortOrder: 'asc',
data: this.initData
};
}
sort(column) {
this.setState(prevState => {
if (prevState.sortField === column) {
const sortOrder = prevState.sortOrder === 'asc' ? 'desc' : 'asc';
return {
sortOrder,
data: [...prevState.data].sort((a, b) => {
const textA = a[column].toUpperCase();
const textB = b[column].toUpperCase();
if (sortOrder === 'asc') {
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
}
else {
return (textA > textB) ? -1 : (textA < textB) ? 1 : 0;
}
})
}
}
else {
return {
sortOrder: 'asc',
sortField: column,
data: [...prevState.data].sort((a, b) => {
const textA = a[column].toUpperCase();
const textB = b[column].toUpperCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
})
}
}
})
}
render() {
return (
<table>
<thead>
<th onClick={() => this.sort('name')}>Name</th>
<th onClick={() => this.sort('family')}>Family</th>
</thead>
<tbody>
{
this.state.data.map(({name, family}, i) => {
console.log(name, family)
return (
<tr key={i}>
<td>{name}</td>
<td>{family}</td>
</tr>
)
})
}
</tbody>
</table>
)
}
}
React.render(<Sample />, document.getElementById('app'));
I am having a Bootstrap-table rendering values from service called in componentDidMount().
Example of my table -
Col1 Col2 Col3
1 2 3
4 5 6
7 8 9
SumValue 15 18 //This last row holds sum of all values
How to calculate SumValue of all the values present in col1 and display in Footer.
Below is the code how i am using react-row for mapping data to rows.
And value is the variable holding data of all columns present in json file returned from service, after setting it to the component's state.
{this.state.value && this.state.value.map(function (value, ind) {
return <Row key={ind} item={value}></Row>
})
}
Initializing state
constructor(props){
super(props)
{
this.state ={
value: [], //Initializing an array
SumValue: '',
default: false,
}
}
Setting state
fetch('https://www.serviceUrl.com')
.then(res => res.json())
.then(value => {
this.setState({
value: value.empRecords, //Here its getting all records from json
default: false
});
})
Let me know guys if any more information is required.
I would get the sum using reduce:
const SumValue = this.state.value && this.state.value.reduce((a, v) => a + v, 0)
1) initial columnNames and array list
state = {
callList: [],
columnModel: [
{ columnName: "date" },
{ columnName: "totalCalls" },
{ columnName: "answeredCalls" },
{ columnName: "abandonedCalls" },
{ columnName: "abandonRate" },
{ columnName: "avgAnswerSpeed" },
]
};
2) Get data from api and prepare array data
try {
const { errors, list, success } = (await apiService.getCalls(request)).data;
if (success) {
// first list is normal numbers count only,
// if you want count avg sum for some columns send second param array list and include column names
// now i want count avg for 'abandonRate', 'avgAnswerSpeed' , others just count sum
this.setState({
callList: list,
callListSum: this.sumCount(
list,
['abandonRate', 'avgAnswerSpeed']
)
})
}
} catch (error) {
console.log(error);
}
sumCount = (list = [], avgColumns = []) => {
const sum = {};
// count numbers
list.map((item) => {
Object.entries(item).map(([key, val]) => {
if (typeof (val) === 'number') {
sum[key] = sum[key] ? (sum[key] + val) : val;
}
})
});
// count avg
avgColumns.map(key => {
if (sum[key]) {
sum[key] = sum[key] / list.length;
}
})
return sum;
}
3) Render data
<table>
<thead>
<tr style={{ backgroundColor: "#F5F7FA" }}>
{
this.state.columnModel.map((item, i) =>
<th key={i}> {item.columnName}</th>
)
}
</tr>
</thead>
<tbody>
{
this.state.callList.map(
(info, index) => (
<tr
key={index}
>
{
this.state.columnModel.map((item, i) =>
(
<td key={i}>
{info[item.columnName]}
</td>
)
)
}
</tr>
)
)}
{/* Render sum area */}
<tr
style={{ backgroundColor: "#F5F7FA" }}
>
{
this.state.columnModel.map((item, i) =>
(
<td style={{ fontWeight: "bold" }} >
{this.state.callListSum[item.columnName]}
</td>
)
)
}
</tr>
</tbody>
</table>