React & Redux CRUD:显示单个项目不起作用
React & Redux CRUD: Displaying a single item not working
编辑:
锦标赛缩减器:
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
};
锦标赛行动:
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 组件
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);
Show One 锦标赛的后端
router.get('/:id', (req, res) => {
Tournament.findById(req.params.id)
.then(tournament => res.json(tournament))
.catch(err => res.json(err));
});
仅当我通过单击“显示全部”页面上的 Link 访问它时,该组件才会呈现数据。但是 componentDidMount() 中的 console.log 显示 showTournament: ""
这很奇怪,因为在我的 redux devtools 中,showTournament:{}
填充了正确的数据。它只是不会持续存在,或者它可能并不存在,这就是控制台日志向我显示的内容。
你点击的按钮是错误的
onClick={this.onShowTournament(_id)}
应该是
onClick={() => this.onShowTournament(_id)}
关于 F5:应用程序丢失上下文是正常的。
您有两个选择:
- 使用查询
?id=...
在路由中编码您的 ID,然后使用 that 之类的东西查看它
- 使用路由参数
<Route path="/:id" component={...} />
和(useParams
或 withRouter
)来自 react-router-dom
- 持续状态(繁重的操作,可能会出现其他错误)
PS 查看 redux-thunk or redux-saga or redux-observable 在 redux
中的异步操作
这是app结构的问题。您已经将导航到页面和加载页面数据的操作组合在一起,这样您就不能缺一不可。您需要将数据获取与导航分开。
如果我直接在浏览器中输入带有“/tournaments/5”的url。您需要调用调度操作来获取锦标赛 5 的数据。
我通常设置它的方式是,个人锦标赛组件有一个选择数据的选择器和一个要分派以获取数据的操作。如果选择器显示数据尚未加载或请求,那么我将派遣操作来获取它。
您的导航 onClick 处理程序应该只处理导航到页面,页面应该处理加载它自己的数据。这样,无论是导航到、刷新还是直接访问,页面都将始终有数据。
编辑:
好消息是您的路由大部分都很好。更具体的路径需要在更广泛的路径之前进行,因此 "/tournaments/:id/start"
需要在 "/tournaments/:id"
之前进行,否则起始页面将始终与主要 /:id
路径相匹配。
I made a little demo 的路由和它的工作原理。 (但是后退按钮很奇怪?我不知道这是否只是一个codesandbox问题)
当 Route
加载组件时,它会传入额外的道具。您可以使用这些道具来获取 id:
const _id = props.match.params.id;
因此您可以调用操作以在 TournamentShow
组件的 componentDidMount
(或 useEffect
)中获取数据。
编辑 2:
当你处理可能存在或可能不存在的数据时,你需要确保对象存在然后你解构它,否则你会得到像Cannot read property '_id' of undefined
因为您正在尝试访问 undefined
上的 属性 而不是 object
.
处理此问题的一种方法是在处于加载状态时尽早退出 render
函数:
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>
....
另一种方式,我认为是更好的方式,将获取、加载和匹配与组件显示分开。您将有一个组件 RenderTournament
来处理当前组件中的大部分内容,但它需要一个有效的锦标赛道具。基本上我们只在数据加载后调用此组件,它会显示数据。它不再需要知道 this.props.match
并且不再分派提取。让我们做到这一点,这个组件根本不需要连接到 redux,因为我们将把动作作为 props 给它。
您将其包装在一个外部组件中,该组件是您的 Route
指向的组件。外部组件从 this.props.match.params
获取 id
并处理调度。它连接到 redux 存储以获取锦标赛状态和要调度的操作。
对于外部组件的 render
,您检查数据是否已加载且有效。您要么渲染一个 Spinner
,要么渲染您的 RenderTournament
组件将其所有道具。
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);
编辑:
锦标赛缩减器:
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
};
锦标赛行动:
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 组件
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);
Show One 锦标赛的后端
router.get('/:id', (req, res) => {
Tournament.findById(req.params.id)
.then(tournament => res.json(tournament))
.catch(err => res.json(err));
});
仅当我通过单击“显示全部”页面上的 Link 访问它时,该组件才会呈现数据。但是 componentDidMount() 中的 console.log 显示 showTournament: ""
这很奇怪,因为在我的 redux devtools 中,showTournament:{}
填充了正确的数据。它只是不会持续存在,或者它可能并不存在,这就是控制台日志向我显示的内容。
你点击的按钮是错误的
onClick={this.onShowTournament(_id)}
应该是
onClick={() => this.onShowTournament(_id)}
关于 F5:应用程序丢失上下文是正常的。 您有两个选择:
- 使用查询
?id=...
在路由中编码您的 ID,然后使用 that 之类的东西查看它
- 使用路由参数
<Route path="/:id" component={...} />
和(useParams
或withRouter
)来自react-router-dom
- 持续状态(繁重的操作,可能会出现其他错误)
PS 查看 redux-thunk or redux-saga or redux-observable 在 redux
中的异步操作这是app结构的问题。您已经将导航到页面和加载页面数据的操作组合在一起,这样您就不能缺一不可。您需要将数据获取与导航分开。
如果我直接在浏览器中输入带有“/tournaments/5”的url。您需要调用调度操作来获取锦标赛 5 的数据。
我通常设置它的方式是,个人锦标赛组件有一个选择数据的选择器和一个要分派以获取数据的操作。如果选择器显示数据尚未加载或请求,那么我将派遣操作来获取它。
您的导航 onClick 处理程序应该只处理导航到页面,页面应该处理加载它自己的数据。这样,无论是导航到、刷新还是直接访问,页面都将始终有数据。
编辑:
好消息是您的路由大部分都很好。更具体的路径需要在更广泛的路径之前进行,因此 "/tournaments/:id/start"
需要在 "/tournaments/:id"
之前进行,否则起始页面将始终与主要 /:id
路径相匹配。
I made a little demo 的路由和它的工作原理。 (但是后退按钮很奇怪?我不知道这是否只是一个codesandbox问题)
当 Route
加载组件时,它会传入额外的道具。您可以使用这些道具来获取 id:
const _id = props.match.params.id;
因此您可以调用操作以在 TournamentShow
组件的 componentDidMount
(或 useEffect
)中获取数据。
编辑 2:
当你处理可能存在或可能不存在的数据时,你需要确保对象存在然后你解构它,否则你会得到像Cannot read property '_id' of undefined
因为您正在尝试访问 undefined
上的 属性 而不是 object
.
处理此问题的一种方法是在处于加载状态时尽早退出 render
函数:
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>
....
另一种方式,我认为是更好的方式,将获取、加载和匹配与组件显示分开。您将有一个组件 RenderTournament
来处理当前组件中的大部分内容,但它需要一个有效的锦标赛道具。基本上我们只在数据加载后调用此组件,它会显示数据。它不再需要知道 this.props.match
并且不再分派提取。让我们做到这一点,这个组件根本不需要连接到 redux,因为我们将把动作作为 props 给它。
您将其包装在一个外部组件中,该组件是您的 Route
指向的组件。外部组件从 this.props.match.params
获取 id
并处理调度。它连接到 redux 存储以获取锦标赛状态和要调度的操作。
对于外部组件的 render
,您检查数据是否已加载且有效。您要么渲染一个 Spinner
,要么渲染您的 RenderTournament
组件将其所有道具。
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);