I'm building a simple campgrounds CRUD app to get some practice with the MERN stack and Redux.
Adding a campground is working fine. I'm routing to the campgrounds list page after adding a campground. But unless I reload the page, fresh data isn't retrieved.
I figured it has something to do with React's lifecycle methods.
Code:
manageCampground.js
import React, { Component } from 'react';
import TextField from '#material-ui/core/TextField';
import Card from '#material-ui/core/Card';
import Button from '#material-ui/core/Button';
import '../../styles/addCampground.css';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
actionAddCampground,
getCampgroundDetails,
actionUpdateCampground
} from './actions/campgroundActions';
class AddCampground extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
description: '',
cost: ''
};
}
componentDidMount() {
const campground = this.props.campground;
if (campground._id) {
this.props.getCampgroundDetails(campground._id);
this.setState({
name: campground.name,
description: campground.description,
cost: campground.cost
});
}
}
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
addCampground = () => {
const name = this.state.name;
const description = this.state.description;
const cost = this.state.cost;
this.props.actionAddCampground({
name,
description,
cost
});
this.props.history.push('/home');
console.log('Campground added successfully');
};
updateCampground = () => {
const name = this.state.name;
const description = this.state.description;
const cost = this.state.cost;
this.props.actionUpdateCampground({
name,
description,
cost
});
this.props.history.push('/home');
console.log('Updated successfully');
};
render() {
console.log(this.props);
return (
<Card className="add-campground-card">
<TextField
name="name"
className="textfield"
label="Campground name"
variant="outlined"
value={this.state.name}
onChange={e => this.handleChange(e)}
/>
<TextField
name="description"
className="textfield"
label="Campground description"
variant="outlined"
value={this.state.description}
onChange={e => this.handleChange(e)}
/>
<TextField
name="cost"
className="textfield"
type="number"
label="Campground cost"
variant="outlined"
value={this.state.cost}
onChange={e => this.handleChange(e)}
/>
{!this.props.campground._id ? (
<Button
variant="contained"
color="primary"
onClick={this.addCampground}>
Add Campground
</Button>
) : (
<Button
variant="contained"
color="primary"
className="update-campground-btn"
onClick={this.updateCampground}>
Update Campground
</Button>
)}
</Card>
);
}
}
const mapStateToProps = state => {
return {
campground: state.campgroundList.singleCampground || ''
};
};
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
actionAddCampground,
getCampgroundDetails,
actionUpdateCampground
},
dispatch
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(AddCampground);
campgroundList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getAllCampgrounds } from './actions/campgroundActions';
import Header from '../common/Header';
import Card from '#material-ui/core/Card';
import CardActionArea from '#material-ui/core/CardActionArea';
import CardActions from '#material-ui/core/CardActions';
import CardContent from '#material-ui/core/CardContent';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import '../../styles/landingPage.css';
class CampgroundLanding extends Component {
componentDidMount() {
this.props.getAllCampgrounds();
console.log('From component did mount');
}
render() {
const { campgrounds } = this.props;
return (
<>
<Header />
{campgrounds.map(campground => (
<Card className="campground-card" key={campground._id}>
<CardActionArea>
<CardContent>
<Typography gutterBottom variant="h5"
component="h2">
{campground.name}
</Typography>
<Typography variant="body2" color="textSecondary"
component="p">
{campground.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Link
style={{ textDecoration: 'none', color: 'white' }}
to={`/campgrounds/${campground._id}`}>
<Button size="small" color="primary">
View Details
</Button>
</Link>
<Button size="small" color="primary">
Learn More
</Button>
</CardActions>
</Card>
))}
<Link
style={{ textDecoration: 'none', color: 'white' }}
to="/campgrounds/add">
<Button color="primary">Add Campground</Button>
</Link>
</>
);
}
}
const mapStateToProps = state => {
return {
campgrounds: state.campgroundList.campgrounds
};
};
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
getAllCampgrounds
},
dispatch
);
};
export default connect(
mapStateToProps,
null
)(CampgroundLanding);
campgroundActions.js
import {
GET_ALL_CAMPGROUNDS,
ADD_CAMPGROUND,
GET_CAMPGROUND_DETAILS,
EDIT_CAMPGROUND
} from '../actionTypes/types';
import axios from 'axios';
const API_URL = `http://localhost:5000/api`;
export const getAllCampgrounds = () => {
return dispatch => {
axios
.get(`${API_URL}/campgrounds`)
.then(res => {
dispatch({
type: GET_ALL_CAMPGROUNDS,
payload: res
});
})
.catch(err => console.log(err));
};
};
export const actionAddCampground = campground => {
return dispatch => {
axios
.post(`${API_URL}/campgrounds`, campground)
.then(res => {
console.log(res);
dispatch({
type: ADD_CAMPGROUND,
payload: res
});
})
.catch(err => console.log(err));
};
};
export const getCampgroundDetails = id => {
return dispatch => {
axios
.get(`${API_URL}/campgrounds/${id}`)
.then(res => {
dispatch({
type: GET_CAMPGROUND_DETAILS,
payload: res
});
})
.catch(err => console.log(err));
};
};
export const actionUpdateCampground = id => {
return dispatch => {
axios
.put(`${API_URL}/campgrounds/${id}`)
.then(res => {
console.log(res);
dispatch({
type: EDIT_CAMPGROUND,
payload: res
});
})
.catch(err => console.log(err));
};
};
campgroundReducers.js
import {
GET_ALL_CAMPGROUNDS,
ADD_CAMPGROUND,
GET_CAMPGROUND_DETAILS,
EDIT_CAMPGROUND
} from '../actionTypes/types';
const initialState = {
campgrounds: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_ALL_CAMPGROUNDS:
const { campgroundList } = action.payload.data;
state.campgrounds = campgroundList;
return {
...state
};
case ADD_CAMPGROUND:
const { campground } = action.payload.data;
return {
...state,
campground
};
case GET_CAMPGROUND_DETAILS:
const { singleCampground } = action.payload.data;
return { ...state, singleCampground };
case EDIT_CAMPGROUND:
const { editedCampground } = action.payload.data;
return { ...state, editedCampground };
default:
return state;
}
};
If I'm using componentDidUpdate, it's leading to infinite loop.
componentDidUpdate(prevProps) {
if (prevProps.campgrounds !== this.props.campgrounds) {
this.props.getAllCampgrounds();
}
}
I know I'm going wrong somewhere, but I can't figure out where.
You have to fix your componentDidUpdate method to avoid this infinity loop. Your are trying to compare objects via === method which will always fail according to this example:
const a = {key: 1}
const b = {key: 1}
console.log(a === b); // false
You could use for example isEqual method from lodash module to compare objects like you want https://lodash.com/docs/4.17.15#isEqual
console.log(_.isEqual({key: 1}, {key: 1})) // true
console.log({key: 1} === {key: 1}) // false
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
Actually why do you want refresh data in this case? When you add object with success into the store this will refresh your component so you don't have to request for fresh data.
Related
I want to display Categories and Category Children in the admin dashboard and the terminal gives no error. But the page consol renders Category.js:55 Uncaught TypeError: categories is not iterable
Uncaught (in promise) TypeError: categories is not iterable
Category Component:
import React, { useEffect ,useState } from 'react';
import { Container, Row, Col ,Modal ,Button} from 'react-bootstrap';
import Layout from '../../components/Layout/Layout'
import { useDispatch, useSelector } from 'react-redux'
import { getAllCategory } from '../../actions'
import Input from '../../components/UI/Input/Input'
import {addCategory} from '../../actions/category.actions'
const Category = () => {
const category = useSelector(state => state.category)
const [categoryName , setCategoryName] =useState('')
const [parentCategoryId , setParentCategoryId] =useState('')
const [categoryImage , setCategoryImage] =useState('')
const dispatch = useDispatch()
useEffect(() => {
console.log('Category.js')
dispatch(getAllCategory())
}, [])
const [show, setShow] = useState(false);
const handleClose = () => {
const form = new FormData()
// const cat ={
// categoryName,
// parentCategoryId,
// categoryImage
// }
form.append('name',categoryName)
form.append('parentId',parentCategoryId)
form.append('categoryImage',categoryImage)
dispatch(addCategory(form))
// console.log('cat',cat)
setShow(false);
}
const handleShow = () => setShow(true);
const renderCategories = (categories) => {
let myCategories = []
for (let category of categories) {
myCategories.push(
<li key={Math.random()}>
{category.name}
{category.children.length > 0 ? (<ul>{renderCategories(category.children)}</ul>) : null}
</li>
)
}
return myCategories;
}
const createCategoryList=(categories,options=[])=>{
for(let category of categories) {
options.push({value: category._id , name: category.name})
if(category.children.length > 0){
createCategoryList(category.children, options)
}
}
return options;
}
const handelCategoryImage =(e)=>{
setCategoryImage(e.target.files[0])
}
return (
<>
<Layout sidebar>
<Container>
<Row>
<Col md={12}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h3>Category</h3>
<button onClick={handleShow}>Add</button>
</div>
</Col>
</Row>
<Row>
<Col md={12}>
<ul>
{renderCategories(category.categories)}
</ul>
</Col>
</Row>
</Container>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Add New Category</Modal.Title>
</Modal.Header>
<Modal.Body>
<Input
value={categoryName}
placeholder={'Category Name'}
onChange={(e)=>setCategoryName(e.target.value)}
/>
<select className="form-control" onChange={(e)=>setParentCategoryId(e.target.value)} value={parentCategoryId}>
<option>Select Category</option>
{
createCategoryList(category.categories).map(option =>
<option key={option.value} value={option.value}>{option.name}</option>)
}
</select>
<input type='file' name='categoryImage' onChange={handelCategoryImage}/>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClose}>
Save Changes
</Button>
</Modal.Footer>
</Modal>
</Layout>
</>
);
};
export default Category;
Category.action.js
import axios from "axios"
import axiosInstance from "../helpers/axios"
import {categoryConstants} from './constants'
export const getAllCategory =()=>{
return async dispatch => {
dispatch({type: categoryConstants.GET_ALL_CATEGORIES_REQUEST})
const res =await axios.get('http://localhost:2000/api/category/getcategory')
console.log("res",res)
if(res.status === 200) {
// const {categoryList} = res.data
// console.log("categoryList",categoryList)
dispatch({
type:categoryConstants.GET_ALL_CATEGORIES_SUCCESS,
payload: {category:res.data.category}
})
}else{
dispatch({
type: categoryConstants.GET_ALL_CATEGORIES_FAILURE,
payload: {error: res.data.error}
})
}
}
}
export const addCategory =(form) => {
const token =window.localStorage.getItem('token')
return async dispatch => {
dispatch({ type: categoryConstants.ADD_NEW_CATEGORY_REQUEST})
const res = await axios.post('http://localhost:2000/api/category/create',form,{headers:{
'Authorization':token ? `Bearer ${token}` :''
}})
if(res.status === 200){
dispatch({
type: categoryConstants.ADD_NEW_CATEGORY_SUCCESS,
payload:res.data.category
})
}else{
dispatch({
type: categoryConstants.ADD_NEW_CATEGORY_FAILURE,
payload:res.data.error
})
}
console.log("res", res)
}
}
category.reducer.js
import {categoryConstants} from '../actions/constants'
const initState ={
categories:[],
loading:false,
error:null,
}
const buildNewCategories =(categories,category)=>{
let myCategories=[]
for(let cat of categories){
myCategories.push({
...cat,
children: cat.children && cat.children.length > 0 ? buildNewCategories(cat.children,category):[]
})
}
return myCategories;
}
export default (state = initState , action)=>{
switch(action.type){
case categoryConstants.GET_ALL_CATEGORIES_SUCCESS:
state={
...state,
categories: action.payload.categories
}
break;
case categoryConstants.ADD_NEW_CATEGORY_REQUEST:
state={
...state,
loading: true,
}
break;
case categoryConstants.ADD_NEW_CATEGORY_SUCCESS:
const updatedCategories=buildNewCategories(state.categories, action.payload.category)
console.log('updated categoires', updatedCategories);
state={
...state,
categories:updatedCategories,
loading: false,
}
break;
case categoryConstants.ADD_NEW_CATEGORY_FAILURE:
state={
...initState,
}
break;
}
return state;
}
When you call const category = useSelector(state => state.category) to get category , you was not sure whether or not category has been fetched successfully yet ( focus on the calling getAllCategory() on your useEffect ).
You just need to check before iterate categories , and some refactor your code like this is fine:
const renderCategories = (categories) => {
if(!Array.isArray(categories)) return null
return categories.map((category, i) => (
<li key={`category-${i}`}>
{category.name}
{Array.isArray(category.children) && category.children.length > 0 ? (
<ul>{renderCategories(category.children)}</ul>
) : null}
</li>)
)
}
Also you can wrap your function renderCategories with useCallback to make more effective
import { useCallback } from 'react'
const renderCategories = useCallback((categories) => {
if(!Array.isArray(categories)) return null
return categories.map((category, i) => (
<li key={`category-${i}`}>
{category.name}
{Array.isArray(category.children) && category.children.length > 0 ? (
<ul>{renderCategories(category.children)}</ul>
) : null}
</li>)
)
}, [])
I am very new to react and javascript, but I am trying to build a simple ToDo App. It wasn't complicated until I wanted to read data from a file and to display that data on the screen. The problem is that I don't know how to create a new Todo object to pass it as parameter for addTodo function.. Thaaank you all and hope you can help me!!
I will let the code here (please see the -loadFromFile- function, there is the problematic place:
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import Todo from './Todo';
import data from './data/data.json'
function TodoList() {
const [todos, setTodos] = useState([]);
const loadFromFile = data.map( ( data) => {
const newTodo = addTodo(new Todo(data.id,data.text));
return ( {newTodo} )});
const addTodo = todo => {
if (!todo.text || /^\s*$/.test(todo.text)) {
return;
}
const newTodos = [todo, ...todos];
setTodos(newTodos);
console.log(...todos);
};
const updateTodo = (todoId, newValue) => {
if (!newValue.text || /^\s*$/.test(newValue.text)) {
return;
}
setTodos(prev => prev.map(item => (item.id === todoId ? newValue : item)));
};
const removeTodo = id => {
const removedArr = [...todos].filter(todo => todo.id !== id);
setTodos(removedArr);
};
const completeTodo = id => {
let updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.isComplete = !todo.isComplete;
}
return todo;
});
setTodos(updatedTodos);
};
return (
<>
<TodoForm onSubmit={addTodo} />
{loadFromFile}
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}
export default TodoList;
I want to create new instance of Todo object. I tried many times, many different forms, but still doesn't work. I have an id and a text from the data.json file. I want to create that instance of Todo object with these two values. But how?
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import EditIcon from '#material-ui/icons/Edit';
import DeleteIcon from '#material-ui/icons/Delete';
const Todo = ({ todos, completeTodo, removeTodo, updateTodo }) => {
const [edit, setEdit] = useState({
id: null,
value: ''
});
const submitUpdate = value => {
updateTodo(edit.id, value);
setEdit({
id: null,
value: ''
});
};
if (edit.id) {
return <TodoForm edit={edit} onSubmit={submitUpdate} />;
}
return todos.map((todo, index) => (
<div
className={todo.isComplete ? 'todo-row complete' : 'todo-row'}
key={index}
>
<p> <div key={todo.id} onClick={() => completeTodo(todo.id)}>
{todo.text}
</div>
</p>
<div className='icons'>
<DeleteIcon fontSize="small"
onClick={() => removeTodo(todo.id)}
className='delete-icon'
/>
<EditIcon
onClick={() => setEdit({ id: todo.id, value: todo.text })}
className='edit-icon'
/>
</div>
</div>
));
};
export default Todo;
import React, { useState, useEffect, useRef } from 'react';
import { Fab, IconButton } from "#material-ui/core";
import AddIcon from '#material-ui/icons/Add';
function TodoForm(props) {
const [input, setInput] = useState(props.edit ? props.edit.value : '');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
});
const handleChange = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 10000),
text: input
});
setInput('');
};
return (
<form onSubmit={handleSubmit} className='todo-form'>
{props.edit ? (
<>
<textarea cols="10"
placeholder='Update item'
value={input}
onChange={handleChange}
name='text'
ref={inputRef}
className='todo-input edit'
/>
<button onClick={handleSubmit} className='todo-button edit'>
Save
</button>
</>
) : (
<>
<input
placeholder='Add item'
value={input}
onChange={handleChange}
name='text'
className='todo-input'
ref={inputRef}
/>
<Fab color="primary" aria-label="add">
< AddIcon onClick={handleSubmit} fontSize="small" />
</Fab>
</>
)}
</form>
);
}
export default TodoForm;
Issue
Ah, I see what you are getting at now, you are wanting to load some list of todos from an external file. The main issue I see in your code is that you are attempting to call/construct a Todo React component manually and this simply isn't how React works. You render data/state/props into JSX and pass this to React and React handles instantiating the components and computing the rendered DOM.
const loadFromFile = data.map((data) => {
const newTodo = addTodo(new Todo(data.id, data.text));
return ({newTodo});
});
Todo shouldn't be invoked directly, React handles this.
Solution
Since it appears the data is already an array of objects with the id and text properties, it conveniently matches what you store in state. You can simply pass data as the initial todos state value.
const [todos, setTodos] = useState(data);
If the data wasn't readily consumable you could create an initialization function to take the data and transform/map it to the object shape your code needs.
const initializeState = () => data.map(item => ({
id: item.itemId,
text: item.dataPayload,
}));
const [todos, setTodos]= useState(initializeState);
Running Example:
import data from "./data.json";
function TodoList() {
const [todos, setTodos] = useState(data); // <-- initial state
const addTodo = (text) => {
if (!text || /^\s*$/.test(text)) {
return;
}
setTodos((todos) => [todo, ...todos]);
};
const updateTodo = (id, newTodo) => {
if (!newTodo.text || /^\s*$/.test(newTodo.text)) {
return;
}
setTodos((todos) => todos.map((todo) => (todo.id === id ? newTodo : todo)));
};
const removeTodo = (id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const completeTodo = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id
? {
...todo,
isComplete: !todo.isComplete
}
: todo
)
);
};
return (
<>
<TodoForm onSubmit={addTodo} />
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}
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.
I'm writting react-redux comment's widget.Faced the following problem. When I click on the add comments button, no addition occurs, and when I click on delete a comment, he writes that map is not a function, although I pass an array of comments. Can you explain where i made a mistake
actions/index.js
let nextCommentId = 0;
export const ADD_COMMENT = 'ADD_COMMENT';
export const REMOVE_COMMENT = 'REMOVE_COMMENT';
export const addComment = (comment) => {
return {
type: ADD_COMMENT,
id: nextCommentId++,
payload: comment
}
}
export const removeComment = (id) => {
return {
type: REMOVE_COMMENT,
id
}
}
components/add-comment.js
import React, { Component, useState } from 'react';
import { addComment } from '../actions/index.js'
class AddComment extends React.Component {
constructor(props) {
super(props);
this.state = {
date: new Date(),
text: '',
name: ''
};
}
render() {
let comment = {
name: this.state.name,
text: this.state.text,
date: this.state.date
}
return (
<div>
<form>
<label htmlFor="username">Введите ваше имя:</label> <br />
<input
type="text"
id="username"
value={this.state.name}
onChange={ev => {
this.setState({ name: ev.target.value });
}}
/> <br /><br />
<label htmlFor="usercomment">Введите ваш комментарий:</label> <br />
<textarea
id="usercomment"
rows="10"
cols="40"
value={this.state.text}
onChange={ev => {
this.setState({ text: ev.target.value });
}}
></textarea> <br />
</form>
<button
className="btn"
onClick={ev => {
addComment(comment);
}}
>
Добавить комментарий
</button>
</div>
);
}
}
export default AddComment;
components/comment-list.js
import React from 'react';
import AddComment from './add-comments.js';
import { removeComment } from '../actions/index';
const CommentList = ({ comments , removeComment }) => {
return (
<ul>
{
comments.map((comment, index) => {
debugger;
return (
<li key={index}>
<b>
{comment.name + comment.date}
</b> <button
className="btn-remove"
onClick={ev => {
if (comments.length === 0) {
return comments
} else {
removeComment(index)
}
}}
>
Удалить комментарий
</button><br />
{comment.text}
</li>
)
})
}
</ul>
)
}
export default CommentList;
containers/app.js
import React from 'react';
import { connect } from 'react-redux';
import CommentList from '../components/comment-list';
import AddComment from '../components/add-comments';
import { addComment,removeComment } from '../actions/index';
let App = ({ comments, addComment, removeComment }) => {
return (
<div>
<CommentList comments={comments} removeComment={removeComment} />
<AddComment addComment={addComment}/>
</div>
)
}
const mapStateToProps = (state) => {
return {
comments: state.comments
}
}
const mapDispatchToProps = (dispatch) => {
debugger;
return {
addComment: (comment) => dispatch(addComment(comment)),
removeComment: (id) => dispatch(removeComment(id))
}
}
App = connect(
mapStateToProps,
mapDispatchToProps
)(App);
export default App;
reducers/index.js
import { ADD_COMMENT, REMOVE_COMMENT } from '../actions/index.js'
const comments = (state = [], action) => {
switch (action.type) {
case ADD_COMMENT:
return [
...state,
action.payload
]
case REMOVE_COMMENT:
return state.comments.filter((comment, id) => id !== action.id);
default:
return state
}
}
export default comments;
src/index.js
import React, {Component} from 'react';
import { render } from 'react-dom';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './containers/app.js';
import { createStore } from 'redux';
import comments from './reducers/index.js';
export const initialState = {
comments: [
{name: 'John', text: 'good', date: '24 октября 17-56'}
]
};
]
const store = createStore( comments, initialState);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#app')
);
you missed the this.props in onClick={addComment} in components/add-comment.js
try like this
components/add-comment.js
...
<button
className="btn"
onClick={ev => {
this.props.addComment(comment);
}}
>
Добавить комментарий
</button>
...
You mapped dispatch to your action, but then didn't use it, you re-imported the action. Use the action from props instead. If it is not bound to dispatch it will do nothing.
this is the code of the StoryCreator
import React from 'react'
import { Helmet } from 'react-helmet'
import { connect } from 'react-redux'
import { Layout, Row, Col, Form, Input, Button, Divider, message, Icon, AutoComplete } from 'antd'
import { Link } from 'react-router-dom'
import ErrorPopover from '../../components/ErrorPopover/ErrorPopover'
import { success } from '../../services/story.services'
import { changeStoryTitle, changeStoryContent , changeStoryBody, changeStoryImage, changeStoryCategoryid, sendStory } from '../../actions/story.actions'
import { fetchCategories } from '../../actions/category.actions'
import '../../vendor/Shadow/Shadow.css'
import '../../vendor/Radius/Radius.css'
import './StoryCreator.css'
const { Content } = Layout
const FormItem = Form.Item
const { TextArea } = Input;
const onload = () => {
const hide = message.loading('Cargando entrada..');
};
class StoryCreator extends React.Component {
componentWillMount() {
this.props.dispatch(fetchCategories())
}
constructor (props) {
super(props)
}
handleChange = (e) => {
if(e.target.id === 'storyTitle') {
this.props.dispatch(changeStoryTitle(e.target.value))
}
}
handleChangeContent = (e) => {
if(e.target.id === 'storyContent') {
this.props.dispatch(changeStoryContent(e.target.value))
}
}
handleChangeBody = (e) => {
if(e.target.id === 'storyBody') {
this.props.dispatch(changeStoryBody(e.target.value))
}
}
handleChangeImage = (e) => {
if(e.target.id === 'storyImage') {
this.props.dispatch(changeStoryImage(e.target.value))
}
}
handleChangeCategoryid = (e) => {
if(e.target.id === 'storyCategoryid') {
this.props.dispatch(changeStoryCategoryid(e.target.value))
}
}
handleSubmit = (e) => {
e.preventDefault()
let storyTitleVal = this.props.storyTitle
let storyContentVal = this.props.storyContent
let storyBodyVal = this.props.storyBody
let storyImageVal = this.props.storyImage
let storyCategoryidVal = this.props.storyCategoryid
if (!storyTitleVal) {
this.storyTitleInput.focus()
return
}
this.props.dispatch(sendStory(storyTitleVal, storyContentVal, storyBodyVal, storyImageVal, storyCategoryidVal), onload)
}
render () {
const {categories} = this.props;
const data = categories;
function Complete() {
return (
<FormItem style={{marginTop: '-10px'}} label='CATEGORY'>
<AutoComplete
style={{ width: 200 }}
dataSource={data}
placeholder="try to type `b`"
filterOption={(inputValue, option) => option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1}
/>
</FormItem>
);
}
return (
<div>
<Form onSubmit={this.handleSubmit}>
<FormItem label='TITLE'>
<Input id='storyTitle' value={this.props.storyTitle} onChange={this.handleChange} ref={(input) => { this.storyTitleInput = input }} size='large' />
</FormItem>
<FormItem style={{marginTop: '-10px'}} label='CONTENT'>
<Input id='storyContent' value={this.props.storyContent} onChange={this.handleChangeContent} ref={(input) => { this.storyContentInput = input }} size='large' />
</FormItem>
<FormItem style={{marginTop: '-10px'}} label='Body'>
<TextArea rows={4} id='storyBody' value={this.props.storyBody} onChange={this.handleChangeBody} ref={(input) => { this.storyBodyInput = input }} size='large' />
</FormItem>
<FormItem style={{marginTop: '-10px'}} label='IMAGE'>
<Input id='storyImage' value={this.props.storyImage} onChange={this.handleChangeImage} ref={(input) => { this.storyImageInput = input }} size='large' />
</FormItem>
<FormItem style={{marginTop: '-10px'}} label='CATEGORY'>
<Input id='storyCategoryid' value={this.props.storyCategoryid} onChange={this.handleChangeCategoryid} ref={(input) => { this.storyCategoryidInput = input }} size='large' />
</FormItem>
<Complete />
<Button onClick={onload} disabled={this.props.isBusy} style={{marginTop: '-10px'}} type='primary' size='large' htmlType='submit' className='shadow-1'>
Send
</Button>
</Form>
</div>
)
}
}
function mapStateToProps (state) {
const { isBusy } = state.appReducer
const { storyTitle, storyContent, storyBody, storyImage, storyCategoryid } = state.storyReducer
return {
isBusy,
storyTitle,
storyContent,
storyBody,
storyImage,
storyCategoryid,
categories
}
}
const StoryCreatorConnected = connect(mapStateToProps)(StoryCreator)
export default StoryCreatorConnected
and this one of the category.actions
import { CATEGORY_CHANGE_NAME, CATEGORIES_FETCHED, CATEGORY_DELETED } from "../constants/category.constants";
import { showLoading, hideLoading } from 'react-redux-loading-bar'
import { toggleBusy } from '../actions/app.actions'
import { SaveCategory, GetCategories, DeleteCategory, UpdateCategory } from '../services/category.services'
import { history } from '../helpers/history'
export const deleteCategory = (id) => {
return { type: CATEGORY_DELETED, id: id}
}
export const changeNameCategory = (name) => {
return { type: CATEGORY_CHANGE_NAME, name: name}
}
export const sendCategory = (name) => {
return dispatch => {
dispatch(toggleBusy(true))
dispatch(showLoading())
SaveCategory(name)
.then(
response => {
dispatch(toggleBusy(false))
dispatch(hideLoading())
dispatch(changeNameCategory(''))
dispatch(fetchCategories())
},
error => {
dispatch(toggleBusy(false))
dispatch(hideLoading())
}
)
}
}
export const updateCategory = (id, name) => {
return dispatch => {
dispatch(toggleBusy(true))
dispatch(showLoading())
dispatch(changeNameCategory(''))
history.push('/admin/category')
UpdateCategory(id, name)
.then(
response => {
dispatch(toggleBusy(false))
dispatch(hideLoading())
dispatch(changeNameCategory(''))
dispatch(fetchCategories())
},
error => {
dispatch(toggleBusy(false))
dispatch(hideLoading())
}
)
}
}
export const destroyCategory = (id) => {
return dispatch => {
dispatch(toggleBusy(true))
dispatch(showLoading())
DeleteCategory(id)
.then(
response => {
dispatch(toggleBusy(false))
dispatch(hideLoading())
dispatch(fetchCategories())
},
error => {
dispatch(toggleBusy(false))
dispatch(hideLoading())
}
)
}
}
export const fetchCategories = () => {
return dispatch => {
dispatch(toggleBusy(true))
dispatch(showLoading())
GetCategories()
.then(
response => {
console.log(response)
dispatch(toggleBusy(false))
dispatch(hideLoading())
dispatch(success(response.categories))
},
error => {
console.log(error)
dispatch(toggleBusy(false))
dispatch(hideLoading())
}
)
}
function success(categories) { return { type: CATEGORIES_FETCHED, categories: categories}}
}
what it does is communicate with a service to be able to take all the loaded categories, the problem is that I can not put all those categories in a variable to be able to list them in an Autocomplete, I do not know how to load the array into a variable.
The strange thing is that I could do it before in another component that lists all the categories. There I leave the code, I hope you can help me
import React from 'react'
import {connect} from 'react-redux'
import {
Card,
Alert,
Icon,
Button,
Table,
Divider,
Popconfirm
} from 'antd';
import {Link} from 'react-router-dom'
import {fetchCategories, destroyCategory, editCategory} from '../../actions/category.actions';
const {Meta} = Card;
class Categories extends React.Component {
componentDidMount() {
this.props.dispatch(fetchCategories())
}
handleDeleteCategory(e) {
//var removedItem = fruits.splice(pos, 1);
this.props.dispatch(destroyCategory(e.id))
}
render() {
const {categories} = this.props;
const data = categories;
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id'
}, {
title: 'Nombre de la categoria',
dataIndex: 'name',
key: 'name'
}, {
title: 'Acciones',
key: 'action',
render: (text, record) => (<span>
<Link to={`/admin/category/edit/${record.id}/${record.name}`}>Editar</Link>
<span className="ant-divider"/>
<a onClick={() => this.handleDeleteCategory(record)}>Eliminar</a>
</span>)
}
];
if (this.props.categories.length == 0)
return (<Alert message="No hay categorias para mostrar." type="error"/>);
return <Table dataSource={data} columns={columns}/>
}
}
function mapStateToProps(state) {
const {categories} = state.categoryReducer
return {categories}
}
const connectedCategories = connect(mapStateToProps)(Categories)
export default connectedCategories
solve it, forget to import the variable to the mapStateToProps and to reduce. Thank you!