On page load, I am using UseEffect to take an object array files. The array is mapped through, if it is male it is added to maleArray, if it is female it is added to femaleArray. At the end, i use console.log to check if the new arrays have filled and they have. I also set hasLoaded to true after this.
const[hasLoaded, setHasLoaded] = useState();
var maleArray= [];
var femaleArray= [];
useEffect(() => {
getPersonGender();
}, [props.files]);
const getPersonGender = () => {
if(!props.files.length > 0){
console.log("WAITING");
}else{
props.files.map( person => {
if(person.gender=== "male"){
maleArray.push(person);
}else{
femaleArray.push(person);
}
});
console.log(maleArray)
console.log(femaleArray)
setHasLoaded(true);
}
}
However when I get to the return statement, the new arrays I filled seem to be empty. For example, I am trying to map the maleArray to a table. I check hasLoaded and it is true but when I get to print nothing displays and I can't figure out why. When I attempted to debug, it seemed that maleArray and femaleArray never filled, but however, I am getting the console.log results saying that they are.
{!hasLoaded
?<div class="spinner-border" role="status"></div>
:
<table class="table">
<tbody>
{maleArray.map(male=> (
<tr>
<td>{male.name}</td>
<td>{male.age}</td>
</tr>
))}
</tbody>
</table>
}
Any reason why the new arrays are not being filled, yet i see the output on console.log? Thanks
The main reason it did not work is that the state was not set. You need to use the useState() hook to assign it to arrays and then use them further
Complete working example
import { useState, useEffect } from "react";
function ConfigurationList(props) {
const [hasLoaded, setHasLoaded] = useState();
// usage of hooks
const [maleArray, setMaleArr] = useState([]);
const [femaleArray, setFemaleArr] = useState([]);
useEffect(() => {
getPersonGender();
}, [props.files]);
const getPersonGender = () => {
let maleBuf = [];
let femaleBuf = [];
if (!props.files.length > 0) {
console.log("WAITING");
} else {
props.files.map((person) => {
if (person.gender === "male") {
maleBuf.push(person);
} else {
femaleBuf.push(person);
}
});
console.log(maleArray);
console.log(femaleArray);
// Setting the value to the state
setFemaleArr([...femaleBuf])
setMaleArr([...maleBuf])
setHasLoaded(true);
}
};
return (
<div>
<div class="card rounded-0">
<div class="card-header bg-dark custom-font rounded-0">Male</div>
<div class="card-body text-center">
{!hasLoaded ? (
<div class="spinner-border" role="status"></div>
) : (
<table class="table">
<thead>
<tr>
<th style={{ width: "30%" }} scope="col">
Name
</th>
<th style={{ width: "30%" }} scope="col">
Age
</th>
</tr>
</thead>
<tbody>
{maleArray.map((male) => (
<tr>
<td>{male.name}</td>
<td>{male.age}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
);
}
export default ConfigurationList;
useState :
You will have to make sure that the array is not directly updated, because that will not render the data, So you should create a local variable and then assign it to the state at the end, or you can use the spread operator and then assign them, Both should work.
I think you have typo in {maleArray.map(male=>
Shouldn't it be { maleArray.map((male) => { rest of code }
Related
The edited task reflects on browser only when I delete an existing task or add a new one.
The edited task is even reflected in the prompt as the pre-existing task, but the edited text is not reflected in the task.
import * as React from 'react';
import Card from 'react-bootstrap/Card';
import Add from './Add';
import List from './List';
import Table from 'react-bootstrap/Table';
const Main = () => {
const [listData, setListData] = React.useState([]);
const listDataMani = (text) => {
const listDataObj = {
id: listData.length + 1,
text: text,
}
const finalList = [...listData, listDataObj]
setListData(finalList);
}
const listDataDelete = (id) => {
const finalData = listData.filter(function (el) {
if (el.id === id) {
return false;
} else {
return true;
}
})
setListData(finalData);
}
const editTaskHandler = (t, li) => {
let compData = listData; // this is the function to update text
for (let i = 0; i < listData.length; i++) {
if (listData[i].id === li) {
listData[i].text = t;
} else {
return;
}
}
setListData(compData);
}
return (
<><div className='container'>
<div className='col-lg-12'>
<div className='main-component'>
<div className='title'>
<Card style={{ marginTop: "10em" }}>
<Card.Body>
<Card.Title>My Todo List</Card.Title>
<Card.Subtitle className="mb-2 text-muted">Manages Time</Card.Subtitle>
<Add listDataMani={listDataMani} />
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>Task Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<List callback={listDataDelete} editTask={editTaskHandler} list={listData} />
</tbody>
</Table>
</Card.Body>
</Card>
</div>
</div>
</div>
</div></>
)
}
export default Main;
import * as React from 'react';
const List =(props)=>{
const deleteHandler =(id)=>{
props.callback(id);
}
const editRequestHandler =(data)=>{
let editedText = prompt("Edit Your Task", data.text);
props.editTask(editedText, data.id);
}
return (
<>
{props.list.map((el)=>(<tr>
<td>{el.id}</td>
<td>{el.text}</td>
<td>
<button onClick={function(){
deleteHandler(el.id)
}}>X</button>
<button onClick={()=>{editRequestHandler(el)}}>✍</button>
</td>
</tr>))}
</>
)
}
export default List;
The edited task reflects on browser only when I delete an existing task or add a new one.
The edited task is even reflected in the prompt as the pre-existing task, but the edited text is not reflected in the task.
You are modifying the internals of an object/array without changing its referencial identify.
setState operations only do anything if when React compares the old data to the new, it has changed. In the case of arrays and objects, they are compared by reference (as opposed to numbers, strings, and other primitives which are compared by value).
To set the state using a modified object, you need to reconstruct it into a new object.
Here is a demo of the issue: https://codesandbox.io/s/setstate-unchanged-h249v3?file=/src/App.js
Notice how one button prints to console, while the other doesn't.
You could try doing this:
const editTaskHandler = (t, li) => {
setListData(
listData.map((item) => {
if (item.id === li) {
return { ...item, text: t };
}
return item;
})
);
};
Developers give me the headings of the table:
const CustomersTable = () => {
var headers=<>
<th>Name</th>
<th>Age</th>
<th>Another text</th>
</>
return <Table
headers={headers}
/>
}
And this is the code of the Table component:
const Table = ({headers}) => {
var clonedHeaders = React.Children
.toArray(headers.props.children)
.map(header => React.cloneElement(header, {
className: "text-gray-900 py-3 font-light text-xs"
}));
return <table>
<thead>
<tr>
{clonedHeaders}
</tr>
</thead>
</table>
}
I can use React.cloneElement to add attributes to the elements I receive as props of my component.
However, I want to be able to change the text content of those received elements too.
For example, I want to call my locale translation function on table header elements, automatically. Right now, if developers want to make their tables multi-lingual, they should write this:
var headers = <>
<th>{t('Name')}</th>
<th>{t('Age')}</th>
<th>{t('Other text')}</th>
</>
I want to centralize that t(text) function for all headers prop. Can I do that?
You can use the same technique on the child elements of the headers as you do on the headers themselves:
const clonedHeaders = React.Children
.toArray(headers.props.children)
.map(header => React.cloneElement(header, {
className: "text-gray-900 py-3 font-light text-xs",
children: React.Children.toArray(header.props.children).map(child => {
return typeof child === "string" ? t(child) : child;
})
}));
Live Example:
const {useState} = React;
function t(english) {
// Just so we can see that it happens
return english.toLocaleUpperCase();
}
const CustomersTable = () => {
var headers=<React.Fragment>
<th>Name</th>
<th>Age</th>
<th>Another text</th>
</React.Fragment>;
return <Table
headers={headers}
/>;
};
const Table = ({headers}) => {
const clonedHeaders = React.Children
.toArray(headers.props.children)
.map(header => React.cloneElement(header, {
className: "text-gray-900 py-3 font-light text-xs",
children: React.Children.toArray(header.props.children).map(child => {
return typeof child === "string" ? t(child) : child;
})
}));
return <table>
<thead>
<tr>
{clonedHeaders}
</tr>
</thead>
</table>;
};
ReactDOM.render(<CustomersTable />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
That example doesn't do any recursion, so it won't handle <th><span className="something">Name</span></th>. If you want to handle that, you'll have to write a recursive function to handle it, but it'll be along the same lines.
I am trying to filter through an array so that when a certain tab is clicked, only those results show. I have managed to isolate the certain variables I want to remain but the others that don't fit the criteria still remain. How do I get the filter method to actually render on the page so that the results can be shown. I have searched for hours for and have tried to get the tbody and sort through that but I just get confused as I am mostly new to javascript and react. Can someone point me in the right direction?
Filter Method
const tbody = document.getElementsByTagName('tbody')
console.log(tbody)
//change active class
function addTabBackground() {
const tabs = document.querySelectorAll('[data-tab]')
window.onload = function () {
tabs.forEach(tab => {
tab.addEventListener('click', () => {
if (tab.getAttribute('data-tab') === 'gains') {
listOfOptions.map(option => {
console.log(option.totalProfit)
})
}
tabs.forEach(tab => {
tab.classList.remove('active')
})
tab.classList.add('active')
})
})
}
}
<div className="outputs" >
<table>
<thead>
<tr>
<th>Date</th>
<th>Stock Name</th>
<th>Price Of Option</th>
<th>Number Of Options</th>
<th>Total Amount Spent</th>
<th>Option Sold At</th>
<th>Amount Of Options Sold</th>
<th>Proft</th>
</tr>
</thead>
{listOfOptions.map(option => {
return (
<tbody>
<tr>
<td>{option.clock}</td>
<td>{option.name.toUpperCase()}</td>
<td>${option.price}</td>
<td>{option.amountOfOptions}</td>
<td>${option.totalAmountSpent.toFixed(2)}</td>
<td>${option.optionPriceSoldAt}</td>
<td>{option.amountOfOptionsSold}</td>
<td style={{ color: option.totalProfit >= 0 ? 'green' : 'red' }}>${option.totalProfit.toFixed(2)}</td>
</tr>
</tbody>
)
})}
</table>
</div>
I have used React-Bootstrap-v5 to get Nav, Nav.Item with eventKey and then passed selectedKey in its onSelect function to change the tabs.
Then once we get the data inside my data variable, I used the map function to go over the array. Inside Map Function I have used the required condition to filter the elements i.e. variable = 'Open' or 'Live'.
This will only show the Open type in Open Tab and Live Type Data inside Live Tab.
Hope it's clear to you.
import React, { useEffect, useState } from 'react';
const TestSeries = () => {
// Declare Variable for data to be fetched from API
const [data, setData] = useState([]);
const fetchTestData = async () => {
const response = await axios.get(site_ip + '/apiLink');
setData(response.data.Content);
};
useEffect(() => {
fetchTestData();
}, []);
// State Variable to keep track of active tab
const [activeTab, setActiveTab] = useState('Open');
return (
<>
<Nav
activeKey={activeTab}
fill
variant="pills"
onSelect={(selectedKey) => {
setActiveTab(selectedKey);
}}
>
<Nav.Item>
<Nav.Link eventKey="Open">Open</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="Live">Live</Nav.Link>
</Nav.Item>
</Nav>
<br />
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>Column1</th>
<th>Column2</th
<th>Column3</th>
<th>Column4</th>
<th>Data Type</th>
</tr>
</thead>
<tbody>
{data.map((item, index) =>
// Condition by which data will be filtered
item.data_type == activeTab ? (
<tr>
<td>{index + 1}</td>
<td>{item.col1}</td>
<td>{item.col2}</td>
<td>{item.col3} </td>
<td>{item.col4}</td>
<td>{item.data_type}</td>
</tr>
) : null
)}
</tbody>
</Table>
</>
);
};
export default TestSeries;
Result
Assuming from the comment that you have something vaguely looking like:
function Page() {
return (
<>
<Navbar />
<Table />
</>
)
}
What you need to do is to store the current tab in a state, and pass this state down to the Table component so that you can use a Array.filter when rendering your table.
function Page() {
const [activeTab, setActiveTab] = useState(DEFAULT_ACTIVE_TAB)
return (
<>
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} />
<Table activeTab={activeTab} />
</>
)
}
Your Navbar component should have a onClick handler where it is calling the setActiveTab function when the active tab change.
Then in your Table component you should have something like this:
function Table({ activeTab }) {
return (
<table>
...
{listOfOptions
.filter(option => /* something depending on the activeTab */)
.map(option => <... />)
}
</table>
}
I want to populate an array before render, and the render is conditional based on the length of the array.
If the length of the array > 0, do the render, otherwise don't.
But it seems that the length is always 0 becasue useEffect is asynchronous, so the render is never done.
Below is my code:
function OrderDetail(props) {
const {userInfo} = useSelector(state => state.user)
const [order, setOrder] = useState({});
const [items, setItems] = useState([]);
useEffect(()=>{
if(!userInfo){
props.history.push("/")
return
}
const fetchOrderAndItems = async () => {
// Get an order from backend.
const order = await axios.get("/api/orders/" + props.match.params.id, {
headers:{
authorization: "Bearer " + userInfo.token
}
});
const order_data = order.data
setOrder(order_data)
// Take out the id of all items in the order, retrieve each product from backend,
//push them in "items_array" and set it in the "items" state.
let items_array = [];
order_data.orderItems.forEach( async (orderItem) => {
const {data} = await axios.get("/api/products/" + orderItem.productId);
items_array.push(data);
})
console.log("items_array.length: " + items_array.length);
console.log(items_array);
setItems(items_array);
}
fetchOrderAndItems();
}, [userInfo]);
// For each item in the "items" state, return a <tr> element.
const itemsHtml = items.map(item => {
return <tr key={item._id}>
Something to display.
</tr>
})
// if itemsHtml.length > 0, render {itemsHtml}, otherwise display "Your cart is empty.".
// But what is displayed is always "Your cart is empty."
return <table className="table text-center">
<thead style={{border: "none"}}>
<tr>
<th style={{border: "none"}} scope="col"></th>
<th style={{border: "none"}} scope="col">Name</th>
<th style={{border: "none"}} scope="col">Price</th>
<th style={{border: "none"}} scope="col">Qty</th>
<th style={{border: "none"}} scope="col">Subtotal</th>
<th style={{border: "none"}} scope="col"></th>
</tr>
</thead>
{
itemsHtml.length > 0?
<tbody>
{itemsHtml}
</tbody>
:
<tbody>
<tr className="text-center">
<th className="text-center" scope="row" colSpan="6">Your cart is empty. </th>
</tr>
</tbody>
}
</table>
The output of the 2 lines of "console.log" is:
We can see that the length of items_array is 0 while there is content in it, and I think it is because it is asynchronous.
And I guess because the length is 0, so the conditional render of itemsHtml.length > 0? is never true, so the desired content is never rendered.
Could anyone teach me how to solve it?
Thanks
Instead of for-each'ing over the items you should map the item ids to an array of GET requests and Promise.all them. The forEach doesn't wait for the callbacks to complete, this is why the array is empty when you log it but become populated by the time the log is displayed.
Promise.all will wait for all mapped requests to resolve before it resolves with an array of resolved item values.
Promise.all(
order_data.orderItems.map(
({ productId }) => axios.get(`/api/products/${productId}`)
))
.then(results => setItems(results));
I'm finally starting to understand how to pass and retrieve data using React. But I have one problem, I have this click handler this.SortASC when I click on the title I would like to sort titles based on alphabetic order.
I'm struggling to get this to work.. any idea how i can fix this?
Thanks in advance.
My code:
import React, { Component } from 'react';
import { getMovies } from '../services/fakeMovieService';
class Movies extends Component {
state = {
movies: getMovies(),
};
handleDelete = movie => {
const updateMovies = this.state.movies.filter(m => m._id !== movie._id); // Display all movies but not the one selected.
this.setState({
movies: updateMovies,
});
};
SortASC = () => {
console.log('Sorted');
};
render() {
return (
<React.Fragment>
{this.state.movies.length > 0 ? (
<div className="m-2">
<p>
Showing {this.state.movies.length} in the database.
</p>
<table className="table table-striped table-dark">
<thead>
<tr>
<th scope="col" onClick={this.SortASC}>
Title
</th>
<th scope="col">Genre</th>
<th scope="col">Stock</th>
<th scope="col">Rate</th>
<th scope="col"> </th>
</tr>
</thead>
<tbody>
{this.state.movies.map(movie => {
const {
_id,
title,
genre,
numberInStock,
dailyRentalRate,
} = movie;
return (
<tr key={_id}>
<th>{title}</th>
<td>{genre.name}</td>
<td>{numberInStock}</td>
<td>{dailyRentalRate}</td>
<td>
<button
onClick={() =>
this.handleDelete(movie)
}
className="btn btn-danger btn-sm">
Delete
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<h4 className="m-4">
There are no movies in the database.
</h4>
)}
</React.Fragment>
);
}
}
export default Movies;
You can just sort the movies array in state in SortASC.
Like this:
SortASC = () => {
const { movies } = this.state;
this.setState({
movies: movies.sort((a, b) => a.title > b.title ? 1 : -1)
})
};
And if you want to sort descending, you can swap the 1 and -1.
you want something like: arr.sort((titleA, titleB) => titleA - titleB)
although it can get a little more complex (checking for same/duplicate titles, etc)