Cin88
Cin88

Reputation: 513

React & Redux CRUD: Displaying a single item not working

EDIT:

Tournament Reducer:

const initialState = {
    tournaments: [],
    showTournament: "",
    loading: false,
};

export default function(state = initialState, action) {
    switch(action.type) {
        case GET_TOURNAMENTS:
            return {
                ...state,
                tournaments: action.payload,
                loading: false
            };
        case SHOW_TOURNAMENT:
            return {
                ...state,
                // showTournament: state.tournaments.find(tournament => tournament._id === action.payload._id),
                showTournament: [action.payload, ...state.showTournament],
                loading: false
            };

Tournament Action:

export const showTournament = _id => dispatch => {
    dispatch(singleTourneyLoading());   
    axios
        .get(`/tournaments/${_id}`)
        .then(res => dispatch({
            type: SHOW_TOURNAMENT,
            payload: res.data
        }))
        .catch(err => dispatch(returnErrors(err.response.data, err.response.status)));
};

ShowTournament component

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import TournamentRules from './rulesets';
import {
    showTournament,
    addParticipant,
    closeTournament,
    shuffleParticipants
} from '../../actions/tournamentActions';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { TournamentSignUp, StartTournament } from './buttons';
import { Button, Spinner } from 'reactstrap';
import moment from 'moment';
import { Redirect } from 'react-router-dom';

class TournamentShow extends Component {
    constructor(props) {
        super(props);
        this.onSignUp = this.onSignUp.bind(this);
        this.onStartTournament = this.onStartTournament.bind(this);
        this.state = {
            showTournament: "",
            redirectToStart: false
        };
    };

    componentDidMount() {
        const _id = this.props.match.params.id;
        // Tried this, using props from state
        this.props.showTournament(_id);
        // Tried this, using local state
        const { tournaments } = this.props.tournament;
        const showTournament = tournaments.find(tournament => tournament._id === _id);
        this.setState({showTournament});
        console.log(
      this.props.tournament
    )
    };

    static propTypes = {
        tournament: PropTypes.object.isRequired,
        auth: PropTypes.object.isRequired
    };

    onSignUp(tournamentId, user) {
        this.props.addParticipant(tournamentId, user);
    };

    onStartTournament(tourneyId, tourneyParticipants) {
        this.props.shuffleParticipants(tourneyId, tourneyParticipants);
        this.props.closeTournament(tourneyId);
        this.setState({
            redirectToStart: true
        });
    };

    render() {
        const { _id, title, type, hostedBy, schedule, status, participants } = this.state.showTournament;
        // const { _id, title, type, hostedBy, schedule, status, participants } = this.props.tournament.showTournament;
        const { isAuthenticated, user } = this.props.auth;
        const redirectToStart = this.state.redirectToStart;
        if(!this.state.showTournament) {
            return <h1 style={{color: "lightgrey"}}>Tournament not found!</h1>
        };
        return (
            <div>
                { redirectToStart ? <Redirect to={`/tournaments/${_id}/start`} /> : null }
                { this.props.tournament.loading ?
                    <Spinner color="light" /> :
                    <div style={{color: "lightgrey"}}>
                        <h1 className="text-center">
                            { title }
                        </h1>
                        <h1 className="text-center" style={{fontSize:'1.2em'}}>Hosted by { hostedBy }</h1>
                        <hr style={{backgroundColor:"lightgrey"}} />
                        <h4>
                            Ruleset: { type }
                        </h4>
                        <h4>
                            <TournamentRules key={_id} type={ type } />
                        </h4>
                        <br/>
                        <h4>
                            Begins { moment(schedule).format("dddd, MMMM Do YYYY") }
                            <p>{ moment(schedule).format("h:mm a") }</p>
                        </h4>
                        <br />
                        <p className="text-center" style={{color: "#56A8CBFF", fontSize: "2em"}}>
                            ~ { status } for registration ~
                        </p>
                        <h4 className="text-left mt-5">
                            {
                                participants && participants.length === 1 ? 
                                `${participants && participants.length} Registered Fighter` : 
                                `${participants && participants.length} Registered Fighters`
                            }
                        </h4>
                        <ul>
                            {
                                participants && participants.map(participant => (
                                    <li key={participant._id} className="text-left" style={{fontSize: "1.1em"}}>{participant.username}</li>
                                ))
                            }
                        </ul>
                        {
                            isAuthenticated ?
                            <div>
                                <TournamentSignUp
                                    participants={participants}
                                    userId={user._id}
                                    onClick={() => this.onSignUp(_id, user)} 
                                />
                            </div> :
                            <Button block disabled>Log in to sign up for this tournament</Button>
                        }
                        {
                            isAuthenticated && user.username === hostedBy ?
                            <div>
                                <StartTournament
                                    participants={participants}
                                    onClick={() => this.onStartTournament(_id, participants)}
                                />
                            </div> :
                            null
                        }
                    </div>
                }
                <br /><Link to="/">Back to Tournaments main page</Link>
            </div>
        )
    }
};

const mapStateToProps = state => ({
    tournament: state.tournament,
    auth: state.auth
});

export default connect(mapStateToProps, 
    { showTournament, addParticipant, closeTournament, shuffleParticipants }
)(TournamentShow);

BackEnd for Show One tournament

router.get('/:id', (req, res) => {
    Tournament.findById(req.params.id)
        .then(tournament => res.json(tournament))
        .catch(err => res.json(err));
});

The component renders the data ONLY if I access it via clicking the Link on the Show All page. However that console.log in componentDidMount() shows me showTournament: ""

Which is odd because in my redux devtools, showTournament:{} is populated with the correct data. It just doesn't persist, or maybe it isn't really there which is what the console log is showing me.

Upvotes: 0

Views: 238

Answers (2)

Linda Paiste
Linda Paiste

Reputation: 42228

This is a problem of app structure. You have combined the actions for navigating to a page and loading data for a page such that you can’t do one without the other. You need to separate data fetching from navigation.

If I were to enter a url with “/tournaments/5” directly into my browser. you need to be calling a dispatch action to fetch the data for tournament 5.

The way I usually set this up is that the individual tournament component has a selector that selects the data and an action to dispatch to fetch the data. If the selector shows that the data hasn’t been loaded or requested, then I dispatch the action to fetch it.

Your navigation onClick handler should just handle navigating to the page, and the page should handle loading it’s own data. That way the page will always have data whether it is navigated to, refreshed, or accessed directly.

EDIT:

The good news is that your routing is fine, mostly. More specific paths need to go before broader ones, so "/tournaments/:id/start" needs to go before "/tournaments/:id" or else start pages will always match the main /:id path.

I made a little demo of the routing and it works. (but the back button is weird? I don't know if that's just a codesandbox issue)

When Route loads the component it passes in extra props. You can use those props to get the id:

const _id = props.match.params.id;

So you can call the action to fetch data within componentDidMount (or useEffect) of your TournamentShow component.

EDIT 2:

When you are dealing with data that might or might not exist, you need to make sure that the object exists before you destructure it, or else you will get errors like Cannot read property '_id' of undefined because you are trying to access a property on undefined which is not an object.

One way to handle this is to exit the render function early when you are in the loading state:

    render() {

        const loading = this.props.tournament.loading || ! this.props.tournament.showTournament;

        if (loading) {
            return (
                <Spinner color="light" />
            )
        }

        const { _id, title, hostedBy, status, participants } = this.props.tournament.showTournament;

        return (
            <div>
               ....

The other way, which I think is the better way, is to separate the fetching, loading, and matching from the component display. You would have a component RenderTournament which handles most of what's in your current component, but it requires a valid tournament prop. Basically we only call this component once the data has loaded, and it shows the data. It no longer needs to be aware of this.props.match and no longer dispatches a fetch. Let's make it so this component doesn't need to be connected to redux at all because we'll give it the actions as props.

You wrap this in an outer component which is the one that your Route directs to. The outer component takes the id from this.props.match.params and handles the dispatch. It connects to the redux store to get the tournament state and the actions to dispatch.

For the render of your outer component, you check whether the data is loaded and valid. You either render a Spinner or you render your RenderTournament component will all of its props.


const RenderTournamentShow = ({
  tournament,
  auth,
  onSignUp,
  onStartTournament
}) => {
  const { _id, title, hostedBy, status, participants } = tournament;
  const { isAuthenticated, user } = auth;

  return (
    <div>
      <div style={{ color: "lightgrey" }}>
        <h1 className="text-center">
          {title}
          <span style={{ fontSize: "0.5em" }}> by {hostedBy}</span>
        </h1>

        <h3>
          <TournamentDescription key={_id} title={title} />
        </h3>

        <br />

        <p
          className="text-center"
          style={{ color: "#56A8CBFF", fontSize: "2em" }}
        >
          ~ {status} for registration ~
        </p>

        <h4 className="text-left mt-5">
          {participants && participants.length === 1
            ? `${participants && participants.length} Registered Fighter`
            : `${participants && participants.length} Registered Fighters`}
        </h4>

        <ul>
          {participants &&
            participants.map((participant) => (
              <li
                key={participant._id}
                className="text-left"
                style={{ fontSize: "1.1em" }}
              >
                {participant.username}
              </li>
            ))}
        </ul>

        {isAuthenticated ? (
          <div>
            <TournamentSignUp
              participants={participants}
              userId={user._id}
              onClick={() => onSignUp(_id, user)}
            />
          </div>
        ) : (
          <Button block disabled>
            Log in to sign up for this tournament
          </Button>
        )}

        {isAuthenticated && user.username === hostedBy ? (
          <div>
            <StartTournament
              participants={participants}
              onClick={() => onStartTournament(_id, participants)}
            />
          </div>
        ) : null}
      </div>
      <br />
      <Link to="/">Back to Tournaments main page</Link>
    </div>
  );
};

class TournamentShow extends Component {
  componentDidMount() {
    const id = this.props.match.params.id;
    this.props.showTournament(id);
  }

  static propTypes = {
    tournament: PropTypes.object.isRequired,
    auth: PropTypes.object.isRequired
  };

  onSignUp(tournamentId, user) {
    this.props.addParticipant(tournamentId, user);
  }

  onStartTournament(tourneyId, tourneyParticipants) {
    this.props.updateTournamentStatus(tourneyId, tourneyParticipants);
  }

  render() {
    const loading =
      this.props.tournament.loading || !this.props.tournament.showTournament;

    if (loading) {
      return <Spinner color="light" />;
    } else {
      return (
        <RenderTournamentShow
          tournament={this.props.tournament.showTournament}
          auth={this.props.auth}
          onSignUp={this.props.addParticipant}
          onStartTournament={this.props.updateTournamentStatus}
        />
      );
    }
  }
}

const mapStateToProps = (state) => ({
  tournament: state.tournament,
  auth: state.auth
});

export default connect(mapStateToProps, {
  showTournament,
  addParticipant,
  updateTournamentStatus
})(TournamentShow);

Upvotes: 1

Gleb Shaltaev
Gleb Shaltaev

Reputation: 254

You button onClick is wrong

onClick={this.onShowTournament(_id)}

should be

onClick={() => this.onShowTournament(_id)}

About F5: it's normal that application loose their context. You have two options:

  1. code your id in route with query ?id=... and look at that with something like that
  2. with route params <Route path="/:id" component={...} /> and (useParams or withRouter) from react-router-dom
  3. persisted state (heavy operations and can make other mistakes)

PS look at redux-thunk or redux-saga or redux-observable for async actions in redux

Upvotes: 1

Related Questions