Now I'm building an application using react redux store and local store.
I have two components "tweetTable_Comp" and "likeButton_Comp".
The redux store has all the tweets record "tweets" fetched by an API, and tweetTable_Comp has a local state "filteredTweets" so as to add filter function later and show only selected genre tweets.
And every tweet has likingUserIds.
tweetTable_Comp passes likingUserIds as props to likeButton_Comp so that it can add different style depending on if you already liked the tweet or not.
The problem here is that changing the "tweets[indexNum].likingUserIds" in the redux store when user push like button also affects on the local state "filteredTweets[indexNum].likingUserIds".
I was gonna change the redux info and local state info one by one like in deleteTweet function which already works well.
But this is not intentionally working.
Can anyone teach me why this is happening?
here is reducer.js
redux tweets has objects as below
・title(string)
・text(string)
・createdDate(string)
・likingUserIds(array)
・userId(number)
const defaultState = {
user: {
loggedIn: false,
id: 0,
account: ''
},
tweets: []
}
export default function reducer(state = defaultState, action) {
switch (action.type) {
case 'UPDATE_TWEETS':
return {
...state,
tweets: action.tweets
}
default:
return state;
}
}
here is actions.js
export function getTweets(tweets){
return {
type: 'UPDATE_TWEETS',
tweets: tweets
}
}
here is tweetTable_Comp
class TweetTable_Comp extends Component{
constructor(props){
super(props)
const {dispatch} = props;
this.action = bindActionCreators(actions, dispatch);
this.deleteButtonClicked = this.deleteButtonClicked.bind(this)
this.editButtonClicked = this.editButtonClicked.bind(this)
this.handleChanged = this.handleChanged.bind(this)
this.state = {
filteredTweets: [],
searchWord: ""
}
}
handleChanged(e){
this.setState({[e.target.name]: e.target.value})
}
deleteButtonClicked(id, index){
confirm("削除しますか?") &&
this.deleteTweet(id, index)
}
editButtonClicked(id){
this.props.history.push("/edit/" + id)
}
deleteTweet(id, index){
fetch("http://localhost:8080/twitter/deleteTweet/" + id, {
method: "DELETE"
})
.then((response) => {
if(response.status === 200) {
const newList = this.props.tweets.slice()
newList.splice(index, 1)
this.action.getTweets(newList)
this.setState({filteredTweets: newList})
}
})
}
componentDidMount(){
fetch("http://localhost:8080/twitter/sendAllTweets", {
method: "GET"
})
.then((response) => {
response.json()
.then(json => {
this.action.getTweets(json)
this.setState({filteredTweets: json.slice()})
})
})
}
render(){
return(
<>
<h1 className="text-center">tweet一覧</h1>
<SearchBar searchWord={this.state.searchWord} handleChanged={this.handleChanged}/>
<Container>
<Row>
<Col>
<br/>
<br/>
<Table striped bordered hover>
<thead>
<tr className="text-center">
<th>投稿日</th>
<th>投稿者</th>
<th>タイトル</th>
<th>内容</th>
<th>いいね</th>
<th>削除</th>
<th>編集</th>
</tr>
</thead>
<tbody>
{ this.state.filteredTweets.map((tweet, index) => (
<tr key={tweet.id}>
<td className="text-center">{tweet.createdDate}</td>
<td className="text-center">{tweet.user.account}</td>
<td>{tweet.title}</td>
<td>{tweet.text}</td>
<td className="text-center">
<LikeButton likingUserIds={tweet.likingUserIds} index={index} id={tweet.id} />
</td>
<td className="text-center">
<Button variant="outline-secondary" onClick={() => this.deleteButtonClicked(tweet.id, index)}>
<FontAwesomeIcon icon={faTrashAlt} />
</Button>
</td>
<td className="text-center">
<Button variant="outline-secondary" onClick={() => this.editButtonClicked(tweet.id)}>
<FontAwesomeIcon icon={faEdit} />
</Button>
</td>
</tr>
))
}
</tbody>
</Table>
</Col>
</Row>
</Container>
</>
)
}
}
TweetTable_Comp.propTypes = {
dispatch: PropTypes.func,
tweets: PropTypes.array,
history: PropTypes.object,
user:PropTypes.object
}
function mapStateToProps(state){
return state
}
export default withRouter(connect(mapStateToProps)(TweetTable_Comp))
here is likeButton_Comp
class LikeButton_Comp extends Component {
constructor(props){
super(props)
const {dispatch} = props
this.action = bindActionCreators(actions, dispatch)
this.likeButtonClicked = this.likeButtonClicked.bind(this)
}
likeButtonClicked(func, index){
const data = {
userId:this.props.user.id,
tweetId:this.props.id
}
if(func === "unlike"){
fetch("http://localhost:8080/twitter/like", {
method: "DELETE",
body: JSON.stringify(data)
})
.then((response) => {
if(response.status === 200){
let tweets = this.props.tweets.slice()
const orgLikingUsers = this.props.tweets[index].likingUserIds.slice()
const newLikingUsers = orgLikingUsers.filter(item => item !== this.props.user.id)
tweets[index].likingUserIds = newLikingUsers
this.action.getTweets(tweets)
} else {
alert("処理に失敗しました")
}
})
.catch(error => console.error(error))
} else {
fetch("http://localhost:8080/twitter/like", {
method: "POST",
body: JSON.stringify(data)
})
.then((response) => {
if(response.status === 200){
let tweets = this.props.tweets.slice()
let likingUsers = this.props.tweets[index].likingUserIds.slice()
likingUsers.push(this.props.user.id)
tweets[index].likingUserIds = likingUsers
this.action.getTweets(tweets)
} else {
alert("処理に失敗しました")
}
})
.catch(error => console.error(error))
}
}
render(){
return(
<>
<span>{this.props.likingUserIds.length} </span>
{this.props.tweets.length > 0 && this.props.likingUserIds.includes(this.props.user.id) ?
<Button variant="outline-danger">
<FontAwesomeIcon icon={faHeart} onClick={() => this.likeButtonClicked("unlike", this.props.index)}/>
</Button> :
<Button variant="outline-secondary">
<FontAwesomeIcon icon={faHeart} onClick={() => this.likeButtonClicked("like", this.props.index)}/>
</Button>
}
</>
)
}
}
LikeButton_Comp.propTypes = {
dispatch: PropTypes.func,
user: PropTypes.object,
tweets: PropTypes.array,
likingUserIds: PropTypes.array,
index: PropTypes.number,
id: PropTypes.number
}
function mapStateToProps(state){
return state
}
export default withRouter(connect(mapStateToProps)(LikeButton_Comp))
Related
I am trying to load a users' cart upon authentication using axios in my react app but it returns an error saying "cannot read properties of undefined(reading 'data')". The error highlights my cartAction.js:15 which is a .catch method that returns the error response data and status but when i console.log the response from my axios.get(/api/cart/${id}) it logs the response successfully in my console but it doesn't render in my react app. I think the problem is from my Cart.js component code but i can't seem to rectify it. The console.log(err) in my cartAction.js getCart constant printed "Error: Checkout(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null." in my console
Here is my cartAction.js code
> export const getCart = (id) => dispatch => {
> dispatch(setCartLoading());
> axios.get(`/api/cart/${id}`)
> .then(res => dispatch({
> type: GET_CART,
> payload: res.data
> })
> )
> .catch(err => {
> console.log(err)
> dispatch(returnErrors(err.response.data, err.response.status))}); }
>
> export const addToCart = (id, productId, quantity) => dispatch => {
> axios.post(`/api/cart/${id}`, {productId, quantity})
> .then(res => dispatch({
> type: ADD_TO_CART,
> payload: res.data
> }))
> .catch(err => dispatch(returnErrors(err.response.data, err.response.status))); }
>
> export const deleteFromCart = (userId, itemId) => dispatch => {
> axios.delete(`/api/cart/${userId}/${itemId}`)
> .then(res => dispatch({
> type: DELETE_FROM_CART,
> payload: res.data
> }))
> .catch(err => dispatch(returnErrors(err.response.data, err.response.status))); }
>
> export const setCartLoading = () => {
> return{
> type: CART_LOADING
> } }
Here is my component/Cart.js code
class Cart extends Component{
state = {
loaded: false
}
static propTypes = {
getCart: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool,
addToCart: PropTypes.func.isRequired,
deleteFromCart: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
cart: PropTypes.object.isRequired,
checkout: PropTypes.func.isRequired
}
getCartItems = async (id) => {
await this.props.getCart(id);
this.state.loaded = true;
}
onDeleteFromCart = (id, itemId) => {
this.props.deleteFromCart(id, itemId);
}
render(){
const user = this.props.user;
if(this.props.isAuthenticated && !this.props.cart.loading && !this.state.loaded){
this.getCartItems(user._id);
}
return(
<div>
<AppNavbar/>
{this.props.isAuthenticated ?
<Fragment>
{ this.props.cart.cart ? null :
<Alert className="text-center" color="info">Your cart is empty!</Alert>
}
</Fragment>
: <Alert className="text-center" color="danger">Login to View</Alert>
}
{this.props.isAuthenticated && !this.props.cart.loading && this.state.loaded && this.props.cart.cart ?
<Container>
<div className="row">
{this.props.cart.cart.items.map((item)=>(
<div className="col-md-4">
<Card>
<CardBody>
<CardTitle tag="h5">{item.name}</CardTitle>
<CardSubtitle>NGN {item.price}</CardSubtitle>
<CardText>Quantity - {item.quantity}</CardText>
<Button color="danger" onClick={this.onDeleteFromCart.bind(this, user._id, item.productId)}>Delete</Button>
</CardBody>
</Card>
<br/>
</div>
))}
<div className="col-md-12">
<Card>
<CardBody>
<CardTitle tag="h5">Total Cost = NGN. {this.props.cart.cart.bill}</CardTitle>
<Checkout
user={user._id}
amount={this.props.cart.cart.bill}
checkout={this.props.checkout}
/>
</CardBody>
</Card>
</div>
</div>
</Container>
: null
}
</div>
);
}
}
const mapStateToProps = (state) => ({
cart: state.cart,
isAuthenticated: state.auth.isAuthenticated,
user: state.auth.user
})
export default connect(mapStateToProps, {getCart, deleteFromCart, checkout})(Cart);
Here's my cartReducer.js code
const initialState = {
cart: null,
loading: false
}
export default function cartReducer (state=initialState, action){
switch(action.type){
case GET_CART:
return {
...state,
cart: action.payload,
loading: false
}
case ADD_TO_CART:
return {
...state,
cart: action.payload
}
case DELETE_FROM_CART:
return {
...state,
cart: action.payload
}
case CART_LOADING:
return {
...state,
loading: true
}
default:
return state;
}
}
I think I see the issue now. You are issuing a side-effect from your render method. render is to be considered a synchronous, pure function. If you need to fetch data it should be in one of the component lifecycle methods, i.e. componentDidMount and/or componentDidUpdate.
Move the "getCartItems" conditional logic into a utility function and invoke from the componentDidMount lifecycle method.
fetchCartItems = () => {
const { cart, isAuthenticated, user } = this.props;
const { loaded } = this.state;
if (isAuthenticated && !cart.loading && !loaded) {
this.getCartItems(user._id);
}
}
componentDidMount() {
this.fetchCartItems();
}
...
render() {
const { user } = this.props;
return (
<div>
...
</div>
);
}
And if there's a chance you may need to again fetch the cart items later after the component has mounted, for example, the authentication status changes, use the componentDidUpdate method.
componentDidUpdate() {
this.fetchCartItems();
}
I want to be able to type into my input fields, and then have a button show up beside it upon typing that says submit edit. right now, I have a button that always is there, but I want it to only show up upon typing. this is all in react btw. so far, I have tried jquery, but react doesn't like it.
here's the whole page, to avoid any confusion of what I am doing and where my stuff is located.
import React, { Component } from "react";
import axios from "axios";
import "../styles/TourPage.css";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true,
};
}
componentDidMount() {
axios
.get("/getResults")
.then((res) => {
this.setState({
myData: res.data
});
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
deleteById = (id) => {
console.log(id)
axios
.post(`/deleteDoc`, {id: id} )
.then(() => {
console.log(id, " worked")
window.location = "/tour"
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
}
editById = (id, siteLocation, Services, cnum) => {
console.log(id, siteLocation, Services, cnum)
axios
.post(`/editDoc`, JSON.stringify({id: id, location: siteLocation, Services: Services, cnum: cnum}),{
headers: {
"Content-Type": "Application/json"
}
} )
.then(() => {
console.log(id, " worked")
window.location = "/tour"
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
}
render() {
// You can handle the loader part here with isLoading flag. In this case No data found will be shown initially and then the actual data
let { myData, isLoading } = this.state;
return (
<table id="customers">
<tr>
<th>siteLocation</th>
<th>Services</th>
<th>cnum</th>
</tr>
{myData.length > 0
? myData.map(({ location, Services, cnum, _id }, index) => (
<tr key={index}>
<td><input type="text" placeholder={location} name="location" id="location" /> </td>
<td><input type="text" placeholder={Services} name="Services" id="Services" /> </td>
<td><input type="text" placeholder={cnum} name="cnumhide" id="cnumhide" /> </td>
<td><input type="hidden" placeholder={cnum} name="cnum" id="cnum" /> </td>
<button
onClick={(e) => {
e.preventDefault();
this.deleteById(_id);
}}
disabled={isLoading}
>
Delete
</button>
<button
onClick={(e) => {
e.preventDefault();
var siteLocation = document.getElementById('location').value
var Services = document.getElementById('Services').value
var cnum = document.getElementById('cnum').value
this.editById(_id, siteLocation, Services, cnum)
}}
>
Submit Edit
</button>
</tr>
))
: "No Data Found"}
</table>
);
}
}
const script = document. createElement("script"); $('input').keyup(function(){
if($.trim(this.value).length > 0)
$('#location').show()
else
$('#location').hide()
});
export default TourPage;
thanks 4 the help in advance.
You can use onfocus() in the text element. If you want to hide the button, use onfocusout() or in case if you want to track only after input has changed, use onchange() event
...
//class function
onTyping =()=>{
this.setState({
showSubmit:true
})
}
...
//render part
render(){
...
//input which you want to track typing
<input type="text" onfocus={()=>this.onTyping()} placeholder={location} name="location" id="location" />
...
//element submit button
{this.state.showSubmit && <button
onClick={(e) => {
e.preventDefault();
var siteLocation = document.getElementById('location').value
var Services = document.getElementById('Services').value
var cnum = document.getElementById('cnum').value
this.editById(_id, siteLocation, Services, cnum)
}}
>
Submit Edit
</button>}
...
Here is an example that helps you,
const {
useState
} = React;
const Test = () => {
const [show, setShow] = useState(false);
const handleChange = (event) => {
if (event.target.value.length > 0)
setShow(true);
else
setShow(false)
}
return ( <div>
<input type = "text"
onChange = {
(event) => handleChange(event)
}/>
{show && < button > Submit changes now! </button>}
</div>
)
}
ReactDOM.render( < Test / > ,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
There is a way to avoid jquery and continue using your react class component to achieve this.
Map over state.myData to render each item with an input and a button.
Use the array index with the input's onChange event callback to add the inputValue into the correct array item's object within state.
Use the array index with the button's onClick event callback to get the item from state.myData before sending it to the server.
If there is an inputValue for the item, you can conditionally render the button.
import React, { Component } from "react";
import axios from "axios";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true
};
}
componentDidMount() {
axios
.get("https://rickandmortyapi.com/api/character")
.then((res) => {
this.setState({
myData: res.data.results
});
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
handleChangeInput = ({ target }, index) => {
const newData = [...this.state.myData];
newData[index].inputValue = target.value;
this.setState({
myData: newData
});
};
handleSubmitEdit = (index) => {
const item = this.state.myData[index];
// submit the edit to the api
console.log(
`Clicked on 'submit edit' for ${item.name} with value ${item.inputValue}`
);
};
render() {
let { myData, isLoading } = this.state;
if (isLoading) {
return "loading...";
}
return (
<div>
{myData.map(({ name, status, species, inputValue }, index) => {
return (
<div key={index}>
<p>{`${name} - ${species} - ${status}`}</p>
<input
type="text"
onChange={(e) => this.handleChangeInput(e, index)}
value={inputValue || ""}
/>
{inputValue && (
<button onClick={() => this.handleSubmitEdit(index)}>
Submit Edit
</button>
)}
</div>
);
})}
</div>
);
}
}
export default TourPage;
If you wanted to have an input per field within each row, you could make some small changes and save your edits to the item's state within a nested object.
Then you could check if there was anything inside that row's edits object to conditionally show the submit button per row.
import React, { Component } from "react";
import axios from "axios";
import isEmpty from "lodash.isempty";
import pick from "lodash.pick";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true
};
}
componentDidMount() {
axios
.get("https://rickandmortyapi.com/api/character")
.then((res) => {
this.setState({
// here we create an empty 'edits' object for each row
myData: res.data.results.map((d) => ({
...pick(d, ["name", "status", "species"]),
edits: {}
}))
});
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
handleChangeInput = ({ target }, index) => {
const newData = [...this.state.myData];
const { value, name } = target;
newData[index].edits[name] = value;
this.setState({
myData: newData
});
};
handleSubmitEdit = (index) => {
const item = this.state.myData[index];
// submit the edit to the api
console.log(`Clicked on 'submit edit' for ${item.name} with edits:`);
console.log(item.edits);
console.log("Updated item: ");
const { edits, ...orig } = item;
const newItem = { ...orig, ...edits };
console.log(newItem);
// Once saved to api, we can update myData with newItem
// and reset edits
const newData = [...this.state.myData];
newData[index] = { ...newItem, edits: {} };
this.setState({
myData: newData
});
};
showButton = (index) => {
const { edits } = this.state.myData[index];
return !isEmpty(edits);
};
render() {
let { myData, isLoading } = this.state;
if (isLoading) {
return "loading...";
}
return (
<table>
<tbody>
{myData.map((row, index) => {
const { edits, ...restRow } = row;
return (
<tr key={index}>
{Object.keys(restRow).map((col) => {
return (
<td>
<label>
{col}:
<input
name={col}
value={edits[col] || restRow[col]}
onChange={(e) => this.handleChangeInput(e, index)}
/>
</label>
</td>
);
})}
<td>
{this.showButton(index) && (
<button onClick={() => this.handleSubmitEdit(index)}>
Submit Edit
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
}
export default TourPage;
I have created a component to function as a "Like/Unlike" button. When the state is true, the "Unlike" button successfully displays, but when I click "Unlike", and it DOES unlike successfully, the state should be set to false as (liked: false). However, I don't see the button.
One thing I noticed is, when I click "Unlike", the "Unlike" button disappears and the "Like" button does appear, for a millisecond, and then it vanishes in thin air. I cannot figure it out why.
Here are all the codes for my like button component:
import React from "react";
import { API, graphqlOperation } from "aws-amplify";
import { Button } from "element-react";
import { createLike, deleteLike } from "../graphql/mutations";
import { UserContext } from "../App";
class Like extends React.Component {
state = {
liked: "",
};
componentDidMount() {
this.setLiked();
}
setLiked() {
console.log(this.props);
const { user } = this.props;
const { post } = this.props;
if (post.likes.items.find((items) => items.liker === user.username)) {
this.setState({ liked: true });
console.log("liked: true");
} else {
this.setState({ liked: false });
console.log("liked: false");
}
}
handleLike = async (user) => {
try {
const input = {
liker: user.username,
likePostId: this.props.postId,
};
await API.graphql(graphqlOperation(createLike, { input }));
this.setState({
liked: true,
});
console.log("Liked!");
} catch (err) {
console.log("Failed to like", err);
}
};
handleUnlike = async (likeId) => {
try {
const input = {
id: likeId,
};
await API.graphql(graphqlOperation(deleteLike, { input }));
this.setState({
liked: false,
});
console.log("Unliked!");
} catch (err) {
console.log("Failed to unlike", err);
}
};
render() {
const { like } = this.props;
const { liked } = this.state;
return (
<UserContext.Consumer>
{({ user }) => (
<React.Fragment>
{liked ? (
<Button type="primary" onClick={() => this.handleUnlike(like.id)}>
Unlike
</Button>
) : (
<Button
type="primary"
onClick={() => this.handleLike(user, like.id)}
>
Like
</Button>
)}
</React.Fragment>
)}
</UserContext.Consumer>
);
}
}
export default Like;
The code of the parent component:
import React from "react";
import { API, graphqlOperation } from "aws-amplify";
import {
onCreateComment,
onCreateLike,
onDeleteLike,
} from "../graphql/subscriptions";
import { getPost } from "../graphql/queries";
import Comment from "../components/Comment";
import Like from "../components/Like";
import LikeButton from "../components/LikeButton";
import { Loading, Tabs, Icon } from "element-react";
import { Link } from "react-router-dom";
import { S3Image } from "aws-amplify-react";
import NewComment from "../components/NewComment";
class PostDetailPage extends React.Component {
state = {
post: null,
isLoading: true,
isAuthor: false,
};
componentDidMount() {
this.handleGetPost();
this.createCommentListener = API.graphql(
graphqlOperation(onCreateComment)
).subscribe({
next: (commentData) => {
const createdComment = commentData.value.data.onCreateComment;
const prevComments = this.state.post.comments.items.filter(
(item) => item.id !== createdComment.id
);
const updatedComments = [createdComment, ...prevComments];
const post = { ...this.state.post };
post.comments.items = updatedComments;
this.setState({ post });
},
});
this.createLikeListener = API.graphql(
graphqlOperation(onCreateLike)
).subscribe({
next: (likeData) => {
const createdLike = likeData.value.data.onCreateLike;
const prevLikes = this.state.post.likes.items.filter(
(item) => item.id !== createdLike.id
);
const updatedLikes = [createdLike, ...prevLikes];
const post = { ...this.state.post };
post.likes.items = updatedLikes;
this.setState({ post });
},
});
this.deleteLikeListener = API.graphql(
graphqlOperation(onDeleteLike)
).subscribe({
next: (likeData) => {
const deletedLike = likeData.value.data.onDeleteLike;
const updatedLikes = this.state.post.likes.items.filter(
(item) => item.id !== deletedLike.id
);
const post = { ...this.state.post };
post.likes.items = updatedLikes;
this.setState({ post });
},
});
}
componentWillUnmount() {
this.createCommentListener.unsubscribe();
}
handleGetPost = async () => {
const input = {
id: this.props.postId,
};
const result = await API.graphql(graphqlOperation(getPost, input));
console.log({ result });
this.setState({ post: result.data.getPost, isLoading: false }, () => {});
};
checkPostAuthor = () => {
const { user } = this.props;
const { post } = this.state;
if (user) {
this.setState({ isAuthor: user.username === post.author });
}
};
render() {
const { post, isLoading } = this.state;
return isLoading ? (
<Loading fullscreen={true} />
) : (
<React.Fragment>
{/*Back Button */}
<Link className="link" to="/">
Back to Home Page
</Link>
{/*Post MetaData*/}
<span className="items-center pt-2">
<h2 className="mb-mr">{post.title}</h2>
</span>
<span className="items-center pt-2">{post.content}</span>
<S3Image imgKey={post.file.key} />
<div className="items-center pt-2">
<span style={{ color: "var(--lightSquidInk)", paddingBottom: "1em" }}>
<Icon name="date" className="icon" />
{post.createdAt}
</span>
</div>
<div className="items-center pt-2">
{post.likes.items.map((like) => (
<Like
user={this.props.user}
like={like}
post={post}
postId={this.props.postId}
/>
))}
</div>
<div className="items-center pt-2">
{post.likes.items.length}people liked this.
</div>
<div>
Add Comment
<NewComment postId={this.props.postId} />
</div>
{/* Comments */}
Comments: ({post.comments.items.length})
<div className="comment-list">
{post.comments.items.map((comment) => (
<Comment comment={comment} />
))}
</div>
</React.Fragment>
);
}
}
export default PostDetailPage;
I think I know why it doesn't show up. It's because at first when the user hasn't liked it, there is no "like" object, so there is nothing to be shown, as it is only shown when there is a "like" mapped to it. I don't know how to fix it though.
UPDATE
Response was an array - but the issue came from the backend (Express/Build folder)
Coming back around to an issue that I came across awhile ago.
In my DEV environment - no issues. Once I deploy (Heroku), I am getting the "this.state.workorders.map is not a function". I also attempted "Object.keys and values" in the event it was being treated as such but just gives me back blank values.
This is what I get back below
const WorkOrder = props => (
<tr>
<td>{props.workorder.employee}</td>
<td>{props.workorder.description}</td>
<td>{props.workorder.duration}</td>
<td>{props.workorder.date.substring(0, 10)}</td>
<td>
<Link to={"/edit/" + props.workorder._id}>Edit</Link> |{" "}
<a
href="#"
onClick={() => {
props.deleteWorkOrder(props.workorder._id);
}}
>
Delete
</a>
</td>
<td>
<a
href="#"
onClick={() => {
props.markComplete(props.workorder._id);
}}
>
Completed
</a>
</td>
</tr>
);
class WorkOrdersList extends Component {
constructor(props) {
super(props);
this.state = {
workorders: []
};
this.deleteWorkOrder = this.deleteWorkOrder.bind(this);
this.markComplete = this.markComplete.bind(this);
}
onLogoutClick = e => {
e.preventDefault();
this.props.logoutUser();
};
componentDidMount = () => {
axios
.get("/workorders/")
.then(response => {
this.setState({ workorders: response.data });
console.log(response);
})
.catch(error => {
console.log(error);
});
};
deleteWorkOrder(id) {
axios.delete("/workorders/" + id).then(response => {
console.log(response.data);
});
this.setState({
workorders: this.state.workorders.filter(el => el._id !== id)
});
}
markComplete(id) {
axios
.patch("/workorders/" + id, { isComplete: "true" })
.then(response => {
console.log(response.data);
})
.catch(error => {
console.log(error);
});
this.setState({
workorders: this.state.workorders.filter(el => el._id !== id)
});
}
workordersList = () => {
return this.state.workorders.map(currentworkorder => {
return (
<WorkOrder
workorder={currentworkorder}
deleteWorkOrder={this.deleteWorkOrder}
markComplete={this.markComplete}
key={currentworkorder._id}
/>
);
})
);
};
render() {
return (
<div className="containerMax">
<div className="row">
<div className="col-9">
<h3>Open Work Orders</h3>
</div>
<div className="col-3">
<button
type="button"
class="btn btn-outline-danger"
onClick={this.onLogoutClick}
>
Logout
</button>
</div>
</div>
<table className="table">
<thead className="thead-light">
<tr>
<th>Employee</th>
<th>Description</th>
<th>Duration (minutes)</th>
<th>Scheduled Date</th>
<th>Actions</th>
<th>Completed Job</th>
</tr>
</thead>
<tbody>{this.workordersList()}</tbody>
</table>
<br />
</div>
);
}
}
WorkOrdersList.propTypes = {
logoutUser: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps, { logoutUser })(WorkOrdersList);
Check in your function componentDidMount() if response.data is an Array:
componentDidMount = () => {
axios
.get("/workorders/")
.then(response => {
this.setState({ workorders: response.data }); <============ HERE
console.log(response);
})
.catch(error => {
console.log(error);
});
};
Then, validate your componentDidMount() function:
workordersList = () => {
if (this.state.workorders && this.state.workorders.length) {
return this.state.workorders.map(currentworkorder => {
return (
<WorkOrder
workorder={currentworkorder}
deleteWorkOrder={this.deleteWorkOrder}
markComplete={this.markComplete}
key={currentworkorder._id}
/>
);
});
} else { return []; }
};
Try binding workordersList in the WorkOrdersList component constructor.
It will look like this:
constructor(props) {
super(props);
this.state = {
workorders: []
};
this.deleteWorkOrder = this.deleteWorkOrder.bind(this);
this.markComplete = this.markComplete.bind(this);
this.workordersList = this.workordersList.bind(this);
}
Also you need to wait until axios in componentDidMount method completely loads all the workorders.
So in your render, in tbody tag you can put the following:
{this.state.workorders.length > 0 ? this.workordersList() : ""}
Hope it helps
I am using react/redux and the error is happening after a deleteRequest is called using an action. The reducer removes the item from the array and I think that is why this is happening, but I should be changing the location so it should not be rendering this page anymore.
The direct error is coming from the Post component down below from the this.props.deleteRecord in the catch block.
I am also using turbo which is how I make the deleteRequest and store the data. If you need a reference here is the docs. https://www.turbo360.co/docs
Reducer:
import constants from '../constants';
const initialState = {
all: null
};
export default (state = initialState, action) => {
switch (action.type) {
case constants.POST_CREATED:
return {
...state,
all: state.all.concat(action.data),
[action.data.id]: action.data
};
case constants.RECORD_UPDATED:
return {
...state,
[action.data.id]: action.data,
all: all.map(item => (item.id === action.data.id ? action.data : item))
};
case constants.RECORD_DELETED:
const newState = {
...state,
all: state.all.filter(item => item.id !== action.data.id)
};
delete newState[action.payload.id];
return newState;
case constants.FETCH_POSTS:
const sortedData = action.data.sort((a, b) => {
return new Date(b.timestamp) - new Date(a.timestamp);
});
return { ...state, all: sortedData };
case constants.FETCH_POST:
return { ...state, [action.data.id]: action.data };
default:
return state;
}
};
Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import swal from 'sweetalert2/dist/sweetalert2.all.min.js';
import actions from '../../actions';
import { DateUtils } from '../../utils';
import { Reply } from '../containers';
import { UpdateRecord } from '../view';
class Post extends Component {
constructor() {
super();
this.state = {
editShow: false
};
}
componentDidMount() {
const { id } = this.props.match.params;
if (this.props.posts[id] != null) {
return;
}
this.props
.getRecord(id)
.then(() => {})
.catch(err => {
console.log(err);
});
}
updateRecord(params) {
const { id } = this.props.match.params;
const post = this.props.posts[id];
const { currentUser } = this.props.user;
if (post.profile.id !== currentUser.id) {
swal({
title: 'Oops...',
text: 'Must be owner of post',
type: 'error'
});
return;
}
this.props
.updateRecord(post, params)
.then(response => {
swal({
title: 'Success',
text: `${currentUser.username} Your post has been updated!`,
type: 'success'
});
})
.catch(err => {
console.log(err);
});
}
deleteRecord() {
const { id } = this.props.match.params;
const post = this.props.posts[id];
const { currentUser } = this.props.user;
if (currentUser.id !== post.profile.id) {
swal({
title: 'Oops...',
text: 'Must be owner of post',
type: 'error'
});
return;
}
this.props
.deleteRecord(post)
.then(() => {
this.props.history.push('/');
swal({
title: 'Post Delete',
text: 'Please create a new post',
type: 'success'
});
})
.catch(err => {
console.log(err);
});
}
render() {
const { id } = this.props.match.params;
const post = this.props.posts[id];
const { currentUser } = this.props.user;
if (post == null) {
return <div />;
}
return (
<div>
<div className="jumbotron">
<h1 className="display-3">{post.title}</h1>
<div className="row" style={{ marginBottom: '25px' }}>
<img className="img-fluid mx-auto" src={`${post.image}`} style={{ maxHeight: '400px' }} />
</div>
<p className="lead">{post.text}</p>
<hr className="my-4" />
{post.video == undefined ? null : (
<div className="row justify-content-center">
<div className="col-8">
<div className="lead" style={{ marginBottom: '25px' }}>
<div className="embed-responsive embed-responsive-16by9">
<video style={{ background: 'black' }} width="800" controls loop tabIndex="0">
<source src={post.video} type={post.videoType} />
Your browser does not support HTML5 video.
</video>
</div>
</div>
</div>
</div>
)}
<div className="lead">
<Link to={`/profile/${post.profile.id}`}>
<button className="btn btn-secondary btn-lg">{post.profile.username}</button>
</Link>
<p style={{ marginTop: '10px' }}>{DateUtils.relativeTime(post.timestamp)}</p>
</div>
{currentUser.id !== post.profile.id ? null : (
<div className="row justify-content-end">
<div className="col-md-2">
<button
onClick={() => {
this.setState({ editShow: !this.state.editShow });
}}
className="btn btn-success"
>
Edit
</button>
</div>
<div className="col-md-2">
<button onClick={this.deleteRecord.bind(this)} className="btn btn-danger">
Delete
</button>
</div>
</div>
)}
</div>
{this.state.editShow === false ? null : (
<div>
<UpdateRecord onCreate={this.updateRecord.bind(this)} currentRecord={post} />
</div>
)}
<div>
<Reply postId={post.id} />
</div>
</div>
);
}
}
const stateToProps = state => {
return {
posts: state.post,
user: state.user
};
};
const dispatchToProps = dispatch => {
return {
getRecord: id => dispatch(actions.getRecord(id)),
updateRecord: (entity, params) => dispatch(actions.updateRecord(entity, params)),
deleteRecord: entity => dispatch(actions.deleteRecord(entity))
};
};
export default connect(stateToProps, dispatchToProps)(Post);
Take a look here
case constants.RECORD_DELETED:
const newState = {
...state,
all: state.all.filter(item => item.id !== action.data.id)
};
delete newState[action.payload.id];
return newState;
You are using action.data when filtering, and action.payload when deleting from object