I am using Next.js and React and struck on this problem for days. So in Next.js pages i have dynamic page [postid].js, as follow,
import Layout from "../../components/layout";
import { useRouter } from "next/router";
import Singlepost from "../../components/post/singlepost";
export default function Post() {
const router = useRouter();
const postId = router.query.postid
return (
<Layout>
{console.log(router.query.postid)}
<h1>{router.query.postid}</h1>
<p>This is the post content.</p>
<Singlepost postId={postId}/>
</Layout>
);
}
Here I am sending query params as props(postId) to singlepost component.
But in the Singlepost component i am trying use this props inside componentDidMount which then makes api call to get data, but postId shows undefined so api call fails to fetch data.
componentDidMount() {
const postId = this.props.postId;
console.log("postId:", postId);
singlePost(postId).then(data => {
if (data.error) {
console.log(data.error);
} else {
this.setState({
post: data,
likes: data.likes.length,
like: this.checkLike(data.likes),
comments: data.comments
});
}
});
}
So how do i get the prop value in componentDidMount? Or is there any other way should i approach this problem?
also here's my complete singlepost.js for Reference,
import React, { Component } from "react";
import {
singlePost,
remove,
like,
unlike
} from "../../components/post/apiPost";
import Link from "next/link";
import { isAuthenticated } from "../../components/auth";
import DefaultPost from "../../public/images/courses.png";
import Router, { withRouter } from "next/router";
class SinglePost extends Component {
constructor(props) {
super(props);
this.state = {
post: "",
redirectToHome: false,
redirectToBack: false,
redirectToSignin: false,
like: false,
likes: 0,
comments: [],
};
}
checkLike = likes => {
const userId = isAuthenticated() && isAuthenticated().user._id;
let match = likes.indexOf(userId) !== -1;
return match;
};
componentDidMount() {
const postId = this.props.postId;
console.log("postId:", postId);
singlePost(postId).then(data => {
if (data.error) {
console.log(data.error);
} else {
this.setState({
post: data,
likes: data.likes.length,
like: this.checkLike(data.likes),
comments: data.comments
});
}
});
}
updateComments = comments => {
this.setState({ comments });
};
likeToggle = () => {
if (!isAuthenticated()) {
this.setState({ redirectToSignin: true });
return false;
}
let callApi = this.state.like ? unlike : like;
const userId = isAuthenticated().user._id;
const postId = this.state.post._id;
const token = isAuthenticated().token;
callApi(userId, token, postId).then(data => {
if (data.error) {
console.log(data.error);
} else {
this.setState({
like: !this.state.like,
likes: data.likes.length
});
}
});
};
deletePost = () => {
const postId = this.props.quota;
const token = isAuthenticated().token;
remove(postId, token).then(data => {
if (data.error) {
console.log(data.error);
} else {
this.setState({ redirectToBack: true });
}
});
};
deleteConfirmed = () => {
let answer = window.confirm("Are you sure you want to delete your post?");
if (answer) {
this.deletePost();
}
};
renderPost = post => {
console.log(post);
const posterId = post.postedBy ? `/user/${post.postedBy._id}` : "";
const posterName = post.postedBy ? post.postedBy.name : " Unknown";
const { like, likes, comments } = this.state;
return (
<div className="column">
<img
src={`${process.env.REACT_APP_API_URL}/post/photo/${post._id}`}
alt={post.title}
onError={i => (i.target.src = `${DefaultPost}`)}
className="img-thunbnail"
style={{
height: "300px",
width: "100%",
objectFit: "cover"
}}
/>
<button onClick={this.likeToggle}>
<i className="far fa-thumbs-up text-success bg-dark" />
{likes} Like
</button>{" "}
<span className="button is-primary" onClick={() => Router.back()}>
<strong> Back to posts </strong>
</span>
{isAuthenticated().user &&
isAuthenticated().user._id === post.postedBy._id && (
<>
<span className="button is-warning">
<Link href={`/post/edit/${post._id}`}>
<strong> Update Post </strong>
</Link>
</span>
<button
onClick={this.deleteConfirmed}
className="button is-danger"
>
Delete Post
</button>
</>
)}
<div>
{isAuthenticated().user && isAuthenticated().user.role === "admin" && (
<div class="column">
<div className="columns">
<h5 className="column">Admin</h5>
<p>Edit/Delete as an Admin</p>
<span className="button is-warning">
<Link href={`/post/edit/${post._id}`}>
<a> Update Post </a>
</Link>
</span>
<button
onClick={this.deleteConfirmed}
className="button is-raised is-danger"
>
Delete Post
</button>
</div>
</div>
)}
</div>
<div>
<h4 className="raw"> Description: </h4>
<p className="column">{post.body}</p>
</div>
<br />
</div>
);
};
render() {
console.log("Render Quota:", this.props.postId, this.state.ispost);
const { postId } = this.props;
const {
post,
redirectToHome,
redirectToSignin,
redirectToBack
} = this.state;
if (redirectToHome) {
Router.push("/");
} else if (redirectToSignin) {
Router.push("/signin");
} else if (redirectToBack) {
Router.back();
}
return (
<section className="section">
<div className="container">
<h2 className="title">{post.title}</h2>
{!post ? (
<div className="hero">
<h2>Loading...</h2>
</div>
) : (
this.renderPost(post)
)}
</div>
</section>
);
}
}
export default SinglePost;
UPDATE- SOLVED
After Ayèch Hamza suggestion added getInitialProps method and thats exactly what needed.
So In [postid].js,
import React from "react";
import Layout from "../../components/layout";
import SinglePost from "../../components/post/singlePost";
Post.getInitialProps = async ctx => {
const PostId = ctx.query.postid;
return { PostId };
};
function Post({ PostId }) {
return (
<Layout>
<SinglePost PostId={PostId}/>
</Layout>
);
}
export default Post;
and in SinglePost.js,
componentDidMount() {
const postId = this.props.PostId;
singlePost(postId).then(data => {
if (data.error) {
console.log(data.error);
} else {
this.setState({
post: data,
likes: data.likes.length,
like: this.checkLike(data.likes),
comments: data.comments
});
}
});
}
Related
I am receiving an invalid hook call error when I try to update the state of my main app class component from my home page functional component. I am trying to send a string to update the state of menuId from home.jsx using useContext. everytime I press a button to update the string it returns the invalid hook error
App.jsx
import React from 'react';
import Home from './pages/home';
import NavBar from './components/nav-bar';
import parseRoute from './lib/parse-route';
import AppContext from './lib/app-context';
import MenuPage from './components/menu-page';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
route: parseRoute(window.location.hash),
menuId: ''
};
this.updateMenu = this.updateMenu.bind(this);
}
componentDidMount() {
addEventListener('hashchange', event => {
this.setState({ route: parseRoute(window.location.hash) });
});
}
renderPage() {
const { route } = this.state;
if (route.path === '') {
return <Home />;
}
if (route.path === 'menu') {
return <MenuPage />;
}
}
updateMenu(id) {
this.setState({ route: 'menu', menuId: id });
}
render() {
const { route, menuId } = this.state;
const updateMenuId = this.updateMenu;
const contextValue = {
route,
menuId,
updateMenuId
};
return (
<AppContext.Provider value={contextValue}>
<div>
<NavBar />
{this.renderPage()}
</div>
</AppContext.Provider>
);
}
}
home.jsx
import React, { useEffect, useRef, useState, useContext } from 'react';
import AppContext from '../lib/app-context';
import parseRoute from '../lib/parse-route';
const apiKey = '';
const mapApiJs = 'https://maps.googleapis.com/maps/api/js';
function loadAsyncScript(src) {
return new Promise(resolve => {
const script = document.createElement('script');
Object.assign(script, {
type: 'text/javascript',
async: true,
src
});
script.addEventListener('load', () => resolve(script));
document.head.appendChild(script);
});
}
const initMapScript = () => {
if (window.google) {
return Promise.resolve();
}
const src = `${mapApiJs}?key=${apiKey}&libraries=places&v=weekly`;
return loadAsyncScript(src);
};
export default function Home() {
const [locations, setLocations] = useState({ locations: ['no results'] });
const searchInput = useRef(null);
const onChangeAddress = autocomplete => {
const place = autocomplete.getPlace();
const longitude = place.geometry.viewport.Ia.lo;
const latitude = place.geometry.viewport.Wa.lo;
restaurantReq(longitude, latitude);
// console.log('longitude', longitude);
// console.log('latitude', latitude);
};
const initAutoComplete = () => {
if (!searchInput.current) return;
const autocomplete = new window.google.maps.places.Autocomplete(searchInput.current);
autocomplete.setFields(['address_component', 'geometry']);
autocomplete.addListener('place_changed', () => onChangeAddress(autocomplete));
};
const reverseGeoCode = ({ latitude: lat, longitude: lng }) => {
restaurantReq(lng, lat);
// console.log('location:', lat, lng);
};
const findMyLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
reverseGeoCode(position.coords);
});
}
};
useEffect(() => {
initMapScript().then(() => { initAutoComplete(); });
});
const ContextMenuId = id => {
const context = useContext(AppContext);
context.route = parseRoute('home');
context.updateMenuId(id);
console.log('Id', id);
};
const restaurantReq = (lng, lat) => {
fetch(`https://trackapi.nutritionix.com/v2/locations?ll=${lat},${lng}&distance=30mi&limit=20`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-app-id': '',
'x-app-key': ''
}
})
.then(res => res.json())
.then(data => setLocations(data)
)
// eslint-disable-next-line no-console
.catch(err => console.log('Fetch Get error:', err));
console.log(locations);
};
if (locations.locations.includes('no results') || locations.locations.length === 0) {
const locArr = locations.locations.map((loc, index) => <h2 key={index}>{loc.name}</h2>);
return (
<div>
<div className='text-center'> <h3>The purpose of this website is to help you lose weight by showing you
meals that are under 500 calories at the closest fast food locations to you.</h3></div>
<div className='row text-center'>
<div className='col-full'>
<form>
<label htmlFor="address" className='block padding'>
Enter Address to view nearby restaurant menus
</label>
<input type="text" placeholder='Address' required className='address-input'
ref={searchInput} />
<i className="fa-sharp fa-solid fa-location-dot" onClick={findMyLocation} />
</form>
<div>
<div>{locArr}</div>
</div>
</div>
</div>
</div>
);
} else if (locations.locations.length > 1) {
const LocSetup = (location, index) => {
const miles = location.distance_km / 0.621371;
if (index % 2 === 0) {
return <div className='col-half' key={index}>
<h2><a href={location.website} target="_blank" rel="noreferrer" className='rest-link'>{location.name}</a></h2>
<h3><button onClick={() => { ContextMenuId(location.brand_id); }}> Link to items on menu under 500 calories</button></h3>
<h4>{location.address} {location.city} {location.zip} {location.state}</h4>
<h4> {miles.toFixed(2) } miles away </h4>
</div>
;
} else {
return <div className='col-half' key={index}>
<h2><a href={location.website} target="_blank" rel="noreferrer" className='rest-link'>{location.name}</a></h2>
<h3> Link to items on menu under 500 calories</h3>
<h4>{location.address} {location.city} {location.zip} {location.state}</h4>
<h4> {miles.toFixed(2)} miles away </h4>
</div>;
}
};
const locArr = locations.locations.map(LocSetup);
return (
<div>
<div className='text-center'> <h3>The purpose of this website is to help you lose weight by showing you
meals that are under 500 calories at the closest fast food locations to you.</h3></div>
<div className='row text-center'>
<div className='col-full'>
<form>
<label htmlFor="address" className='block padding'>
Enter Address to view nearby restaurant menus
</label>
<input type="text" placeholder='Address' required className='address-input'
ref={searchInput} />
<i className="fa-sharp fa-solid fa-location-dot" onClick={findMyLocation} />
</form>
<div>
<h1>Nearby Restaurants</h1>
<div className='row'>{locArr}</div>
</div>
</div>
</div>
</div>
);
}
}
You should only call hooks on top level of the component.
export default function Home() {
const [locations, setLocations] = useState({ locations: ['no results'] });
const searchInput = useRef(null);
// Use hooks only on top-level
const context = useContext(AppContext);
const ContextMenuId = id => {
// const context = useContext(AppContext); <-- remove
context.route = parseRoute('home');
context.updateMenuId(id);
console.log('Id', id);
};
after onclick event occurs in backpackList.js, fetch data in context.js and then through setState I want to update noneUserCart . After that i want to get data from context.js to backpackList.js to show web page. but the data is inital data []. How can I solve this problem?!
I think this is a Asynchronous problem, but I'm new react, so I don't know how to write code for this. or do I use async, await.
Help me please!
import React, { Component } from 'react';
const ProductContext = React.createContext();
const ProductConsumer = ProductContext.Consumer;
class ProductProvider extends Component {
constructor() {
super();
this.state = {
totalProducts: 0,
isLogin: false,
cartList: [],
isNavOpen: false,
isCartOpen: false,
noneUserCart: [],
};
}
noneUserAddCart = bagId => {
fetch('/data/getdata.json', {
method: 'GET',
})
.then(res => res.json())
.catch(err => console.log(err))
.then(data => {
this.setState(
{
noneUserCart: [...this.state.noneUserCart, data],
},
() => console.log(this.state.noneUserCart)
);
});
};
render() {
return (
<ProductContext.Provider
value={{
...this.state,
handleCart: this.handleCart,
getToken: this.getToken,
addNoneUserCart: this.addNoneUserCart,
hanldeCheckout: this.hanldeCheckout,
openNav: this.openNav,
showCart: this.showCart,
habdleCartLsit: this.habdleCartLsit,
deleteCart: this.deleteCart,
noneUserAddCart: this.noneUserAddCart,
}}
>
{this.props.children}
</ProductContext.Provider>
);
}
}
export { ProductProvider, ProductConsumer };
import React, { Component } from 'react';
import { ProductConsumer } from '../../context';
export default class BackpackList extends Component {
render() {
const {
backpackdata,
backdescdata,
isdescOpen,
showDesc,
descClose,
rangenumone,
rangenumtwo,
} = this.props;
return (
<div>
{backdescdata.map((bag, inx) => {
return (
<>
{isdescOpen && bag.id > rangenumone && bag.id < rangenumtwo && (
<div className="listDescContainer" key={inx}>
<div className="listDescBox">
<ProductConsumer>
{value => (
<div
className="cartBtn"
onClick={() => {
const token = value.getToken();
if (token) {
value.handleCart(bag.id, token);
} else {
value.noneUserAddCart(bag.id);
console.log(value.noneUserCart);
// this part. value.noneUserCart is undefined
}
}}
>
add to cart.
</div>
)}
</ProductConsumer>
<span className="descClosebtn" onClick={descClose}>
X
</span>
</div>
</div>
</div>
)}
</>
);
})}
</div>
);
}
}
fetch is asynchronous, this.setState is yet called when console.log
<div
className="cartBtn"
onClick={() => {
const token = value.getToken();
if (token) {
value.handleCart(bag.id, token);
} else {
value.noneUserAddCart(bag.id);
console.log(value.noneUserCart);
// this part. value.noneUserCart is undefined
}
}}
>
add to cart.
{value.noneUserCart}
{/* when finished, result should show here */}
</div>
I have two pages on my react app. One page allows you to submit a post, and the second page shows all of the posts. I need to be able to retrieve the data from the state on one page, but I am receiving an error. What am I doing wrong to display this, because I thought I could use props to gather the state from my post page.
My Display Post Page:
import React from 'react';
import './App.css';
export default class Scroll extends React.Component {
render() {
return (
<div className="flex-container">
<div className="post">
{this.props.displayPost(this.props.state.posts)}
</div>
</div>
);
}
}
My post page:
import React from 'react';
import axios from 'axios';
import './App.css';
import { post } from '../../routes/routes';
export default class PersonList extends React.Component {
state = {
title: "",
body: "",
posts: []
};
componentDidMount = () => {
this.getPost();
}
getPost = () => {
axios.get("http://localhost:5000/posts/save")
.then((response) => {
const data = response.data;
this.setState({ posts: data });
console.log("Data has been recieved")
})
.catch(() => {
alert("Error recieving data")
})
}
handleChange = (event) => {
const target = event.target;
const name = target.name;
const value = target.value;
this.setState({
[name]: value
})
};
submit = (event) => {
event.preventDefault();
const payload = {
title: this.state.title,
body: this.state.body,
}
axios({
url: 'http://localhost:5000/posts/save',
method: 'POST',
data: payload,
})
.then(() => {
console.log('Data sent to the server');
})
.catch(() => {
console.log('Internal server error');
});
};
displayPost = (posts) => {
if (!post.length) return null;
return posts.map((post, index) => {
<div key={index}>
<h3 id="post-text">{post.title}</h3>
<p id="post-text">{post.body}</p>
</div>
});
}
render() {
console.log("State ", this.state)
return (
<div className="flex-container-home">
<div className="app">
<form onSubmit={this.submit}>
<input
placeholder="title"
type="text"
name="title"
value={this.state.title}
onChange={this.handleChange}
/>
<textarea placeholder="description"
name="body"
cols="30" rows="10"
value={this.state.body}
onChange={this.handleChange}
>
</textarea>
<button>Submit</button>
</form>
</div>
</div>
)
}
}
Here is working example:
import React from "react";
export default class PersonList extends React.Component {
state = {
title: "",
body: "",
posts: [],
};
componentDidMount = () => {
this.getPost();
};
getPost = () => {
this.setState({ posts: ["post1", "post2", "post3"] });
};
displayPost = (posts) => {
if (!posts || !posts.length) return null;
return posts.map((post, index) => (
<div key={index}>
<p>{post}</p>
</div>
));
};
render() {
return (
<div className="App">
<Scroll displayPost={this.displayPost} posts={this.state.posts} />
</div>
);
}
}
class Scroll extends React.Component {
render() {
return (
<div className="post">
Posts: {this.props.displayPost(this.props.posts)}
</div>
);
}
}
I am using MERN stack and Redux. I have created a function to update a property within a database. I have tested the api on Postman and it works. When i try and run it it seems to clash with another function and i get the error 'TypeError: this.props.subjects.map is not a function' which works prior to the trueVote function being called. Anyone any idea what i am missing here?
Print outs show the action and reducer are being hit but not the api, even though that works on Postman. The function is being called from within the voteHandler
EDIT: The console.log message within the api doesn't print but shows in the terminal window on VS when i refresh my page after the error the function has done what it should and the relevant data has been updated. Is it a case of managing the error? If so how would i do this so it doesn't crash the app?
api
// put req for a true vote
subjectRouter.put("subject/true/:_id/:currTrue", (req, res) => {
console.log("True api hitting");
Subject.findOneAndUpdate(
{ _id: req.params._id },
{
true: Number(req.params.currTrue) + 1,
},
{
new: true,
useFindAndModify: false,
}
)
.then((subject) => res.json(subject))
.catch((err) => console.log(err));
});
action
// true vote
export const trueVote = (_id, currTrue) => (dispatch) => {
console.log("trueVote hitting");
fetch(`/api/subjects/subject/true/${_id}/${currTrue}`, {
method: "PUT",
})
.then((res) => res.json())
.then((subject) =>
dispatch({
type: TRUE_VOTE,
subjects: subject,
})
);
};
reducer
case TRUE_VOTE:
console.log("true reducer hitting");
return {
...state,
items: action.subjects,
};
component
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchLatestSubjects } from "../../actions/subject";
import { fetchTopicSubjects } from "../../actions/subject";
import { fetchTopicComments } from "../../actions/comment";
import { fetchComments } from "../../actions/comment";
import { rateSubject } from "../../actions/subject";
import { fetchUsers } from "../../actions/authActions";
import { rateUser } from "../../actions/authActions";
import { rateComment } from "../../actions/comment";
import { trueVote } from "../../actions/subject";
import { falseVote } from "../../actions/subject";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchLatestSubjects();
this.props.fetchComments();
this.props.fetchUsers();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects
// description, summary and comments all invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
topic: "subjects",
};
}
componentWillReceiveProps(nextProps) {
// new subject and comments are added to the top
// of the arrays
if (nextProps.newPost) {
this.props.subjects.unshift(nextProps.newPost);
}
if (nextProps.newPost) {
this.props.comments.unshift(nextProps.newPost);
}
}
clickHandler = (id) => {
// when a subject title is clicked pass in its id
const { viewDescription } = this.state;
this.setState({ comments: [] });
var temp = [];
// get the details of the author of the subject and save to state
const subject = this.props.subjects.find((subject) => subject._id === id);
const user = this.props.users.find((user) => user._id === subject.author);
// save comments for subject to temp array
var i;
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
console.log(temp);
// for each comment add a property with the authors name
temp.forEach((comment) => {
var commentAuthor = this.props.users.find(
(user) => user._id === comment.author
);
comment.authName = commentAuthor.name;
});
// save the subject id to local storage
// this is done incase a new comment is added
// then the subject associated with it can be retrieved
// and added as a property of that comment
localStorage.setItem("passedSubject", id);
localStorage.setItem("passedTopic", subject.topic);
// add all changes to the state
this.setState({
viewDescription: viewDescription === id ? -1 : id,
comments: temp,
subAuthor: user.name,
authRating: user.rating,
authNoOfVotes: user.noOfVotes,
});
};
// hovering on and off subjects toggles the visibility of the summary
hoverHandler = (id) => {
this.setState({ viewSummary: id });
};
hoverOffHandler = () => {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate, item) => {
if (item === "subject") {
// this function rates the subject and the author
const subject = this.props.subjects.find((subject) => subject._id === id);
const author = this.props.users.find(
(user) => user._id === subject.author
);
// call the rateSubject and rateUser functions
this.props.rateSubject(id, rate, subject.noOfVotes, subject.rating);
this.props.rateUser(author._id, rate, author.noOfVotes, author.rating);
console.log(author.name);
alert("Thank you for rating this subject.");
} else if (item === "comment") {
const comment = this.props.comments.find((comment) => comment._id === id);
const author = this.props.users.find(
(user) => user._id === comment.author
);
// call the rateComment and rateUser functions
this.props.rateComment(id, rate, comment.noOfVotes, comment.rating);
this.props.rateUser(author._id, rate, author.noOfVotes, author.rating);
console.log(author.name);
alert("Thank you for rating this comment.");
}
};
voteHandler = (id, currVote, vote) => {
if (vote == "True") {
console.log(id, currVote, vote);
this.props.trueVote(id, currVote);
} else if (vote == "False") {
console.log(id, currVote, vote);
this.props.falseVote(id, currVote);
}
};
render() {
const subjectItems = this.props.subjects.map((subject) => {
// if the state equals the id set to visible if not set to invisible
var view = this.state.viewDescription === subject._id ? "" : "none";
var hover = this.state.viewSummary === subject._id ? "" : "none";
var comments = this.state.comments;
var subjectAuthor = this.state.subAuthor;
var authRating = this.state.authRating;
var authNoOfVotes = this.state.authNoOfVotes;
var className = "";
if (subject.category === "Education") {
className = "Education";
} else if (subject.category === "Environment") {
className = "Environment";
} else if (subject.category === "Politics") {
className = "Politics";
} else if (subject.category === "Health") {
className = "Health";
} else if (subject.category === "Other") {
className = "Other";
}
return (
<div key={subject._id}>
<div
className={className}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
<p className="title" onClick={() => this.clickHandler(subject._id)}>
{subject.title}
</p>
<p className="vote" style={{ textAlign: "Right" }}>
True:{" "}
{((100 / (subject.true + subject.false)) * subject.true).toFixed(
1
)}
% {" False: "}
{((100 / (subject.true + subject.false)) * subject.false).toFixed(
1
)}
%
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="leftSubjectBody">
<div className="subjectAuthor">
<p className="author">
Subject created by: {subjectAuthor} -{" "}
{(authRating / authNoOfVotes).toFixed(1)}/5 Star user
{/* <br /> {subject.date} */}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links: {subject.links}</div>
</div>
<div className="rightSubjectBody">
<div className="rate">
<p> Rate this subject:</p>
<br />
<button
onClick={() => this.rateHandler(subject._id, 1, "subject")}
>
1
</button>
<button
onClick={() => this.rateHandler(subject._id, 2, "subject")}
>
2
</button>
<button
onClick={() => this.rateHandler(subject._id, 3, "subject")}
>
3
</button>
<button
onClick={() => this.rateHandler(subject._id, 4, "subject")}
>
4
</button>
<button
onClick={() => this.rateHandler(subject._id, 5, "subject")}
>
5
</button>
<p>
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
</div>
<div className="voting">
<p>
Do you think this subject question is true or false based on
the evidence provided and your own reseach in the area? <br />
</p>
<p>Please vote and leave comments.</p>
<br />
<div
className="voteButton"
onClick={() =>
this.voteHandler(subject._id, subject.true, "True")
}
>
TRUE
</div>
<div
className="voteButton"
onClick={() =>
this.voteHandler(subject._id, subject.false, "False")
}
>
FALSE
</div>
</div>
</div>
<div className="subjectComments">
<p style={{ fontWeight: "bold" }}>Comments:</p>
{comments.map((comment, i) => {
return (
<div key={i} className="singleComment">
<p>
{comment.title}
<br />
{comment.comment}
<br />
Comment by : {comment.authName} - This user has a rating
of {(comment.rating / comment.noOfVotes).toFixed(1)}/5
STARS
</p>
<div className="rate">
Rate this comment:
<button
onClick={() =>
this.rateHandler(comment._id, 1, "comment")
}
>
1
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 2, "comment")
}
>
2
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 3, "comment")
}
>
3
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 4, "comment")
}
>
4
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 5, "comment")
}
>
5
</button>
<p>
Rating:{" "}
{(comment.rating / comment.noOfVotes).toFixed(1)}/5
</p>
</div>
</div>
);
})}
<br />
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchLatestSubjects: PropTypes.func.isRequired,
fetchTopicSubjects: PropTypes.func.isRequired,
fetchTopicComments: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
fetchUsers: PropTypes.func.isRequired,
rateSubject: PropTypes.func.isRequired,
rateComment: PropTypes.func.isRequired,
rateUser: PropTypes.func.isRequired,
trueVote: PropTypes.func.isRequired,
falseVote: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
users: PropTypes.array.isRequired,
newPost: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
users: state.auth.users,
newComment: state.comments.item,
});
// export default Subject;
export default connect(mapStateToProps, {
fetchLatestSubjects,
fetchTopicSubjects,
fetchTopicComments,
fetchComments,
fetchUsers,
rateSubject, // rate subject
rateUser,
rateComment,
trueVote,
falseVote,
})(Subject, Comment);
Because you are returning a single subject as part of your reducer action, you presumably want to exchange the existing subject with the updated subject, so you would need to update your reducer to be:
case TRUE_VOTE:
const index = state.items.subjects.findIndex( subject => action.subjects.id === subject.id );
return {
items: [...state.items.slice(0, index), action.subjects, ...state.items.slice( index + 1 )] };
};
To make it a bit more clear, you could of course also change your action to indicate it's only a single subject you are returning
// true vote
export const trueVote = (_id, currTrue) => (dispatch) => {
console.log("trueVote hitting");
fetch(`/api/subjects/subject/true/${_id}/${currTrue}`, {
method: "PUT",
})
.then((res) => res.json())
.then((subject) =>
dispatch({
type: TRUE_VOTE,
subject
})
);
};
Which would then be more clear inside your reducer that you are only expecting 1 subject
I'm experiencing some strange activity with my Chatkit app built using React. Essentially, I'm testing with two different users in different rooms. When I send a message from a user in one room. The other user is able to see that message, although they are not in the same room. Here's a screenshot of what's going on.
This only appears to happen when the users have been in the same room at least once.
Buggy Chat
I can tell the messages are being created correctly because I see them in the right place in the ChatKit API. Also, if I re-render the component, the messages end up in the right place. But The cross-room messaging bug still persists.
Corrected Chat
I'm under the impression that it definitely has something to do with the state of the MessageList component. I've made sure to update the component state every time we enter a new room, but I suppose the real question is whether or not other instances of the applications even care about the change in component state for a different instance.
So without further ado, here is my code:
ChatScreen (Main app)
import React from "react"
import Chatkit from "#pusher/chatkit"
import MessageList from "./MessageList"
import SendMessageForm from "./SendMessageForm"
import WhosOnlineList from "./WhosOnlineList"
import RoomList from "./RoomList"
import NewRoomForm from "./NewRoomForm"
import { getCurrentRoom } from "../../actions/chatkitActions"
import { connect } from "react-redux"
class ChatScreen extends React.Component{
constructor(props){
super(props)
this.state = {
messages: [],
currentRoom: {},
currentUser: {},
usersWhoAreTyping: [],
joinableRooms: [],
joinedRooms: [],
errors: {}
}
this.sendMessage = this.sendMessage.bind(this)
this.sendTypingEvent = this.sendTypingEvent.bind(this)
this.subscribeToRoom = this.subscribeToRoom.bind(this)
this.getRooms = this.getRooms.bind(this)
this.createRoom = this.createRoom.bind(this)
}
componentDidMount(){
//setup Chatkit
let tokenUrl
let instanceLocator = "somecode"
if(process.env.NODE_ENV === "production"){
tokenUrl = "somenedpoint"
} else {
tokenUrl = "http://localhost:3000/api/channels/authenticate"
}
const chatManager = new Chatkit.ChatManager({
instanceLocator: instanceLocator,
userId: this.props.chatUser.name,
connectionTimeout: 120000,
tokenProvider: new Chatkit.TokenProvider({
url: tokenUrl
})
})
//initiate Chatkit
chatManager.connect()
.then((currentUser) => {
this.setState({
currentUser: currentUser
})
//get all rooms
this.getRooms()
// if the user is returning to the chat, direct them to the room they last visited
if(this.props.chatkit.currentRoom.id > 0){
this.subscribeToRoom(this.props.chatkit.currentRoom.id)
}
})
}
sendMessage = (text) => {
this.state.currentUser.sendMessage({
roomId: this.state.currentRoom.id,
text: text
})
}
sendTypingEvent = () => {
this.state.currentUser
.isTypingIn({
roomId: this.state.currentRoom.id
})
.catch((errors) => {
this.setState({
errors: errors
})
})
}
getRooms = () => {
this.state.currentUser.getJoinableRooms()
.then((joinableRooms) => {
this.setState({
joinableRooms: joinableRooms,
joinedRooms: this.state.currentUser.rooms
})
})
.catch((errors) => {
this.setState({
errors: { error: "could not retrieve rooms"}
})
})
}
subscribeToRoom = (roomId) => {
this.setState({
messages: []
})
this.state.currentUser.subscribeToRoom({
roomId: roomId,
hooks: {
onNewMessage: (message) => {
this.setState({
messages: [...this.state.messages, message]
})
},
onUserStartedTyping: (currentUser) => {
this.setState({
usersWhoAreTyping: [...this.state.usersWhoAreTyping, currentUser.name]
})
},
onUserStoppedTyping: (currentUser) => {
this.setState({
usersWhoAreTyping: this.state.usersWhoAreTyping.filter((user) => {
return user !== currentUser.name
})
})
},
onUserCameOnline: () => this.forceUpdate(),
onUserWentOffline: () => this.forceUpdate(),
onUserJoined: () => this.forceUpdate()
}
})
.then((currentRoom) => {
this.setState({
currentRoom: currentRoom
})
this.getRooms()
//store currentRoom in redux state
this.props.getCurrentRoom(currentRoom)
})
.catch((errors) => {
this.setState({
errors: errors
})
})
}
createRoom = (roomName) => {
this.state.currentUser.createRoom({
name: roomName
})
.then((newRoom) => {
this.subscribeToRoom(newRoom.id)
})
.catch((errors) => {
this.setState({
errors: { error: "could not create room" }
})
})
}
render(){
const username = this.props.chatUser.name
return(
<div className="container" style={{ display: "flex", fontFamily: "Montserrat", height: "100vh"}}>
<div
className="col-md-3 bg-dark mr-2 p-0"
style={{display: "flex", flexDirection: "column", maxHeight: "80vh", padding: "24px 24px 0px"}}
>
<div style={{flex: "1"}} className="p-4">
<WhosOnlineList users={this.state.currentRoom.users}/>
<RoomList
roomId={this.state.currentRoom.id}
rooms={[...this.state.joinedRooms, ...this.state.joinableRooms]}
subscribeToRoom={this.subscribeToRoom}
/>
</div>
<NewRoomForm createRoom={this.createRoom} user={this.state.currentUser}/>
</div>
<div
className="col-md-9 border p-0"
style={{display: "flex", flexDirection: "column", maxHeight: "80vh"}}
>
<div className="mb-3">
{ this.state.currentRoom.name ? (
<h4
className="bg-black text-light m-0"
style={{padding: "1.0rem 1.2rem"}}
>
{this.state.currentRoom.name}
</h4>
) : (
this.props.chatkit.currentRoom.id > 0 ) ? (
<h3 className="text-dark p-4">Returning to room...</h3>
) : (
<h3 className="text-dark p-4">← Join a Room!</h3>
)}
</div>
<div style={{flex: "1"}}>
<MessageList messages={this.state.messages} room={this.state.currentRoom.id} usersWhoAreTyping={this.state.usersWhoAreTyping}/>
</div>
<SendMessageForm
sendMessage={this.sendMessage}
userTyping={this.sendTypingEvent}
currentRoom={this.state.currentRoom}
/>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return{
chatkit: state.chatkit
}
}
const mapDispatchToProps = (dispatch) => {
return{
getCurrentRoom: (currentRoom) => {
dispatch(getCurrentRoom(currentRoom))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatScreen)
MessageList (component)
import React from "react"
import ReactDOM from "react-dom"
import TypingIndicator from "./TypingIndicator"
class MessageList extends React.Component{
constructor(props){
super(props)
this.state = {
currentRoom: {}
}
}
componentWillReceiveProps(nextProps){
if(nextProps.room){
console.log(nextProps.room)
this.setState({
currentRoom: nextProps.room
})
}
}
componentWillUpdate(){
const node = ReactDOM.findDOMNode(this)
//scrollTop is the distance from the top. clientHeight is the visible height. scrollHeight is the height on the component
this.shouldScrollToBottom = node.scrollTop + node.clientHeight + 100 >= node.scrollHeight
}
componentDidUpdate(){
//scroll to the bottom if we are close to the bottom of the component
if(this.shouldScrollToBottom){
const node = ReactDOM.findDOMNode(this)
node.scrollTop = node.scrollHeight
}
}
render(){
const messages = this.props.messages
let updatedMessages = []
for(var i = 0; i < messages.length; i++){
let previous = {}
if(i > 0){
previous = messages[i - 1]
}
if(messages[i].senderId === previous.senderId){
updatedMessages.push({...messages[i], senderId: ""})
} else{
updatedMessages.push(messages[i])
}
}
return(
<div>
{this.props.room && (
<div style={{overflow: "scroll", overflowX: "hidden", maxHeight: "65vh"}}>
<ul style={{listStyle: "none"}} className="p-3">
{updatedMessages.map((message, index) => {
return (
<li className="mb-1" key={index}>
<div>
{message.senderId && (
<span
className="text-dark d-block font-weight-bold mt-3"
>
{message.senderId}
</span>
)}
<span
className="bg-info text-light rounded d-inline-block"
style={{padding:".25rem .5rem"}}
>
{message.text}
</span>
</div>
</li>
)
})}
</ul>
<TypingIndicator usersWhoAreTyping={this.props.usersWhoAreTyping}/>
</div>
)}
</div>
)
}
}
export default MessageList
RoomList (component)
import React from "react"
class RoomList extends React.Component{
render(){
const orderedRooms = [...this.props.rooms].sort((a, b) => {
return a.id - b.id
})
return(
<div>
{ this.props.rooms.length > 0 ? (
<div>
<div className="d-flex justify-content-between text-light mb-2">
<h6 className="font-weight-bold">Channels</h6><i className="fa fa-gamepad"></i>
</div>
<ul style={{listStyle: "none", overflow: "scroll", overflowX: "hidden", maxHeight: "27vh"}} className="p-2">
{orderedRooms.map((room, index) => {
return(
<li key={index} className="font-weight-bold mb-2">
<a
onClick={() => {
this.props.subscribeToRoom(room.id)
}}
href="#"
className={room.id === this.props.roomId ? "text-success": "text-info"}
style={{textDecoration: "none"}}
>
<span className="mr-2">#</span>{room.name}
</a>
</li>
)
})}
</ul>
</div>
) : (
<p className="text-muted p-2">Loading...</p>
)}
</div>
)
}
}
Here's the component (ChannelsContainer) that's rendering the ChatScreen as well
import React from "react"
import UsernameForm from "./UsernameForm"
import ChatScreen from "./ChatScreen"
import { connect } from "react-redux"
class ChannelsContainer extends React.Component{
constructor(props){
super(props)
this.state = {
chatScreen: false
}
}
componentWillMount(){
if(this.props.chatkit.chatInitialized){
this.setState({
chatScreen: true
})
}
}
componentWillReceiveProps(nextProps){
if(nextProps.chatkit.chatInitialized){
this.setState({
chatScreen: true
})
}
}
render(){
let chatStage
if(this.state.chatScreen){
chatStage = <ChatScreen chatUser={this.props.chatkit.chatUser}/>
} else{
chatStage = <UsernameForm/>
}
return(
<div style={{minHeight: "90vh"}}>
{chatStage}
</div>
)
}
}
const mapStateToProps = (state) => {
return{
chatkit: state.chatkit
}
}
export default connect(mapStateToProps)(ChannelsContainer)
Please let me know what you guys think.
Fixed. All I had to do was compare room id of the message against the current room id. If they're the same, then I'll update my component state messages field.
onNewMessage: (message) => {
if(message.room.id === this.state.currentRoom.id){
this.setState({
messages: [...this.state.messages, message]
})
}
}