React component not receiving props on Redux store update - javascript

I am building a social media application using the MERN stack and Redux as the state manager. I have a feed component which renders PostItem components which display the post and allow for actions such as liking, and commenting. I also have a Post component that renders the same PostItem component that opens when the user clicks the comment button on the PostItem component in the feed. When I like a post via the feed component it receives the updated props and rerenders the component showing the changes. However when I click the like button in the Post component it updates the Redux store but does not receive the updated props.
Feed.js
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPosts } from '../../actions/post';
// Components
import UserProfileCard from './UserProfileCard';
import PostForm from './PostForm';
import Footer from '../layout/Footer';
import PostItem from '../posts/PostItem';
import Spinner from '../layout/Spinner';
const Feed = ({ getPosts, post: { posts, loading } }) => {
//Same as component did mount
useEffect(() => {
getPosts();
}, [getPosts]);
return (
<Fragment>
<div className='main-container mt-3'>
<div className='container'>
<div className='row'>
<UserProfileCard />
<PostForm />
</div>
{loading ? (
<Spinner />
) : (
posts.map(post => <PostItem key={post._id} post={post} />)
)}
</div>
</div>
<Footer />
</Fragment>
);
};
Feed.propTypes = {
getPosts: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPosts })(Feed);
Post.js
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPost } from '../../actions/post';
import { Link } from 'react-router-dom';
// Components
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import PostCommentForm from './PostCommentForm';
import Comment from './Comment';
// Assets
import { ArrowLeft } from 'react-feather';
const Post = ({ getPost, post: { post, loading }, match }) => {
useEffect(() => {
// get id from url in params for getPost function
getPost(match.params.id);
}, [getPost]);
return loading || post === null ? (
<Spinner />
) : (
<div className='main-container mt-3'>
<div className='container'>
<div className='row'>
{/* TODO ADD BROWSER HISTORY FUNCTIONALITY TO ALLOW USER TO GO BACK TO PROFILE OR FEED */}
<Link className='mb-1' to='/feed'>
<button className='btn btn-logo-color'>
<ArrowLeft />
</button>
</Link>
<PostItem key={post._id} post={post} />
<PostCommentForm />
</div>
<Comment />
</div>
</div>
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
PostItem.js
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Moment from 'react-moment';
import { connect } from 'react-redux';
import { addLike, removeLike, deletePost } from '../../actions/post';
// Assets
import { ThumbsUp, ThumbsDown, MessageSquare, XCircle } from 'react-feather';
import avi from '../assets/default-avatar.png';
const PostItem = ({
auth,
post: { _id, text, firstname, lastname, user, likes, comments, date },
addLike,
removeLike,
deletePost
}) => {
return (
<div className='card mt-1 post'>
<div className='card-body'>
<div className='row'>
<div className='col-lg-2'>
<Link className='feed-link' to={`/profile/${user}`}>
<img src={avi} alt='avatar' className='avatar' />
<h5 className='card-title mt-2'>
{firstname} {lastname}
</h5>
</Link>
</div>
<div className='col-lg-10'>
<p className='card-text'>{text}</p>
<p className='text-muted post-date'>
<Moment format='LLL'>{date}</Moment>
</p>
<div className='post-buttons'>
<button
type='button'
className='btn btn-outline-primary mr-1'
onClick={e => addLike(_id)}
>
<ThumbsUp />
<span className='badge badge-light'>
{likes.length > 0 && <span>{likes.length}</span>}
</span>
</button>
<button
type='button'
className='btn btn-outline-danger mr-1'
onClick={e => removeLike(_id)}
>
<ThumbsDown />
</button>
{/* TODO ADD CONDITIONAL RENDERING TO REMOVE WHEN POST IS OPEN */}
<Link to={`/post/${_id}`}>
<button type='button' className='btn btn-outline-info mr-1'>
<MessageSquare />
<span className='badge badge-light'>
{comments.length > 0 && (
<span className='comment-count'>{comments.length}</span>
)}
</span>
</button>
</Link>
{!auth.loading && user === auth.user._id && (
<button
type='button'
className='btn btn-outline-danger mr-1'
onClick={() => deletePost(_id)}
>
<XCircle />
</button>
)}
</div>
</div>
</div>
</div>
</div>
);
};
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
addLike: PropTypes.func.isRequired,
removeLike: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps, { addLike, removeLike, deletePost })(
PostItem
);
Post Reducer
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
posts: [payload, ...state.posts],
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post._id !== payload),
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.postId ? { ...post, likes: payload.likes } : post
),
loading: false
};
case ADD_COMMENT:
return {
...state,
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
comment => comment._id !== payload
)
},
loading: false
};
default:
return {
...state
};
}
}
Post actions
import axios from 'axios';
import { setAlert } from './alert';
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from './types';
//Get posts
export const getPosts = () => async dispatch => {
try {
const res = await axios.get('/api/posts');
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add like
export const addLike = postId => async dispatch => {
try {
const res = await axios.put(`/api/posts/like/${postId}`);
dispatch({
type: UPDATE_LIKES,
payload: { postId, likes: res.data }
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Remove like
export const removeLike = postId => async dispatch => {
try {
const res = await axios.put(`/api/posts/unlike/${postId}`);
dispatch({
type: UPDATE_LIKES,
payload: { postId, likes: res.data }
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add Post
export const addPost = formData => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post('/api/posts', formData, config);
dispatch({
type: ADD_POST,
payload: res.data
});
dispatch(setAlert('Post Created', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Delete Post
export const deletePost = id => async dispatch => {
try {
const res = await axios.delete(`/api/posts/${id}`);
dispatch({
type: DELETE_POST,
payload: id
});
dispatch(setAlert('Post Removed', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
//Get post
export const getPost = id => async dispatch => {
try {
const res = await axios.get(`/api/posts/${id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add Comment
export const addComment = (postId, formData) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post(
`/api/posts/comment/${postId}`,
formData,
config
);
dispatch({
type: ADD_COMMENT,
payload: res.data
});
dispatch(setAlert('Comment Added', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Delete Comment
export const deleteComment = (postId, commentId) => async dispatch => {
try {
const res = await axios.delete(`/api/posts/comment/${postId}/${commentId}`);
dispatch({
type: REMOVE_COMMENT,
payload: commentId
});
dispatch(setAlert('Comment Removed', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};

You're updating the posts array here which is what you use to render the PostItems in Feed.
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.postId ? { ...post, likes: payload.likes } : post
),
loading: false
};
However in Post.js you use the Post object, not the Posts array. Post has not been updated by the UPDATE_LIKES action so your component doesn't re-render.

Related

facing isssue while deleting a target comment

When I click the delete button, the correct comment id and text is logged in the console.
But the comment before it is deleted.
This implies that the target id changes between click and deletion.
Toggler component to toggle the delete button -
list post is mapped to create:
component{post title, body, likes, comment}.
code for comment component:
import React, { Fragment, useState, useEffect } from "react";
import "../../styles/posts/postComponent.scss";
import { connect } from "react-redux";
import { removeComment } from "../../redux/actions/#posts";
import PropTypes from "prop-types";
const Comment = ({ admin, auth, post_id, removeComment, comments }) => {
const commentList = comments
? comments.map(comment => {
return (
<div className='c-img-text'>
<img
className='c-img'
height={"40px"}
src={comment.avatar}
alt='pic'
onClick={() => console.log(comment.avatar)}
/>
<div className='c-nt'>
<a href='#' className='c-n'>
{comment.name}
</a>
<span className='c-t'>{comment.text.toString()}</span>
<i className='c-d'>{comment.date}</i>
</div>
<Toggler
auth={auth}
pst_id={post_id}
adm_id={admin._id}
cmt_id={comment._id}
cmt_user={comment.user}
removeComment={removeComment}
cmt_txt={comment.text}
/>
</div>
);
})
: "";
return (
<Fragment>
<div className='c-container'>{commentList}</div>
</Fragment>
);
};
const Toggler = ({
auth,
pst_id,
adm_id,
cmt_id,
cmt_txt,
cmt_user,
removeComment
}) => {
const [showDelete, setShowDelete] = useState(false);
const deleteComment = cmt_id => {
removeComment(pst_id, cmt_id);
console.log(cmt_txt, cmt_id, pst_id);
};
return (
<div onClick={() => setShowDelete(!showDelete)}>
{auth && adm_id ? adm_id === cmt_user && <div>...</div> : ""}
{showDelete && (
<div
className='c-delete'
onClick={async () => await deleteComment(cmt_id)}
>
icon
</div>
)}
</div>
);
};
Comment.propTypes = {
admin: PropTypes.object.isRequired,
auth: PropTypes.bool.isRequired,
post_id: PropTypes.object.isRequired,
removeComment: PropTypes.func.isRequired,
comments: PropTypes.object.isRequired
};
const mapStateToProps = state => {
return { state };
};
export default connect(mapStateToProps, { removeComment })(Comment);
code for action (redux):
//remove comment
export const removeComment = (post_id, comment_id) => async dispatch => {
try {
setAuthToken(localStorage.token);
const res = await axios.delete(
`/api/posts/comments/${post_id}/${comment_id}`
);
dispatch({
type: REMOVE_COMMENT,
payload: { postId: post_id, comments: res.data }
});
console.log("from", comment_id);
} catch (error) {
console.error(error.message);
dispatch({
type: POST_ERROR
});
}
};
code for backend:
//#route DELETE api/posts/comments/:post_id/:comment_id
//#desc remove a comment
//#access Private
const removeComment = async (req, res) => {
try {
const post = await Post.findById(req.params.post_id);
//Pull out comment
const comment = post.comments.find(
comment => comment.id === req.params.comment_id
);
//make sure comment exists
if (!comment) {
res.status(404).json({ error: "Comment does not exist" });
}
//check admin
if (comment.user.toString() !== req.admin.id) {
return res.status(401).json({ msg: "User not authorized" });
}
//Get remove index
const removeIndex = post.comments
.map(comment => comment.user.toString())
.indexOf(req.admin.id);
post.comments.splice(removeIndex, 1);
await post.save();
res.json(post.comments);
} catch (error) {
console.error(error.message);
res.status(500).send("Server Error");
}
};
TECHSTACK :mongodb, express, reactjs , nodejs
Try this, I changed the removeIndex statement.
const removeComment = async (req, res) => {
try {
const post = await Post.findById(req.params.post_id);
//Pull out comment
const comment = post.comments.find(
comment => comment.id === req.params.comment_id
);
//make sure comment exists
if (!comment) {
res.status(404).json({ error: "Comment does not exist" });
}
//check admin
if (comment.user.toString() !== req.admin.id) {
return res.status(401).json({ msg: "User not authorized" });
}
//Get remove index
const removeIndex = post.comments.indexOf(comment);
post.comments.splice(removeIndex, 1);
await post.save();
res.json(post.comments);
} catch (error) {
console.error(error.message);
res.status(500).send("Server Error");
}
};

Maximum update depth exceeded in React Infinite Scroll

I am trying to implement a lazy loading in a MERN stack app like in producthunt. I want to have the posts created on the current date shown by default. If the user scroll down, it will fetch more data on the previous date. I am using react infinite scroll. However, it seems like the app requests to api like an infinite loop without listening on scrolling. I got the following error.
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
The function is async/await so I don't understand why it keeps calling new requests even though the old request is not resolved yet.
In a Post components
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import UserItem from '../users/UserItem';
import TopDiscussion from '../TopDiscussion';
import SmallAbout from '../SmallAbout';
import { getPostsByDate } from '../../actions/post';
import Moment from 'react-moment';
import InfiniteScroll from 'react-infinite-scroller';
const Posts = ({ getPostsByDate, post: { posts, loading } }) => {
const now = new Date();
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate()
);
// startOfToday = startOfToday -1
useEffect(() => {
getPostsByDate(startOfToday);
}, [getPostsByDate]);
const [date, setDate] = useState(startOfToday);
const [shown, setShown] = useState();
const getPosts = () => {
getPostsByDate(date);
let count = new Date(date);
count.setDate(count.getDate() - 1);
setDate(count);
};
return loading ? (
<Spinner />
) : (
<div className='main-grid'>
<div className='posts-grid'>
<h1 className='large text-primary'>Ideas</h1>
<div className='posts'>
<div className='post-dummy'>
<InfiniteScroll
dataLength={posts.length}
pageStart={0}
loadMore={getPosts}
hasMore={posts && posts.length < 10}
loader={
<div className='loader' key={0}>
Loading ...
</div>
}
>
{posts
.sort((a, b) =>
a.likes.length > b.likes.length
? -1
: b.likes.length > a.likes.length
? 1
: 0
)
.map(post => (
<PostItem key={post._id} post={post} />
))}
</InfiniteScroll>
</div>
</div>
</div>
<div className='right-panel-grid'>
<SmallAbout />
<UserItem />
<TopDiscussion posts={posts} />
<div
className='fb-group'
data-href='https://www.facebook.com/groups/ideatoshare/'
data-width='350'
data-show-social-context='true'
data-show-metadata='false'
></div>
<iframe
title='producthunt'
style={{ border: 'none' }}
src='https://cards.producthunt.com/cards/posts/168618?v=1'
width='350'
height='405'
frameBorder='0'
scrolling='no'
allowFullScreen
></iframe>
</div>
</div>
);
};
Posts.propTypes = {
getPostsByDate: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(
mapStateToProps,
{ getPostsByDate }
)(Posts);
Post reducer
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
UPDATE_LIKE,
UPDATE_COMMENT_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT,
ADD_SUB_COMMENT,
REMOVE_SUB_COMMENT,
UPDATE_STATUS
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: [...state.posts, ...payload],
// posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
post: payload,
// posts: [payload, ...state.posts],
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_COMMENT_LIKES:
return {
...state,
post: {
...state.post,
comments: payload
},
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.id ? { ...post, likes: payload.likes } : post
),
loading: false
};
case UPDATE_LIKE:
return {
...state,
post: { ...state.post, likes: payload },
loading: false
};
case UPDATE_STATUS:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.id ? { ...post, status: payload.status } : post
),
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post._id !== payload),
loading: false
};
case ADD_COMMENT:
return {
...state,
// payload is all the comments
post: { ...state.post, comments: payload },
loading: false
};
case ADD_SUB_COMMENT:
return {
...state,
// payload is all the comments of a post
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
comment => comment._id !== payload
),
loading: false
}
};
case REMOVE_SUB_COMMENT:
return {
...state,
post: {
...state.post,
comments: payload
// comments: state.post.comments.map(comment =>
// {
// if (comment._id === payload.commentId) {
// comment.subComments.filter(
// subcomment => subcomment._id === payload.subcommentId
// );
// }
// }
// )
},
loading: false
};
default:
return state;
}
}
Post action
//GetTodayPost
export const getPostsByDate = date => async dispatch => {
try {
const res = await axios.get(`/api/posts/${date}`);
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
post API
router.get('/:date', async (req, res) => {
try {
const startOfToday = new Date(req.params.date);
const endOfToday = new Date(req.params.date);
endOfToday.setDate(endOfToday.getDate() + 1);
const posts = await Post.find({
date: { $gte: startOfToday, $lte: endOfToday }
}).sort({
date: -1
});
res.json(posts);
} catch (err) {
console.error(err.message);
res.send(500).send('Server Error');
}
});
Edit: I have updated your repo with a working example.. Your issue is that your API is not 'unlimited', as you claimed, and you do in fact need to check if all posts have been loaded or not.. Using the example I supplied along with the updates I made to your repo, you should be able to figure out things from here.
Ok.. so after some testing with InfiniteScroll, this appears to be happening because your hasMore property always equals true... You have to specify some type of condition so that InfiniteScroll knows when to, and when not to, load more data.
I got the same error as you before adding a check, which tells InfiniteScroll that there is no more data to load.
I have built the following example to show how to use InfiniteScroll
You can view a live demo here
PostsContainer.js
import React, { useState, useEffect } from "react";
import Posts from "./Posts";
import InfiniteScroll from "react-infinite-scroller";
const loadingStyle = {
textAlign: "center",
fontSize: "48px",
color: "red"
};
function PostsContainer({ url, itemsToDisplay = 5 }) {
const [data, setData] = useState();
const [shownData, setShownData] = useState();
useEffect(() => {
(async () => {
let items = await fetchPosts(url);
let itemsToShow = selectNItems(items, itemsToDisplay);
setShownData(itemsToShow);
setData(items);
})();
}, [url]);
async function fetchPosts(url) {
let res = await fetch(url);
return await res.json();
}
const selectNItems = (obj, n) => {
return obj.slice(0, n);
}
const loadMorePosts = () => {
let items =
data &&
shownData &&
selectNItems(data, shownData.length + itemsToDisplay)
setShownData(items);
};
return (
<InfiniteScroll
pageStart={0}
loadMore={loadMorePosts}
hasMore={data && shownData && data.length > shownData.length}
loader={<div style={loadingStyle}>Loading ...</div>}
useWindow={true}
>
<Posts posts={shownData} />
</InfiniteScroll>
);
}
export default PostsContainer;
Posts.js
import React from 'react';
import Post from './Post';
const headingStyle = {
textAlign: 'center',
}
function Posts({ posts }) {
return(
<div>
<h1 style={headingStyle}>Posts</h1>
{posts && posts.length > 0 && posts.map((p, i) => <Post key={i} data={p} index={i} />)}
</div>
);
}
export default Posts;
Post.js
import React from "react";
const containerStyle = {
border: "1px solid black",
margin: "10px auto",
maxWidth: "50vw",
padding: '0px 10px 0px 0px'
};
const postHeaderStyle = {
textAlign: "center",
padding: "0px"
};
function Post({ data, index }) {
return (
<div style={containerStyle}>
{index !== "" && <h3 style={postHeaderStyle}>Post #{index}</h3>}
<ul>
<li>
<b>userId:</b> {data.userId}
</li>
<li>
<b>id:</b> {data.id}
</li>
<li>
<b>title:</b> {data.title}
</li>
<li>
<b>body:</b> {data.body}
</li>
</ul>
</div>
);
}
export default Post;
index.js
import React from "react";
import { render } from "react-dom";
import PostsContainer from "./Components/PostsContainer";
function App() {
return (
<PostsContainer
itemsToDisplay={5}
url="https://jsonplaceholder.typicode.com/posts"
/>
);
}
render(<App />, document.getElementById("root"));

How to show props from reducers in component

I have a problem when want to show
const mapStateToProps = state => {
return {
loading: state.auth.loading,
error: state.auth.error,
userId: state.auth.userId,
tokenId: state.auth.token
}
}
this in my function
register = (event) => {
event.preventDefault()
this.props.onAuth( this.state.email, this.state.password, this.state.isSignup );
localStorage.setItem('token', this.props.tokenId);
localStorage.setItem('userId', this.props.userId);
}
I see token and userId after the second click. But I can't see after the first click. What I need more to show immediately?
This is my auth.js reducers
import * as actionTypes from '../actions/actionsTypes';
import { updateObject } from '../utility';
const initialState = {
token: null,
userId: null,
error: null,
loading: false
};
const authStart = ( state, action ) => {
return updateObject( state, { error: null, loading: true } );
};
const authSuccess = (state, action) => {
return updateObject( state, {
token: action.idToken,
userId: action.userId,
error: null,
loading: false
} );
};
const authFail = (state, action) => {
return updateObject( state, {
error: action.error,
loading: false
});
}
const reducer = ( state = initialState, action ) => {
switch ( action.type ) {
case actionTypes.AUTH_START: return authStart(state, action);
case actionTypes.AUTH_SUCCESS: return authSuccess(state, action);
case actionTypes.AUTH_FAIL: return authFail(state, action);
default:
return state;
}
};
export default reducer;
But, after the first click, I got token in my render function.
{this.props.tokenId}
Could you please help me? I think I need to use async/await. But I am not sure.
Here you go Header.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as actions from '../../store/actions/index'
import PropTypes from 'prop-types'
import './header.css'
class Header extends Component {
constructor(props) {
super(props)
this.state = {
email: '',
password: '',
isSignup: true,
token: false
}
this.handleChange = this.handleChange.bind(this);
}
handleChange (evt) {
this.setState({ [evt.target.name]: evt.target.value });
}
switchAuthModeHandler = (event) => {
event.preventDefault()
this.setState(prevState => {
return {
isSignup: !prevState.isSignup
}
})
}
register = (event) => {
event.preventDefault()
this.props.onAuth( this.state.email, this.state.password, this.state.isSignup );
localStorage.setItem('token', this.props.tokenId);
localStorage.setItem('userId', this.props.userId);
}
render() {
let regBtn = ''
if (this.state.isSignup) {
regBtn = 'Register'
}
else {
regBtn = 'Login'
}
let login = null
if(!this.props.tokenId) {
login = (
<div className="login">
<form onSubmit={this.register}>
<input type="email" placeholder="email" name="email" onChange={this.handleChange} />
<input type="password" placeholder="password" name="password" onChange={this.handleChange} />
<button>{regBtn}</button>
</form>
<div onClick={this.switchAuthModeHandler} className="switch">Switch to {this.state.isSignup ? 'Login' : 'Register'}</div>
</div>
)
}
else {
login = (
<div>
<p>Hello: {this.props.userId}</p>
<button>Logout</button>
</div>
)
}
if(this.props.loading) {
login = <div>Loading...</div>
}
return (
<div>
<div className="header-inner">
{this.props.tokenId}
{login}
<img src="http://civcic.com/assets/images/header-bg.jpg" alt="img" />
<div className="header-content">
<h2>React.JS DEVELOPER</h2>
<a className="knowmore-btn" href="https://www.upwork.com/freelancers/~01f507600be26cc2a3" rel="noopener noreferrer" target="_blank">Upwork profile</a><br />
<a className="knowmore-btn" href="https://www.linkedin.com/in/boris-civcic-37244378/" rel="noopener noreferrer" target="_blank">Linkedin</a><br />
<a className="knowmore-btn" href="https://github.com/fixman93" rel="noopener noreferrer" target="_blank">GitHub</a>
</div>
</div>
</div>
)
}
}
Header.defaultProps = {
tokenId: ''
}
Header.propTypes = {
tokenId: PropTypes.string
}
const mapStateToProps = state => {
return {
loading: state.auth.loading,
error: state.auth.error,
userId: state.auth.userId,
tokenId: state.auth.token
}
}
const mapDispatchToProps = dispatch => {
return {
onAuth: ( email, password, isSignup) => dispatch( actions.auth(email, password, isSignup))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header)
import axios from 'axios';
import * as actionTypes from './actionsTypes';
export const authStart = () => {
return {
type: actionTypes.AUTH_START
}
}
export const authSuccess = (token, userId) => {
return {
type: actionTypes.AUTH_SUCCESS,
idToken: token,
userId: userId
}
}
export const authFail = (error) => {
return {
type: actionTypes.AUTH_FAIL,
error: error
};
};
export const auth = (email, password, isSignup) => {
return dispatch => {
dispatch(authStart());
const authData = {
email: email,
password: password,
fullName: 'Boris Civcic',
role: 'admin',
returnSecureToken: true
};
let url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyC5nW8-XOJADEvU7Mi7sgmhUNhHfRxXNQI';
if (!isSignup) {
url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyC5nW8-XOJADEvU7Mi7sgmhUNhHfRxXNQI';
}
axios.post(url, authData)
.then(response => {
console.log(response);
dispatch(authSuccess(response.data.idToken, response.data.localId));
// dispatch(checkAuthTime(response.data.expiresIn));
})
.catch(err => {
dispatch(authFail(err.response.data.error));
})
};
};
this is auth.js action
and this is utility
export const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
};
};
In your register handler, onAuth is an asynchronous action but you've populated localStorage immediately. You should wait onAuth returns and then set your localStorage items.
first return a promise from your thunk ( simply by adding return before axios ):
...
return axios.post(url, authData)
.then(response => {
console.log(response);
dispatch(authSuccess(response.data.idToken, response.data.localId));
// dispatch(checkAuthTime(response.data.expiresIn));
})
.catch(err => {
dispatch(authFail(err.response.data.error));
})
...
Then set your localStorage items like this:
register = (event) => {
event.preventDefault();
this.props.onAuth( this.state.email, this.state.password, this.state.isSignup )
.then(() => {
localStorage.setItem('token', this.props.tokenId);
localStorage.setItem('userId', this.props.userId);
});
}

Ruby on Rails and React/Redux, Uncaught TypeError: (0 , _tasks.getTask) is not a function

I want to make a transition from a link to an article, to the article itself, when I click on a link, the construction is triggered: Not found. Tell me what I'm doing wrong.
task_conteiner.js
import React, { Component } from 'react';
import { browserHistory } from 'react-router';
import { Link } from 'react-router';
export default class TasksContainer extends Component {
onShowMoreTask(id) {
browserHistory.push(`#/tasks/${id}`);
location.reload()
}
renderTasks() {
let filterComleted = this.props.tasks.tasks
let str = '★'
let style
if (this.props.Completed === "task.completed") {
filterComleted = filterComleted.filter(task => task.completed);
} else {
filterComleted = filterComleted.filter(task => !task.completed);
}
return filterComleted.map((task) => {
if(task.completed){
style = {
textDecoration: 'line-through'
}
}
return (
<div key={task.id}>
<li className="todo">
<div className="container">
<div className="col-md-3" onClick={() => this.onShowMoreTask( task.id )}>{ task.title }</div>
<div className="col-md-3" style={style}>{ task.description }</div>
<div className="col-md-3" style={style}>{ task.due_date }</div>
<div className="col-md-1 star">{ str.repeat(task.priority) }</div>
<span onClick={() => this.props.onCompletedTask(task.id, task.completed)} className={task.completed ? "glyphicon glyphicon-repeat" : "glyphicon glyphicon-ok"} title={task.active ? "Mark active" : "Mark completed"}></span>
<span onClick={() => this.props.onEditTask( this.props.editId, task.id )} className="glyphicon glyphicon-pencil" title="Edit task"></span>
<span onClick={() => this.props.onDeleteTask(task.id)} className="glyphicon glyphicon-trash" title="Delete"></span>
</div>
</li>
</div>
);
});
}
render() {
return (
<div>
{this.renderTasks()}
</div>
);
}
}
task_details.js
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import Exit from '../authentication/exit';
import { browserHistory } from 'react-router';
import { getTask } from '../../actions/tasks';
import TasksList from './tasks_list';
import '../../index.css';
import Link from 'react-router'
class TaskDetails extends Component {
componentDidMount () {
let id = this.props.params.id;
this.props.onGetTask(id);
};
render() {
const { task } = this.props
console.log(this.props.location.pathname, "xxxxxxxx")
return (
<div>
{ this.props.task ?
<div className="container">
<h2 className="text-center">{task.title}</h2>
<div className="col-md-2">
<h4 className="pull-right"><i>{task.due_date}</i></h4>
</div>
<div className="clearfix"></div>
<div className="description">
<p>{task.description}</p>
</div>
</div>
:
<div className="container">
<div><h2>Not found</h2></div>
</div>
}
</div>
);
}
};
export default connect(
state => ({
task: state.tasks.item
}),
dispatch => ({
onGetTask: (id) => {
dispatch(getTask(id));
},
})
)(TaskDetails);
The task_details triggers the construction:
<div className="container">
<div><h2>Not found</h2></div>
</div>
There is an error in the console:
/tasks/6 xxxxxxxx
task_details.js:50 Uncaught TypeError: (0 , _tasks.getTask) is not a function
at Object.onGetTask (task_details.js:50)
at TaskDetails.componentDidMount (task_details.js:14)
at ReactCompositeComponent.js:265
at measureLifeCyclePerf (ReactCompositeComponent.js:75)
at ReactCompositeComponent.js:264
at CallbackQueue.notifyAll (CallbackQueue.js:76)
at ReactReconcileTransaction.close (ReactReconcileTransaction.js:80)
at ReactReconcileTransaction.closeAll (Transaction.js:206)
at ReactReconcileTransaction.perform (Transaction.js:153)
at batchedMountComponentIntoNode (ReactMount.js:126)
Thank you for your help.
and
reducers/task.js
export default function tasks(state = {
tasks: [],
edit: '',
sortBy: {title: "priority", asc: "desc"}
}, action) {
switch (action.type) {
case "FETCH_TODOS_SUCCESS":
return {
...state,
tasks: action.payload
};
case "GET_TASKS":
return {
...state,
tasks: action.payload
}
case "ADD_TASK":
return {
...state,
tasks: [action.payload, ...state.tasks]
}
case "DELETE_TASK":
state.tasks = state.tasks.filter(t => t.id !== action.payload);
return {
...state,
tasks: [...state.tasks]
}
case "EDIT_TASK":
state.tasks = state.tasks.filter(t => t.id !== action.payload.id);
return {
...state,
tasks: [action.payload, ...state.tasks]
}
case "COMPLITED_TASK":
state.tasks = state.tasks.filter(t => t.id !== action.payload.id);
return {
...state,
tasks: [action.payload, ...state.tasks]
};
case "EDIT_ID":
return {
...state,
edit: action.payload
}
case "SORT_BY":
return {
...state,
sortBy: action.payload
}
default:
return state;
}
}
action/task.js
import axios from 'axios';
import cookie from 'react-cookies';
//const API_URL = `https://evening-taiga-79121.herokuapp.com/todos`;
const API_URL = `http://localhost:3000/todos`;
let headers = { 'Content-Type': 'application/json', };
const token = cookie.load('token');
export function fetchTasks(user_id){
return function(dispatch, getState) {
let body = JSON.stringify({ token: token });
headers['Authorization'] = `Bearer ${token}`;
axios.get(`${API_URL}`, { headers, body })
.then(res => {
if (res.status === 200) {
dispatch({ type: 'GET_TASKS', payload: res.data });
}
})
.catch(e => {
console.error("error: ", e);
})
}
}
export function deleteTask(id){
return function(dispatch, getState) {
let body = { token: token };
axios.delete(`${API_URL}/${id}`, { params: body, headers: headers })
.then(res => {
dispatch({ type: 'DELETE_TASK', payload: id });
})
.catch(id => {
console.error("error", id);
})
}
}
export function addTask(task){
return function(dispatch, getState) {
let body = JSON.stringify({todo: task, token: token});
console.log(body);
axios.post(API_URL, body, { headers: headers })
.then(res => {
dispatch({ type: 'ADD_TASK', payload: res.data });
})
.catch(e => {
console.error(e);
})
}
}
export function completedTask(id, complete){
return function(dispatch, getState) {
if (complete === true) {
complete = false
} else {
complete = true
}
let task = {id: id, completed: complete};
let body = {todo: task, token: token};
axios.patch(`${API_URL}/${task.id}`, body, { headers: headers })
.then(res => {
dispatch({ type: 'COMPLITED_TASK', payload: res.data });
})
.catch(e => {
console.error("error: ", e);
})
}
}
export function sortTasks(sortBy){
return function(dispatch, getState) {
let body = JSON.stringify({ token: token, sortByTitle: sortBy.title, sortByAsc: sortBy.asc });
axios.post(`${API_URL}/sort`, body, { headers: headers })
.then(res => {
console.log(res);
if (res.status === 200) {
dispatch({ type: 'SORT_BY', payload: sortBy });
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: res.data });
}
})
.catch(e => {
console.error("error: ", e);
})
}
}
export function editTask(task){
return function(dispatch, getState) {
let body = JSON.stringify({todo: task, token: token});
axios.patch(`${API_URL}/${task.id}`, body, { headers: headers })
.then(res => {
dispatch({ type: 'EDIT_TASK', payload: res.data });
})
.catch(e => {
console.error("error: ", e);
})
}
}

Dispatching an in-progress action in Redux

I have a signup form, which posts data asynchronously to an API and then does some stuff based on the response. I am dispatching a "signup_in_progress" action as soon as the function is called, with a payload of "true", and update my state based on that, then dispatching this action with a payload of "false" when the promise is resolved.
I can see that the dispatch happens as intended by putting a console.log() statement in the reducer.
What I would like to happen is for a spinner to appear instead of the signup form when the signup_in_progress piece of state is "true." But that's not happening. Any idea what I'm missing?
My action:
export function signUpUser(props) {
return function(dispatch) {
dispatch({ type: SIGNUP_IN_PROGRESS, payload: true });
axios
.post(`${ROOT_URL}/signup`, props)
.then(() => {
dispatch({ type: SIGNUP_IN_PROGRESS, payload: false });
dispatch({ type: SIGNUP_SUCCESS });
browserHistory.push(`/signup/verify-email?email=${props.email}`);
})
.catch(response => {
dispatch({ type: SIGNUP_IN_PROGRESS, payload: false });
dispatch(authError(SIGNUP_FAILURE, response.response.data.error));
});
};
}
The relevant part of my reducer:
import {
...
SIGNUP_IN_PROGRESS,
SIGNUP_SUCCESS,
SIGNUP_FAILURE...
} from '../actions/types';
export default function(state = {}, action) {
switch (action.type) {
...
case SIGNUP_IN_PROGRESS:
return { ...state, signing_up: action.payload };
...
My connected component:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import * as actions from '../../actions';
import SignupFirstPage from './signupComponents/signupFirstPage';
import SignupSecondPage from './signupComponents/signupSecondPage';
import SignupThirdPage from './signupComponents/SignupThirdPage';
import Spinner from 'react-spinkit';
class SignUp extends Component {
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.state = {
page: 1
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
nextPage() {
this.setState({ page: this.state.page + 1 });
}
previousPage() {
this.setState({ page: this.state.page - 1 });
}
handleFormSubmit(props) {
this.props.signUpUser(props);
}
render() {
const { handleSubmit } = this.props;
const { page } = this.state;
if (this.props.signingUp) {
return (
<div className="dashboard loading">
<Spinner name="chasing-dots" />
</div>
);
}
return (
<div>
{page === 1 && <SignupFirstPage onSubmit={this.nextPage} />}
{page === 2 && (
<SignupSecondPage
previousPage={this.previousPage}
onSubmit={this.nextPage}
/>
)}
{page === 3 && (
<SignupThirdPage
previousPage={this.previousPage}
onSubmit={values => this.props.signUpUser(values)}
/>
)}
<div>
{this.props.errorMessage &&
this.props.errorMessage.signup && (
<div className="error-container">
Oops! {this.props.errorMessage.signup}
</div>
)}
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
singingUp: state.auth.signing_up,
errorMessage: state.auth.error
};
}
SignUp = reduxForm({ form: 'wizard' })(SignUp);
export default connect(mapStateToProps, actions)(SignUp);
You just have a typo in your code:
function mapStateToProps(state) {
return {
singingUp: state.auth.signing_up, // TYPO!!
errorMessage: state.auth.error
};
}
should be changed to
function mapStateToProps(state) {
return {
signingUp: state.auth.signing_up,
errorMessage: state.auth.error
};
}
I recommend using redux-pack which allows you to handle three phases of the request in the reducer: start, success and error.
Then you can use the start handler to set the boolean to true, and on success set it to false.

Categories

Resources