Reputation: 513
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
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
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:
?id=...
and look at that with something like that<Route path="/:id" component={...} />
and (useParams
or withRouter
) from react-router-dom
PS look at redux-thunk or redux-saga or redux-observable for async actions in redux
Upvotes: 1