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.
The effect you are trying to accomplish is called Optimistic UI which means that when some async action happens you are faking the effect of that action succeeding.
Eg. Facebook Like functionality 1. When you click Like on Facebook post the count is automatically increased by one (you) and then in the background ajax request is sent to API. 2. When you get response from your ajax request you will know if it succeeded or failed. If it succeeded then you can choose to do nothing but if it failed you will probably want to reduce the likes count you previously added and show some error to the user (or not).
In your case, you could do the same thing but if you want to have a real-time updates you will need to create some solution like long polling or web sockets .co
Also, getDerivedStateFromProps you provided looks good.
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!
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.