rednate95
rednate95

Reputation: 53

optimistic ui updates - react

I imagine this is a basic in react but I'm not sure how to get it to work, basically when I delete, create or edit anything in my components I want the change to happen in realtime without refreshing the page, I've achieved it at some level with the search function but not entirely sure how to do with for the delete function for example:

Here is what I'm working with, how would I get this to work with my axios delete function? Thanks

import { connect } from 'react-redux';
import { fetchTournaments } from '../actions/tournaments';
import Item from './Item';
import EditTournament from './EditTournament';
import axios from 'axios';

import '../styles/Item.css';

class SearchAndDisplay extends React.PureComponent {
  componentDidMount() {
    this.props.fetchTournaments();
  }

  state = {
    searchCriteria: '',
    isLoading: false
  };

  handleChange = event => {
    this.setState({
      searchCriteria: event.target.value
    });
  };

  async handleDelete(id) {
    const url = `http://localhost:4000/tournaments/`;

    await axios
      .delete(url + id)
      .then(res => {
        console.log(res.data);
      })
      .catch(err => {
        console.log(err);
      });
  }

  formatDate(date) {
    let options = {
      year: 'numeric',
      month: 'numeric',
      day: 'numeric',
      hour: 'numeric',
      minute: 'numeric',
      second: 'numeric',
      hour12: false
    };

    let newDate = new Date(Date.parse(date));
    let format = new Intl.DateTimeFormat('default', options).format(newDate);

    return format;
  }

  handleChange = event => {
    this.setState({ searchCriteria: event.target.value });
  };

  renderList() {
    let tournmentsArray = this.props.tournaments;

    const filterTournaments = tournmentsArray.filter(item =>
      item.name.includes(this.state.searchCriteria)
    );

    if (filterTournaments === undefined || filterTournaments.length === 0) {
      return (
        <React.Fragment>
          <div className="notFound">
            Something went wrong.
            <br />
            <button
              className="notFoundButton"
              onClick={() => {
                this.setState({ searchCriteria: '' });
              }}
            >
              Retry
            </button>
          </div>
        </React.Fragment>
      );
    } else {
      return filterTournaments.map(item => (
        <Item
          key={item.name}
          name={item.name}
          organizer={item.organizer}
          participants={Object.values(item.participants)}
          game={item.game}
          start={this.formatDate(item.startDate)}
        >
          <div className="buttonBar">
            <EditTournament id={item.id} />
            <button
              className="button"
              onClick={() => {
                if (
                  window.confirm('Are you sure you want to delete this item?')
                ) {
                  this.handleDelete(item.id);
                }
              }}
            >
              Delete
            </button>
          </div>
        </Item>
      ));
    }
  }

  render() {
    return (
      <div className="container">
        <input
          onChange={this.handleChange}
          className="input"
          placeholder="Search..."
          id="searchField"
          value={this.state.searchCriteria}
        />
        <div className="row">{this.renderList()}</div>
      </div>
    );
  }
}

function mapStateToProps({ tournaments }) {
  return {
    tournaments: Object.values(tournaments).flat()
  };
}

export default connect(mapStateToProps, {
  fetchTournaments
})(SearchAndDisplay);

unlike delete the create and edit data is handled by redux like so:

Create tournament:

import { reduxForm, Field } from 'redux-form';
import '../styles/promptForms.css';
import '../styles/Header.css';
import { connect } from 'react-redux';
import { createTournaments } from '../actions/tournaments';

class CreateTournamentPromptFrom extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      showHide: false
    };
  }
  createTournamentButton() {
    return (
      <div>
        <button
          className="genericButton"
          onClick={() => this.setState({ showHide: true })}
        >
          CREATE TOURNAMENT
        </button>
      </div>
    );
  }

  renderInput = ({ input, label }) => {
    return (
      <div>
        <label>{label}</label>
        <br />
        <input className="promptInput" {...input} autoComplete="off" />
      </div>
    );
  };

  onSubmit = formValues => {
    this.props.createTournaments(formValues);
  };

  render() {
    const { showHide } = this.state;

    return (
      <React.Fragment>
        <div className={`overlay ${showHide ? 'toggle' : ''} `} />

        <div className={`promptBox ${showHide ? 'toggle' : ''} `}>
          <h3>localhost:3000 says</h3>
          <form onSubmit={this.props.handleSubmit(this.onSubmit)}>
            <Field
              name="name"
              component={this.renderInput}
              label="Enter Tournament:"
            />

            <button className="okayButton">OK</button>
          </form>
          <button
            className="cancelButton"
            onClick={() => this.setState({ showHide: false })}
          >
            Cancel
          </button>
        </div>
        {this.createTournamentButton()}
      </React.Fragment>
    );
  }
}

const formWrapped = reduxForm({
  form: 'promptForm'
})(CreateTournamentPromptFrom);

export default connect(null, { createTournaments })(formWrapped);

actions:

import {
  FETCH_TOURNAMENTS,
  FETCH_TOURNAMENT,
  CREATE_TOURNAMENT,
  EDIT_TOURNAMENT
} from './types';

import { API_TOURNAMENTS_URL } from '../constants/api';
import axios from 'axios';

export const fetchTournaments = () => async dispatch => {
  const response = await axios.get(API_TOURNAMENTS_URL);

  dispatch({
    type: FETCH_TOURNAMENTS,
    payload: response.data.flat()
  });
};

export const fetchTournament = id => async dispatch => {
  const response = await axios.get(`http://localhost:4000/tournaments/${id}`);

  dispatch({ type: FETCH_TOURNAMENT, payload: response.data });
};

export const createTournaments = formValues => async dispatch => {
  const response = await axios.post(API_TOURNAMENTS_URL, {
    ...formValues
  });

  dispatch({ type: CREATE_TOURNAMENT, payload: response.data });
};

export const editTournaments = (id, formValues) => async dispatch => {
  const response = await axios.patch(
    `http://localhost:4000/tournaments/${id}`,
    formValues
  );

  dispatch({ type: EDIT_TOURNAMENT, payload: response.data });
};

reducers:

import _ from 'lodash';

import {
  FETCH_TOURNAMENT,
  CREATE_TOURNAMENT,
  FETCH_TOURNAMENTS,
  EDIT_TOURNAMENT,
  DELETE_TOURNAMENT
} from '../actions/types';

export default (state = {}, action) => {
  switch (action.type) {
    case FETCH_TOURNAMENT:
      return { ...state, [action.payload.id]: action.payload };
    case FETCH_TOURNAMENTS:
      return { ...state, [action.payload.id]: action.payload };
    case CREATE_TOURNAMENT:
      return { ...state, [action.payload.id]: action.payload };
    case EDIT_TOURNAMENT:
      return { ...state, [action.payload.id]: action.payload };
    case DELETE_TOURNAMENT:
      return _.omit(state, action.payload);
    default:
      return state;
  }
};

Upvotes: 1

Views: 2614

Answers (2)

Quentin Grisel
Quentin Grisel

Reputation: 4987

Here is how you can do using forceUpdate() (Since you don't want to use state):

import React, { Component } from 'react';
import { render } from 'react-dom';

class App extends Component {
  constructor() {
    super();
      this.items = [
        {id: 1, name: "item 1"},
        {id: 2, name: "item 2"},
      ];

    this.handleDelete = this.handleDelete.bind(this);
  }

  handleDelete(index) {
    const newState = [...this.items];
    delete newState[index];
    // or
    // newState.splice(index, 1);
    this.items = newState;
    this.forceUpdate();
  }

  render() {
    return (
      <div>
        {
          this.items.map((item, index) => {
            return (
              <div>
                {item.name}
                <button onClick={() => this.handleDelete(index)}>Delete item</button>
              </div>
            )
          })
        }
      </div>
    );
  }
}

render(<App />, document.getElementById('root'));

Just pass the index in the map method : ...map((item, index) => ...);

Do it in the then() after your axios call.

Note that the documentation highly advice you to avoid using forceUpdate() when you can, so you really should use a state for this, I don't see any good reason for you not to use it here.

Here is a quick repro on Stackblitz.

Upvotes: 1

Drew Reese
Drew Reese

Reputation: 202801

To "optimistically" delete an item from state you'll need to immediately delete it from state to reflect the change right away in the UI. BUT you will need to add extra redux state to "hold" a pending delete with your backend. When the delete is successful you clear the held delete, if it fails you clear the held delete and add it back in to your regular data (and perhaps display some error message or toast, etc..).

I see you don't do the delete via redux, so use local component state and you'll have to filter your tournament data when rendering.

class SearchAndDisplay extends PureComponent {
  componentDidMount() {
    this.props.fetchTournaments();
  }

  state = {
    searchCriteria: "",
    isLoading: false,
    optimisticTournaments: null // <-- state to hold temp "deleted" data
  };

  handleChange = event => {
    this.setState({
      searchCriteria: event.target.value
    });
  };

  async handleDelete(id) {
    console.log("delete id", id);
    // optimistically remove element
    this.setState({
      optimisticTournaments: this.props.tournaments.filter(
        item => item.id !== id
      )
    });

    await axios
      .delete(url + id)
      .then(res => {
        console.log(res.data);
        // Need to create a call back to let parent know element was deleted
        this.props.deleteSuccess(id);
      })
      .catch(err => {
        console.log(err);
        alert("Failed to delete");
      })
      .finally(() => {
        this.setState({ optimisticTournaments: null });
      });
  }

  formatDate(date) {
    let options = {
      year: "numeric",
      month: "numeric",
      day: "numeric",
      hour: "numeric",
      minute: "numeric",
      second: "numeric",
      hour12: false
    };

    let newDate = new Date(Date.parse(date));
    let format = new Intl.DateTimeFormat("default", options).format(newDate);

    return format;
  }

  handleChange = event => {
    this.setState({ searchCriteria: event.target.value });
  };

  renderList() {
    let tournmentsArray =
      this.state.optimisticTournaments || this.props.tournaments;

    const filterTournaments = tournmentsArray.filter(item =>
      item.name.includes(this.state.searchCriteria)
    );

    if (filterTournaments === undefined || filterTournaments.length === 0) {
      return (
        <React.Fragment>
          <div className="notFound">
            Something went wrong.
            <br />
            <button
              className="notFoundButton"
              onClick={() => {
                this.setState({ searchCriteria: "" });
              }}
            >
              Retry
            </button>
          </div>
        </React.Fragment>
      );
    } else {
      return filterTournaments.map(item => (
        <Item
           key={item.name}
           name={item.name}
           organizer={item.organizer}
           participants={Object.values(item.participants)}
           game={item.game}
           start={this.formatDate(item.startDate)}
         >
          <div className="buttonBar">
            <EditTournament id={item.id} />
            <button
              className="button"
              onClick={() => {
                if (
                  window.confirm("Are you sure you want to delete this item?")
                ) {
                  this.handleDelete(item.id);
                }
              }}
            >
              Delete
            </button>
          </div>
        </Item>
      ));
    }
  }

  render() {
    return (
      <div className="container">
        <input
          onChange={this.handleChange}
          className="input"
          placeholder="Search..."
          id="searchField"
          value={this.state.searchCriteria}
        />
        <div className="row">{this.renderList()}</div>
      </div>
    );
  }
}

Edit optimistic delete

Upvotes: 1

Related Questions