When I change a value in the table, it disappears when I update the state with the new value. I have checked state and it is updated correctly but the table is now blank. I am using a bootstrap table.
Everything in state is now updated but the grid is empty, can I use useEffect to do rebind the table?
const GridEdit = () => {
const store = useContext(StoreContext);
const handleBlur = (e, arrayRow, editableFields) => {
const newVals = store.gridItems[0][0];
newVals["Status"] = e.target.innerHTML;
store.gridItems[1](newVals);
console.log("hello ", store.gridItems[0]);
};
const dataTable = store.gridItems[0];
function tableHeaders(data) {
let tableHeaders = [];
if (data.length > 0) {
let headers = Object.keys(data[0]);
headers.map((header) =>
tableHeaders.push(<th key={header}>{header}</th>)
);
}
if (tableHeaders.length === 0) {
return null;
} else return tableHeaders;
}
function tableRows(dataTable) {
let tableLength = dataTable.length;
let table = [];
for (let i = 0; i < tableLength; i++) {
let children = [];
let row = Object.values(dataTable[i]);
const readOnlyFields = row.slice(0, 4);
const editableFields = row.slice(4, 7);
readOnlyFields.map((data) => children.push(<td id={row[0]}>{data}</td>));
editableFields.map((data) =>
children.push(
<td ContentEditable="true" id={row[0]}>
{data}
</td>
)
);
table.push(
<tr key={row} onBlur={(e) => handleBlur(e, i, editableFields)}>
{children}
</tr>
);
}
if (table.length === 0) {
return null;
} else {
return table;
}
}
return (
<tbody className="tableHeaders">
<tr>{tableHeaders(dataTable)}</tr>
{tableRows(dataTable)}
</tbody>
);
};
export default GridEdit;
Related
I use to react-paginate package for paginate. After searching, the pagination pattern is broken. While there should be 5 items per page, this situation breaks down after the search. I share my ss and codes. thanks for your time.
here is default paginate:
and after search paginate:
and here is my code:
const displayUsers = (users, setUsers, userCurrentPage) => { // pagination configs
const startIndex = (userCurrentPage - 1) * 5
const endIndex = userCurrentPage * 5
const productsToDisplay = users.slice(startIndex, endIndex)
setUsers(productsToDisplay)
}
const handleSearch = (e) => { // filter script
let searchValue = e.target.value
let filteredTasks = users.filter((task) => {
return task.UserID.toLowerCase().includes(searchValue.toLowerCase())
})
setPaginationUsers(filteredTasks)
}
useEffect(() => {
if (searchfield === '') {
setPaginationUsers(users)
} else {
const dynamicFilter = users.filter((user) => {
return user.UserID.toLowerCase().includes(searchfield.toLowerCase())
})
setPaginationUsers(dynamicFilter)
}
}, [searchfield])
// And here is mapping area
{paginationUsers.map((userDetail, index) => {
const {
UserID,
Country,
City,
MMA,
Time,
Game,
Revenue,
Timezone,
Device_Model,
Ad_Type,
SubNetwork,
} = userDetail
if (typeof userDetail.Revenue === 'string') {
userDetail.Revenue = parseFloat(userDetail.Revenue).toFixed(6)
}
return (
<tr key={index}>
<td>{UserID}</td>
<td>{Country}</td>
<td>{City}</td>
<td>{MMA}</td>
<td>{SubNetwork}</td>
<td>{Time}</td>
<td>{Revenue}</td>
<td>{Game}</td>
<td>{Timezone}</td>
<td>{Device_Model}</td>
<td>{Ad_Type}</td>
</tr>
)
})}
I have a React app with selecting logic but everything in this logic breaks because of "useState()". I know that "useState()" is asynchronous and value is not assigned immediately but to work my logic value is needed immediately when I click on the row. And the question is how to set value immediately in the "setEntry()" ?
This is my code:
const TableRow = (props) => {
const [entry, setEntry] = useState('');
const array = Object.entries(props);
const navigate = useNavigate();
let previousRow;
const deleteContact = async (e) => {
e.preventDefault();
const id = e.target.getAttribute('id');
await requester(urls.accountWithId + '/' + id, methods.delete);
await requester(urls.contacts + '/' + id, methods.delete)
.then(() => {
navigate(urls.mainPage);
notificationsReceiver('Contact is deleted successfully!');
})
.catch((e) => {
alert(e.message);
});
};
const checkIsSelected = (id) => {
if (entry === id) {
setEntry('');
return;
}
setEntry(id);
};
const changeStyle = (e) => {
const currentRowId = e.currentTarget.getAttribute('id');
if (entry === '') {
e.currentTarget.style.backgroundColor = '#E3E5E7';
setEntry(currentRowId);
previousRow = e.currentTarget;
}
else if (entry === currentRowId) {
e.currentTarget.style.backgroundColor = '#F5F7FA';
setEntry('');
}
else if (entry !== currentRowId) {
setEntry(currentRowId);
e.currentTarget.style.backgroundColor = '#E3E5E7';
previousRow.style.display = '#F5F7FA';
previousRow = e.currentTarget;
}
};
const navigateToDetails = (e) => {
e.preventDefault();
const id = e.target.getAttribute('id');
navigate(urls.details + '/:' + id);
};
const editContact = async (e) => {
e.preventDefault();
const id = e.target.getAttribute('id');
navigate(urls.editContact + '/:' + id);
}
return (
<tr className={styles['contact-row']} id={array[1][1].id} onDoubleClick={navigateToDetails} key={array[1][1].id} onClick={(e) => { checkIsSelected(array[1][1].id); changeStyle(e); }}>
<td id={array[1][1].id}>{array[1][1].name}</td>
<td id={array[1][1].id}>{array[1][1].continentAndCountry}</td>
<td id={array[1][1].id}>{array[1][1].email}</td>
<td id={array[1][1].id}><a href={array[1][1].baseUrlForFreeGuyz + '/' + array[1][1].accountNameForFreeGuyz}>{array[1][1].accountNameForFreeGuyz}</a></td>
<td id={array[1][1].id}><a href={array[1][1].baseUrlForInstagram + '/' + array[1][1].accountNameForInstagram}>{array[1][1].accountNameForInstagram}</a></td>
<td id={array[1][1].id}><a href={array[1][1].baseUrlForTwitter + '/' + array[1][1].accountNameForTwitter}>{array[1][1].accountNameForTwitter}</a></td>
<td id={array[1][1].id}>{array[1][1].accountType}</td>
<td>
{entry && <>
<button className={`btn btn-warning ${styles['edit-button']}`} id={array[1][1].id} onClick={editContact}>Edit</button>
<button className={`btn btn-danger ${styles['delete-button']}`} id={array[1][1].id} type="submit" onClick={deleteContact}>Delete</button></>}
</td>
</tr>
);
}
export default TableRow;
I'll be grateful if anyone can help me.
Instead of continuously calling setSentry for state updates, you can use a local variable to check value changes and after all, you can update entry at once.
const changeStyle = (e) => {
//local variable to proceed the internal logic
let updatedEntry = e.currentTarget.getAttribute('id')
if (updatedEntry === currentRowId) {
e.currentTarget.style.backgroundColor = '#F5F7FA';
updatedEntry = ""
} else {
updatedEntry = currentRowId
e.currentTarget.style.backgroundColor = '#E3E5E7';
previousRow.style.display = '#F5F7FA';
previousRow = e.currentTarget;
}
currentRowId = e.currentTarget.getAttribute('id');
//only update `entry` state once after all
setEntry(updatedEntry)
};
For this project I am currently working on, I need to highlight the button that was clicked on each layer/row. However, the way I have right now it highlights every button that was clicked.
I need something like this:
correct highlighted path
But then when I click on the same row, it does not remove the highlight from the button that I pressed before. How can I update and reset the state of the previous button that was clicked? I tried to use the useRef hook for this but I haven't been successful so far.
wrong highlighted path
EDIT: Added code
This is the code that I have for the component of each row in the website:
function StackRow({ data, partition, level, index, onClick, getInfo, isApp }) {
const classes = useStyles({ level: level });
const rowRef = useRef();
const handleSelectedButtons = (flag, setFlag, btnRef) => {
console.log(rowRef);
};
return (
<Card
key={partition + '_' + index + '_' + level}
className={classes.card}
id={level}
ref={rowRef}
>
{data.map((field) => {
return (
<StackItem
key={partition + '_' + field[0] + '_' + level}
data={field[0]}
info={field[1]}
level={level}
onClick={onClick}
getInfo={getInfo}
isApp={isApp}
handleSelectedButtons={handleSelectedButtons}
rowRef={rowRef}
/>
);
})}
</Card>
);
}
And this is the code I have for each button of the row:
function StackItem({
data,
info,
level,
onClick,
getInfo,
isApp,
handleSelectedButtons,
}) {
const [flag, setFlag] = useState(false);
const btnRef = useRef();
const styleProps = {
backgroundColor: flag ? '#06d6a0' : level % 2 === 0 ? '#22223b' : '#335c67',
};
const classes = useStyles(styleProps);
return (
<Button
ref={btnRef}
isselected={flag.toString()}
key={data}
className={classes.button}
variant="outlined"
onClick={(event) => {
onClick(event, setFlag, btnRef);
handleSelectedButtons(flag, setFlag, btnRef);
getInfo(info, level, isApp);
}}
disableElevation={true}
>
{data}
</Button>
);
}
There are some useless variables and states there because I have been trying all sort of stuff to do this.
EDIT: Added data sample & project structure
Data looks like:
{
application: {
cmake: {
info: str,
versions: {
version_no: {
application: {...}
}
}
},
gcc: {...},
git: {...},
intel: {...},
.
.
.
}
}
The structure of the project is like:
App
L Stack
L StackRow
L StackItem
Where App is the entire application, Stack is the container for everything in the images apart from the search box, StackRow matches one row of the Stack, and StackItem is one item/button from the StackRow.
EDIT: Added Stack component
function Stack({ data, partition, getInfo }) {
const [level, setLevel] = useState(0);
const [cards, setCards] = useState([]);
const [isApp, setIsApp] = useState(true);
const [selected, setSelected] = useState([]);
const [prevLevel, setPrevLevel] = useState(-1);
const cardsRef = useRef();
const handleClick = (event, setFlag, btnRef) => {
let rows = cardsRef.current.childNodes;
let currBtn = event.target.innerText;
let curr;
for (let i = 0; i < rows.length; i++) {
let rowItems = rows[i].childNodes;
for (let j = 0; j < rowItems.length; j++) {
if (currBtn === rowItems[j].textContent) {
curr = rowItems[j].parentElement;
}
}
}
let id;
for (let i = 0; i < rows.length; i++) {
if (curr.textContent === rows[i].textContent) {
id = i;
}
}
if (level === id) {
if (id % 2 === 0) {
setIsApp(true);
if (selected.length === 0) {
setSelected([...selected, data[currBtn].versions]);
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(level + 1);
} else {
let newSelected = selected.slice(0, id);
if (id % 2 === 0) {
setIsApp(true);
if (newSelected.length === 0) {
setSelected([...newSelected, data[currBtn].versions]);
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(id + 1);
}
setFlag(true);
};
useEffect(() => {
let fields = [];
let lastSelected = selected[selected.length - 1];
if (level % 2 !== 0) {
fields = Object.keys(lastSelected).map((key) => {
let path = lastSelected[key].path;
let module = lastSelected[key].module_name;
let info = 'module: ' + module + ' path: ' + path;
return [key, info];
});
} else {
if (selected.length !== 0)
fields = Object.keys(lastSelected).map((key) => {
let info = lastSelected[key].info;
return [key, info];
});
}
if (fields.length > 0) {
if (level > prevLevel) {
setCards((prevState) => [...prevState, fields]);
} else {
setCards((prevState) => [
...prevState.slice(0, selected.length),
fields,
]);
}
}
}, [selected, level, prevLevel]);
useEffect(() => {
let fields = Object.keys(data).map((key) => {
let info = data[key].info;
return [key, info];
});
setCards([fields]);
setLevel(0);
}, [data]);
useEffect(() => {
setLevel(0);
setPrevLevel(-1);
setSelected([]);
}, [partition]);
if (cards) {
return (
<div ref={cardsRef}>
{cards.map((card, index) => (
<StackRow
data={card}
partition={partition}
level={index}
index={cards.indexOf(card)}
onClick={handleClick}
getInfo={getInfo}
isApp={isApp}
/>
))}
</div>
);
} else return null;
}
EDIT: Added data sample
{
cmake: {
info: "A cross-platform, open-source build system. CMake is a family of tools designed to build, test and package software.",
versions: {
"3.17.3": {
child: {},
module_name: "cmake/3.17.3",
path: "/opt/apps/nfs/spack/var/spack/environments/matador/modules/linux-centos8-x86_64/Core/cmake/3.17.3.lua",
version_no: "3.17.3"
}
}
},
gcc: {
info: "...",
versions: {
"8.4.0": {
child: {
cmake: {...},
cuda: {...},
cudnn: {...},
openmpi: {...},
.
.
.
},
module_name: "...",
path: "...",
version_no: "..."
}
"9.3.0": {...},
"10.1.0": {...}
}
}
}
It must be something really silly I do wrong here. useEffect() works perfectly with MonthModificatorHandler but not re-render when using dayClick. When dayclick was only adding days re-render worked properly. After adding logic to remove days already in state re-rendering stopped. I can call saveChanges and loadTimeline to fix functionality but if you click few days in a row asynchronous call leads to unexpected results. Thanks for your time.
export default function DatePicker(props) {
const classes = useStyles();
const theme = useTheme();
const [monthModificator, setMonthModificator] = React.useState(0);
const [monthMatrix, setMonthMatrix] = React.useState([]);
const [selectedDates, setSelectedDates] = React.useState([]);
const MonthModificatorHandler = value => {
setMonthModificator(monthModificator + value);
};
const dayClick = day => {
let data = selectedDates;
let addDay = true;
if (data.length === 0) {
data.push(day);
} else {
data.map((date, index) => {
if (day.equals(date)) {
data.splice(index, 1);
addDay = false;
}
});
if (addDay) {
data.push(day);
}
}
setSelectedDates(data);
// saveChanges();
// loadTimeline();
};
let now = DateTime.local().plus({ months: monthModificator });
let firstDayOfFirstWeek = now.startOf("month").startOf("week");
let lastDayOfLasttWeek = now.endOf("month").endOf("week");
let monthToDisplay = Interval.fromDateTimes(
firstDayOfFirstWeek,
lastDayOfLasttWeek
);
function loadTimeline() {
axios.get(`/timeline`).then(response => {
let selectedDays = [];
response.data.map(date => {
selectedDays.push(DateTime.fromISO(date));
});
setSelectedDates(selectedDays);
});
}
useEffect(() => {
let load = true;
if (load) {
loadTimeline();
load = false;
}
var matrix = [];
for (let v = 0; v < monthToDisplay.length("day"); v++) {
matrix.push(firstDayOfFirstWeek.plus({ day: v }));
}
setMonthMatrix(matrix);
}, [selectedDates, monthModificator]);
function saveChanges() {
let arrayOfDataObjects = selectedDates;
let arrayOfDataStrings = arrayOfDataObjects.map(singleDataObject => {
return (
"," +
JSON.stringify(singleDataObject.toISODate()).replaceAll('"', "") // extra quotes removed
);
});
axios.post(`/timeline`, {
timeline: arrayOfDataStrings
});
}
return (
<Grid container justify="space-around">
<Button onClick={() => MonthModificatorHandler(1)}>+</Button>
<Button onClick={() => MonthModificatorHandler(-1)}>-</Button>
<Card className={classes.root}>
{monthMatrix.map((day, index) => {
let color = "secondary";
selectedDates.map(workingDay => {
if (day.equals(workingDay)) {
color = "primary";
}
});
return (
<Button
color={color}
variant="contained"
onClick={() => dayClick(day)}
className={classes.days}
key={index}
>
{day.day}
</Button>
);
})}
</Card>
<Button onClick={() => saveChanges()}>Save Changes</Button>
<Button onClick={() => loadTimeline()}>Update Changes</Button>
</Grid>
);
}
Maybe the problem is that you compute new state from previous state. It should be done with callback https://reactjs.org/docs/hooks-reference.html#functional-updates
Try something like
const dayClick = day => setSelectedDates((_data) => {
let data =[..._data];
let addDay = true;
if (data.length === 0) {
data.push(day);
} else {
data.map((date, index) => {
if (day.equals(date)) {
data.splice(index, 1);
addDay = false;
}
});
if (addDay) {
data.push(day);
}
}
return data
})
Answered by Kostya Tresko, thank you. On top of that, another mistake was in the hook itself. The way I loaded data caused re rending loop.
if (load) {
loadTimeline();
load = false;
}
DO NOT DO THAT
the following code doesn't update my component.
the state is updated with another function. so I'd assume the component would update aswell.
Entire Class is here.
class QuestionList extends Component
{
constructor(props)
{
super(props);
this.state = {
questions : []
}
this.childHandler = this.childHandler.bind(this);
}
updateData()
{
const get = '/application/questions/'
api.get(get)
.then(response => {
console.log(response);
this.setState({questions : response.data});
})
.catch(err => console.log(err));
}
componentDidMount(){
var tempArray = [];
const get = '/application/questions/'
api.get(get)
.then(response => {
console.log(response);
this.setState({questions : response.data});
})
.catch(err => console.log(err));
}
childHandler( update )
{
const {questions} = this.state;
let tempQs = questions;
const length = questions.length;
var temp = [];
var temp1 = [];
console.log ( tempQs );
for(var i = 0; i < length; i++)
{
if(questions[i].q_id == update[1])//find New
{
temp = [questions[i].q_id,questions[i].question];
for(var x = 0; x < length; x++)//find old
{
if(questions[x].q_id == update[0] && questions[x].q_id != questions[i].q_id )
{
temp1 = [questions[x].q_id,questions[x].question];
break;
}
}
break;
}
}
tempQs[temp[0]-1].question = temp1[1];
tempQs[temp1[0]-1].question = temp[1];
this.setState({questions : tempQs},console.log(questions));
}
render()
{
var { questions } = this.state;
console.log(questions);
var qs;
qs = questions.map(val => {
return(
<QuestionCards q_id={val.q_id} max={questions.length} action={this.childHandler}>{val.question}</QuestionCards>
)
});
return(
<Table hover>
<tbody>
<tr className="title">
<th>Id</th>
<th>Question</th>
<td colspan="3" ><Button color="primary">Add Question</Button></td>
</tr>
{qs}
</tbody>
</Table>
);
}
}
here is the cards component
class QuestionCards extends Component
{
constructor ( props )
{
super(props)
this.state = {
fireModal : false,
modal : false,
q_id : this.props.q_id,
question : this.props.children,
max : this.props.max
}
this.handleClick = this.handleClick.bind(this);
this.handleModal = this.handleModal.bind(this);
this.triggerModal = this.triggerModal.bind(this);
this.moveUp = this.moveUp.bind(this);
this.moveDown = this.moveDown.bind(this);
}
triggerModal ( trig )
{
const {q_id} = this.state;
if (trig)
return (
<QListModal q_id={q_id} trigger={trig} action={this.childHandler}/>
);
}
handleModal ( val )
{
const { fireModal } = this.state;
console.log('fireModel: ' + fireModal)
if( !fireModal )
{
this.setState({
mTarget : val,
fireModal : true ,
update : []
});
}
else
{
this.setState({fireModal:false})
}
}
moveUp ()
{
var tempArray = [];
const { q_id } = this.state;
const dir = 'up';
const get = '/application/move/' + q_id +'/'+ dir;
api.get(get).then(res => {
console.log(res);
this.setState({
update : [res.data.newId,res.data.oldId]
})
return this.props.action(this.state.update);
});
//return this.props.action(this.state.update);
}
moveDown ()
{
var tempArray = [];
const { q_id } = this.state;
const dir = 'down';
const get = '/application/move/' + q_id +'/'+ dir;
api.get(get).then(res => {
this.setState({
update : [res.data.newId,res.data.oldId]})
return this.props.action(this.state.update);
});
//return this.props.action();
}
render()
{
const {
fireModal,
q_id,
question,
max,
update
} = this.state
let ButtonUp;
let ButtonDown;
if( q_id <= 1)
{
ButtonUp = <td></td>
}
else
{
ButtonUp = <td><Button id={q_id} onClick={this.moveUp}>▲</Button></td>
}
if( q_id == max)
{
ButtonDown = <td></td>
}
else
{
ButtonDown = <td><Button id={q_id} onClick={this.moveDown}>▼</Button></td>
}
return(
<tr>
<th>{q_id}</th>
<td>{question}</td>
<td><Button onClick={this.handleModal}>Edit</Button></td>
{ButtonUp}
{ButtonDown}
{this.triggerModal(fireModal)}
</tr>
)
}
}
render()
{
var { questions } = this.state;
var qs = questions.map(val => {
return(
<QuestionCards q_id={val.q_id} max={questions.length} action={this.childHandler}>{val.question}</QuestionCards>
)
});
return(
<Table hover>
<tbody>
{qs}
</tbody>
</Table>
);
}
}
what the app is trying to do is every time the up or down arrow is pressed. it updates it on the page.
For some reason after updating the state it doesn't update the output itself.
though when i console.log the state it self it is updated.
this is my first independent project I'm still learning React/Javascript as a whole.
as you can see the state updates properly. but just doesn't re render anything.
the profiler tool in react-dev-tools outputs nothing rendered. could it be because of the parent component?
Solution
My problem was with the constructor for question cards.
super(props)
this.state = {
fireModal : false,
modal : false,
q_id : this.props.q_id,
question : this.props.children, // This line in particular
max : this.props.max
}
I wasn't updating the state with the new Info.
so i just assign the value of this.props.children to a constant in the render function
this is the updated render for QuestionCards
render()
{
const {
fireModal,
q_id,
max
} = this.state
const question = this.props.children;
let ButtonUp;
let ButtonDown;
if( q_id <= 1)
{
ButtonUp = <td></td>
}
else
{
ButtonUp = <td><Button id={q_id} onClick={this.moveUp}>▲</Button></td>
}
if( q_id == max)
{
ButtonDown = <td></td>
}
else
{
ButtonDown = <td><Button id={q_id} onClick={this.moveDown}>▼</Button></td>
}
return(
<tr>
<th>{q_id}</th>
<td>{question}</td>
<td><Button onClick={this.handleModal}>Edit</Button></td>
{ButtonUp}
{ButtonDown}
{this.triggerModal(fireModal)}
</tr>
)
}
also removed the console logs to declutter the post!
Thank you all for helping me trouble shoot!
Issue
Looks like a state mutation in cardHandler
childHandler( update ) {
const {questions} = this.state;
let tempQs = questions; // <-- saved current state ref to tempQs
...
tempQs[temp[0]-1].question = temp1[1]; // <-- mutated state ref
tempQs[temp1[0]-1].question = temp[1]; // <-- mutated state ref
this.setState({questions : tempQs},console.log(questions)); // <-- saved state ref
}
Solution
Shallow copy questions into new array reference to update. This should allow react's state/props reference test to detect that state is a new object and rerender.
const {questions} = this.state;
let tempQs = [...questions]; // <-- spread existing array into new array