简体   繁体   中英

React.js, how to render users action instantaneously (before waiting for server propagation)

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM