I'm building a shopping list web app. Items in the list can be toggled 'checked' or 'unchecked'.
My data flow is this: click on item checkbox --> send db update request --> re-render list data with new checkbox states.
If I handle the checkbox state entirely in react local component state, the user action updates are very fast.
Fast Demo: https://youtu.be/kPcTNCErPAo
However, if I wait for the server propagation, the update appears slow.
Slow Demo: https://youtu.be/Fy2XDUYuYKc
My question is: how do I make the checkbox action appear instantaneous (using local component state), while also updating checkbox state if another client changes the database data.
Here is my attempt (React component):
import React from 'react';
import ConfirmModal from '../ConfirmModal/ConfirmModal';
import {GrEdit} from 'react-icons/gr';
import {AiFillDelete, AiFillCheckCircle} from 'react-icons/ai';
import {MdRadioButtonUnchecked} from 'react-icons/md';
import './ListItem.scss';
class ListItem extends React.Component{
constructor(props){
super(props);
this.state = {
confirmOpen: false,
checkPending: false,
itemChecked: props.item.checked,
};
}
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.item.checked != prevState.itemChecked){
return ({itemChecked: nextProps.item.checked})
}
return null;
}
render(){
return (
<div className={`listItemWrapper${this.state.itemChecked ? ' checked': ''} `}>
{this.state.confirmOpen ?
<ConfirmModal
triggerClose={() => this.setState({confirmOpen: false})}
message={`Do you want to delete: ${this.props.item.content}?`}
confirm={() => {
this.clickDelete();
this.setState({confirmOpen: false});
}}
/> : null
}
<div className="listItem">
<div className='listContent'>
{ this.state.itemChecked ?
<strike>{this.props.item.content}</strike>
: this.props.item.content
}
<div className={`editBtn`}>
<GrEdit onClick={() => {
let {content, category, _id} = this.props.item;
this.props.edit({content, category, _id});
}}
/>
</div>
</div>
</div>
<div className={`listToolsWrapper`}>
<div className = "listTools">
<div onClick={() => this.setState({confirmOpen: true})} className={`tool`}><AiFillDelete className="listItemToolIcon deleteIcon"/></div>
<div onClick={() => !this.state.checkPending ? this.clickCheck() : null} className={`tool`}>
{this.state.itemChecked ? <AiFillCheckCircle className="listItemToolIcon checkIcon"/> : <MdRadioButtonUnchecked className="listItemToolIcon checkIcon"/>}
</div>
</div>
<div className = "listInfo">
<div className ="itemDate">
{this.props.item.date}
{this.props.item.edited ? <p className="edited">(edited)</p> : null}
</div>
</div>
</div>
</div>
);
}
async clickCheck(){
this.setState(prevState => ({checkPending: true, itemChecked: !prevState.itemChecked}));
await fetch(`/api/list/check/${this.props.item._id}`,{method: 'POST'});
this.setState({checkPending: false});
//fetch updated list
this.props.fetchNewList();
}
async clickDelete(){
await fetch(`/api/list/${this.props.item._id}`,{method: 'DELETE'});
//fetch updated list
this.props.fetchNewList();
}
}
export default ListItem;
I'm confused about how to properly use react lifecycle methods here. I'm using local state to mirror a component prop. I attempt to use getDerivedStateFromProps() to sync state and props, but that doesn't make the render faster.
This is what ended up working for me -- although it isn't the most elegant solution
I changed getDerivedStateFromProps to this:
static getDerivedStateFromProps(nextProps, prevState){
// Ensure that prop has actually changed before overwriting local state
if(nextProps.item.checked != prevState.itemChecked &&
prevState.prevPropCheck != nextProps.item.checked){
return {itemChecked: nextProps.item.checked}
}
return null;
}
The problem, I believe, was that getDerivedStateFromProps was overwriting the local state change every time I tried to setState() and re-render the component. The nextProps.item.checked != prevState.itemChecked
always evaluated to true, because the nextProps.item.checked
binding referenced the previous props (the props hadn't changed), but the prevState.itemChecked
binding's value had flipped.
Therefore, this function always overwrote the state with the prior prop state.
So I needed to add prevState.prevPropCheck != nextProps.item.checked
to check that the props did in fact change.
I'm not sure if this is getDerivedStateFromProps() intended usage, but a prevProps parameter seemed to be what I needed!
Please let me know if you see a more elegant solution
Thanks to @vedran for the help!