Server Error: Error: Error serializing. Reason: `object` (“[object Object]”) cannot be serialized as JSON Next.js MongoDB - mongodb

I am working on an online shopping project and implying a review system for shopping products. I am using Next.js and MongoDB for my project. It works fine without any reviews submitted but when I submit my review and the error happened as below;
the error is from getServerSideProps in pages/product/[slug].js and the code is as below;
import axios from 'axios';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router'
import React, { useContext, useState, useEffect } from 'react'
import Layout from '../../components/Layout'
import Product from '../../models/Product';
import db from '../../utils/db';
import { Store } from '../../utils/Store';
import { toast } from 'react-toastify';
import Rating from '#material-ui/lab/Rating';
import { getSession } from 'next-auth/react';
import { useForm } from "react-hook-form";
import { getError } from '../../utils/error';
export default function ProductScreen (props) {
const { product, user } = props;
const {state, dispatch} = useContext(Store);
const router = useRouter();
const [ reviews, setReviews ] = useState([]);
const [ rating, setRating ] = useState(0);
const [ comment, setComment ] = useState('');
const [ loading, setLoading ] = useState(false);
const {
handleSubmit,
register,
formState: { errors },
setValue,
} = useForm();
const submitHandler = async () => {
setLoading(true);
try {
await axios.post(`/api/products/${product._id}/reviews`, { rating, comment, user });
setLoading(false);
toast.success('Review submitted successfully');
fetchReviews();
} catch (err) {
setLoading(false);
return toast.error(getError(err));
}
}
const fetchReviews = async () => {
try {
const { data } = await axios.get(`/api/products/${product._id}/reviews`);
setReviews(data);
} catch (err) {
return toast.error('fetchReview err');
}
}
useEffect(() => {
fetchReviews();
}, []);
if (!product) {
return <Layout title="Product Not Found">Product Not Found</Layout>;
}
const addToCartHandler = async () => {
const existItem = state.cart.cartItems.find((x) => x.slug === product.slug);
const quantity = existItem ? existItem.quantity + 1 : 1;
const { data } = await axios.get(`/api/products/${product._id}`);
if (data.countInStock < quantity) {
return toast.error('Sorry. Product is out of stock');
}
dispatch ({ type: 'CART_ADD_ITEM', payload: { ...product, quantity }});
router.push("/cart");
};
return (
<Layout title={product.name}>
<div className='py-2'>
<Link href="/">back to products</Link>
</div>
<div className='grid md:grid-cols-4 md:gap-3'>
<div className='md:col-span-2'>
<Image
src={product.image}
alt={product.name}
width={640}
height={640}
layout='responsive'
/>
</div>
<div>
<ul>
<li className='mt-4'>
<h1 className='text-lg'>{product.name}</h1>
</li>
<li className='mt-4'>Category: {product.category}</li>
<li className='mt-4'>Brand: {product.brand}</li>
<li className='flex mt-4'>
<Rating value={product.rating} readOnly />
<Link href='#reviews'>
<a>({product.numReviews} reviews)</a>
</Link>
</li>
<li className='mt-4'>Description: {product.description}</li>
</ul>
</div>
<div>
<div className='card p-5'>
<div className='mb-2 flex justify-between'>
<div>Price</div>
<div>${product.price}</div>
</div>
<div className='mb-2 flex justify-between'>
<div>Status</div>
<div>{product.countInStock > 0 ? 'In stock' : 'Unavailable'}</div>
</div>
<button className='primary-button w-full' onClick={addToCartHandler}>Add to cart</button>
</div>
</div>
</div>
<div id='reviews' className='mt-4'>
<h2 className='text-xl'>Customer Reviews</h2>
{reviews.length === 0 && <div>No reviews</div>}
{reviews.map((review) => (
<div key={review._id}>
{review.name}
{review.createdAt.substring(0, 10)}
<Rating value={review.rating} readOnly />
{review.comment}
</div>
))}
</div>
<div>
{user ? (
<form onSubmit={handleSubmit(submitHandler)}>
<h1>Leave your review</h1>
<div>
<Rating
name='simple-controlled'
value={rating}
onChange={(e) => setRating(e.target.value)}
/>
<label htmlFor='comment'>Comment</label>
<input
type='text'
{...register('comment', { required: 'Please enter comment',})}
id='comment'
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
{errors.comment && (
<div className='text-red-500'>{errors.comment.message}</div>
)}
<button className='primary-button'>
{loading? 'Loading' : 'Submit'}
</button>
</div>
</form>
) : (
<div>
Please <Link href={`/login?redirect=/product/${product.slug}`}>Login</Link> to write a review.
</div>
)}
</div>
</Layout>
)
}
export async function getServerSideProps(context) {
const { params, req } = context;
const { slug } = params;
const session = await getSession({ req });
await db.connect();
const product = await Product.findOne({ slug }).lean();
await db.disconnect();
return (
{
props: {
product: product ? db.convertDocToObj(product) : null,
user: session ? session : null,
},
}
);
}
as my understanding, the getServerSideProps function at the bottom should convert the props into object with below code;
product: product ? db.convertDocToObj(product) : null,
am I using it correctly?
my product schema with review is as below;
import mongoose from 'mongoose';
const reviewSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
name: { type: String, required: true },
rating: { type: Number, default: 5 },
comment: { type: String, required: true },
},
{
timestamps: true,
});
const productSchema = new mongoose.Schema(
{
name: { type: String, required: true },
slug: { type: String, required: true, unique: true },
category: { type: String, required: true },
image: { type: String, required: true },
price: { type: Number, required: true },
brand: { type: String, required: true },
rating: { type: Number, required: true, default: 0 },
numReviews: { type: Number, required: true, default: 0 },
countInStock: { type: Number, required: true, default: 0 },
description: { type: String, required: true },
reviews: [reviewSchema],
}, {
timestamps: true,
}
);
const Product = mongoose.models.Product || mongoose.model('Product', productSchema);
export default Product;
the MongoDB Compass for Product is as below;
So, how can I fix my problew?
Thanks
Stan

latest Axios version(v1.2.0) changed header field, it is know issue
Not working old code, so you can avoid this error by adding header information.
https://github.com/axios/axios/issues/5298
Your three axios API call needs to add header with correct content type.
(I suppose all of json type in your code)
before
await axios.post(`/api/products/${product._id}/reviews`, { rating, comment, user });
const { data } = await axios.get(`/api/products/${product._id}`);
const { data } = await axios.get(`/api/products/${product._id}/reviews`);
after
await axios.post(`/api/products/${product._id}/reviews`, { rating, comment, user }, {
headers: {
'Content-Type': 'application/json'
}
});
const { data } = await axios.get(`/api/products/${product._id}`,
{
headers: {
'Accept-Encoding': 'application/json'
}
});
const { data } = await axios.get(`/api/products/${product._id}/reviews`,
{
headers: {
'Accept-Encoding': 'application/json'
}
});

Related

Preset input values of the form with Vuex

In my project (laravel/vue spa) I have a form opened in a modal window, the form shall display the initial values from the object kept in the vuex store. The problem is the input doesn't see objet property that I set through value, so with my code now when the modal is opened, no data is displayed inside the input, although if i output the section object in the markup it generally sees it.
How can I make the input work with the preset values?
Here's my code:
ModalForm.vue markup
<template>
<app-modal>
<Form #submit="submitSectionForm">
<div class="form-group">
<label for="title" class="control-label">Title</label>
//<p>{{ section }}</p>
<Field
:value="sectionTitle" #input="handleUpdateTitle"
:rules="isRequired"
type="text"
name="section_title"
class="form-control"
/>
<ErrorMessage
name="section_title"
class="pr-1 display-block color-red"
/>
</div>
<button class="btn btn-default" #click="closeModal">Cancel</button>
<button class="btn btn-primary ml-1">Ok</button>
</Form>
</app-modal>
</template>
ModalForm.vue functionality
<script>
import AppModal from "../../components/Modals/Modal.vue";
import { Field, ErrorMessage } from "vee-validate";
import { mapGetters, mapActions, mapState, mapMutations } from "vuex";
export default {
props: {
menu_type_id: Number,
menu_id: Number,
},
components: {
AppModal,
Field,
ErrorMessage,
},
methods: {
...mapActions("menu_sections", ["saveSection"]),
...mapMutations("menu_sections", ['updateTitle']),
isRequired(value) {
if (value && value.trim()) {
return true;
}
handleUpdateTitle(e) {
this.updateTitle(e.target.value);
}
submitSectionForm(value) {
console.log(value);
this.saveSection(value);
this.closeModal();
},
computed: {
...mapGetters("menu_sections", { section: "getSection" } ),
...mapGetters("menu_sections", { sectionTitle: "getSectionTitle" }),
},
};
</script>
section_store.js
import axios from "axios";
export default {
namespaced: true,
state: {
section: {},
message: "",
},
getters: {
indexById: (state) => (id) =>
state.sections.findIndex((item) => item.id == id),
sectionById: (state) => (id) =>
state.sections.filter((item) => item.id == id),
getSection: (state) => state.section,
getSectionTitle: (state )=>state.section.title
},
mutations: {
setSection(state, section) {
state.section = { ...section };
},
saveSection(state, data) {
state.message = data.message;
},
updateTitle(state, title) {
state.section.title = title;
console.log(state.section.title);
},
},
actions: {
saveSection({ commit }, section_object) {
axios
.post("/api/menu-sections/" + section_object)
.then((response) => {
commit("saveSection", response.data);
})
.catch((e) => {
console.log("Section save error");
});
}
},
};

Patch returning null instead of comments

Hi,
Im rather new to backend. I´m trying to patch a newComment to a list of comments but gets updatedTravelTips null. Where am I doing wrong? and which mongo _id should i target?
I know my database is a bit messy with nested objects...
this it how my users look like in mongoDBcompass:
https://i.stack.imgur.com/moncj.png
const onTravelTips = (event) => {
event.preventDefault()
const options = {
method: 'PATCH',
headers: {
Authorization: accessToken,
'Content-Type': 'application/json'
},
// neWcomment comes from a text input in a form
body: JSON.stringify({ comments: newComment })
}
//countryId comes from selected country from a dropdown and stored in state
fetch(API_URL(`countries/${countryId}`), options)
.then(res => res.json())
.then(data => {
if (data.success) {
console.log(data)
dispatch(user.actions.setErrors(null))
} else {
dispatch(user.actions.setErrors({ message: 'Failed to add travel tips' }))
}
})
}
<form className="add-tips-form">
<p>Choose one of your visited countries and add some tips:</p>
<select value={newCountryId} onChange={(event) => dispatch(user.actions.setCountryId(event.target.value))}>
<optgroup label='Countries'>
<option value="" disabled defaultValue>Select country</option>
{visitedList && visitedList.map(country => (
<option
key={country.country._id}
// country._id gets the new one, country.country._id gets the countryid
value={country._id}
>{country.country.country} {console.log(country._id)}</option>
))}
</optgroup>
</select>
<input
type="text"
value={newComment}
onChange={(event) => setNewComment(event.target.value)}
className="username-input"
placeholder="food"
/>
<button className="add-tips-button" onClick={onTravelTips}>Add travel tips</button>
</form>
const Country = mongoose.model('Country', {
country: String,
alphaCode: String,
})
const User = mongoose.model('User', {
username: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true
},
accessToken: {
type: String,
default: () => crypto.randomBytes(128).toString('hex')
},
visitedCountries:[ {
country: {
type: Object,
ref: "Country",
},
comments: Array
}]
})
app.patch('/countries/:countryid', authenticateUser)
app.patch('/countries/:countryid', async (req, res) => {
const { countryid } = req.params
const { comments, } = req.body
const {id} = req.user
try {
console.log(countryid) // working, gets the country id or the nested object id depening on what we pass in FE
console.log(comments) // working, gets whatever we write in text input. *should it be so?
console.log(id) // working, gets user id
console.log("comment",newComment) // not working, return undefined
const updatedTravelTips = await User.findOneAndUpdate( {id, countryid, comments }, {
$push: {
visitedCountries: { comments: comments}
},
}, { new: true })
res.json({ success: true, updatedTravelTips })
console.log(updatedTravelTips) // not working, return null
} catch (error) {
res.status(400).json({ success: false, message: "Invalid request", error })
}
})
app.patch('/countries/:countryid', authenticateUser)
app.patch('/countries/:countryid', async (req, res) => {
const { countryid } = req.params
const { comments, } = req.body
const {id} = req.user
try {
const updatedTravelTips = await User.findOneAndUpdate( {_id: id, "visitedCountries._id": countryid }, {
$push: {
"visitedCountries.$.comments": comments
},
}, { new: true })
res.json({ success: true, updatedTravelTips })
} catch (error) {
res.status(400).json({ success: false, message: "Invalid request", error })
}
})

The API request from the Vuejs component is executed twice

I am creating To-do Application using Vuejs, Expressjs, and Mongodb.
I have already written through the New Component with a separate link. This is normal, but I am currently using the New Component by importing it into the Root Component without any additional links.
This works, but the API request is executed twice. (Two articles are written.)
I just imported the component, what's the problem?
I have only sent one request but strangely two responses are flying. (id is different, title and content are the same.)
frontend/components/TodoForm.vue
<template>
<form #submit.prevent="create">
title : <input v-model="todo.title">
content : <input v-model="todo.content">
<button v-on:click="create" >Create</button>
</form>
</template>
<script>
export default {
data: function () {
return {
todo: {}
}
},
methods: {
create: function() {
this.$http.post('/api/todos/create', this.todo)
.then(
(response) => {
console.log(response.data)
this.$router.push({name: 'Todo', params: { id: response.data._id }})
},
(err) => {
alert('Error')
}
)
.catch(function (error) {
alert('error')
})
}
}
}
</script>
fromtend/components/TodoList.vue
<template>
<todo-form/>
<div class="todos">
<h1>Todos</h1>
<div v-for="todo in todos" v-bind:key="todo" class="todo">
<div>
<strong>{{todo.title}}</strong>
<router-link :to="{ name: 'Todo', params: { id: todo._id }}">detail</router-link>
<router-link :to="{ name: 'Edit', params: { id: todo._id }}">edit</router-link>
<button type="submit" #click="deleteTodo(todo._id)">Delete</button>
</div>
</div>
</div>
</template>
<script>
import TodoForm from './TodoForm';
export default {
data () {
return {
todos: {}
}
},
created () {
this.$http.get('/api/todos')
.then((response) => {
this.todos= response.data
})
},
components: {
TodoForm
},
methods: {
deleteTodo (id) {
const targetIndex = this.todos.findIndex(v => v._id === id)
this.$http.delete(`/api/todos/${id}`)
.then((response) => {
this.todos.splice(targetIndex, 1)
})
}
}
}
</script>
backend/api
router.post ('/create', (req, res) => {
let todo = new Todo({
title: req.body.title || req.body.todo.title,
content: req.body.content || req.body.todo.content
});
todo.save((err) => {
if (err) {
res.status(500).send('Something broke!');
}
res.json(todo)
});
});
Try to change the following code :
<template>
<form>
title : <input v-model="todo.title">
content : <input v-model="todo.content">
<button v-on:click.once="create" >Create</button>
</form>
</template>

Express Vue not creating association, userId is null

I am following this tutorial using Express.js Postgres and Vue to create a CRUD app for Songs with login and basic associations : https://www.youtube.com/watch?v=ipYlztBRpp0
I am using Vuex to manage the state object. However, for some reason, though registration of new users works to create a user record, when I try to create a Bookmark, which essentially pairs a UserId with a SongId, UserId is always recorded as null. SongId is saved correctly. Checking the requests in the network and Vue tabs of developer tools reveals that the User object seems to be empty.
Here are the relevant files.
User.js model
const Promise = require('bluebird')
const bcrypt = Promise.promisifyAll(require('bcrypt-nodejs'))
function hashPassword (user, options) {
const SALT_FACTOR = 8
if (!user.changed('password')) {
return;
}
return bcrypt
.genSaltAsync(SALT_FACTOR)
.then(salt => bcrypt.hashAsync(user.password, salt, null))
.then(hash => {
user.setDataValue('password', hash)
})
}
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
unique: true
},
password: DataTypes.STRING
}, {
hooks: {
// beforeCreate: hashPassword,
// beforeUpdate: hashPassword,
beforeSave: hashPassword
}
})
User.prototype.comparePassword = function (password) {
return bcrypt.compareAsync(password, this.password)
}
return User
}
Songs model:
module.exports = (sequelize, DataTypes) => {
const Song = sequelize.define('Song', {
title: DataTypes.STRING,
artist: DataTypes.STRING,
genre: DataTypes.STRING,
album: DataTypes.STRING,
albumImageUrl: DataTypes.STRING,
youtubeId: DataTypes.STRING,
lyrics: DataTypes.TEXT,
tab: DataTypes.TEXT
})
return Song
}
Bookmark's controller:
const {Bookmark} = require('../models')
module.exports = {
async index (req, res) {
try {
const {songId, userId} = req.query
const bookmark = await Bookmark.findOne({
where: {
SongId: songId,
UserId: userId
}
})
res.send(bookmark)
} catch (err) {
res.status(500).send({
error: 'An error has occurred trying to fetch the songs'
})
}
},
async post (req, res) {
try {
const {songId, userId} = req.body
const bookmark = await Bookmark.findOne({
where: {
SongId: songId,
UserId: userId
}
})
if (bookmark) {
return res.status(400).send({
error: 'you already have this song bookmarked'
})
}
const newBookmark = await Bookmark.create({
SongId: songId,
UserId: userId
})
res.send(newBookmark)
} catch (err) {
console.log(err)
res.status(500).send({
error: 'An error has occurred trying to create the bookmark.'
})
}
},
async delete (req, res) {
try {
const {bookmarkId} = req.params
const bookmark = await Bookmark.findById(bookmarkId)
await bookmark.destroy()
res.send(bookmark)
} catch (err) {
res.status(500).send({
error: 'An error has occurred trying to delete the bookmark'
})
}
}
}
BookMarksService (for axios connection)
import Api from '#/services/Api'
export default {
index (bookmark) {
return Api().get('bookmarks', {
params: bookmark
})
},
post (bookmark) {
return Api().post('bookmarks', bookmark)
},
delete (bookmarkId) {
return Api().delete(`bookmarks/${bookmarkId}`)
}
}
and the component SongsMetaData:
<template>
<panel title='Song Metadata'>
<v-layout>
<v-flex xs6>
<div class="song-title">
{{song.title}}
</div>
<div class="song-artist">
{{song.artist}}
</div>
<div class="song-genre">
{{song.genre}}
</div>
<v-btn
dark
class="cyan"
:to="{
name: 'song-edit',
params () {
return {
songId: song.id
}
}
}">
Edit
</v-btn>
<v-btn
v-if="isUserLoggedIn && !bookmark"
dark
class="cyan"
#click="setAsBookmark">
Bookmark
</v-btn>
<v-btn
v-if="isUserLoggedIn && bookmark"
dark
class="cyan"
#click="unsetAsBookmark">
UnBookmark
</v-btn>
</v-flex>
<v-flex xs6>
<img class="album-image" :src="song.albumImageUrl" />
<br />
<div class="song-album">
{{song.album}}
</div>
</v-flex>
</v-layout>
</panel>
</template>
<script>
import {mapState} from 'vuex'
import BookmarksService from '#/services/BookmarksService'
export default {
props: [
'song'
],
data () {
return {
bookmark: null
}
},
computed: {
...mapState([
'isUserLoggedIn'
])
},
watch: {
async song () {
if (!this.isUserLoggedIn) {
return
}
try {
this.bookmark = (await BookmarksService.index({
songId: this.song.id,
userId: this.$store.state.user.id
})).data
} catch (err) {
console.log(err)
}
}
},
methods: {
async setAsBookmark () {
try {
this.bookmark = (await BookmarksService.post({
songId: this.song.id,
userId: this.$store.state.user.id
})).data
} catch (err) {
console.log(err)
}
},
async unsetAsBookmark () {
try {
await BookmarksService.delete(this.bookmark.id)
} catch (err) {
console.log(err)
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.song {
padding: 20px;
height: 330px;
overflow: hidden;
}
.song-title {
font-size: 30px;
}
.song-artist {
font-size: 24px;
}
.song-genre {
font-size: 18px;
}
.album-image {
width: 70%;
margin: 0 auto;
}
textarea {
width: 100%;
font-family: monospace;
border: none;
height: 600px;
border-style: none;
border-color: transparent;
overflow: auto;
padding: 40px;
}
</style>
and the vuex store.js file:
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
export default new Vuex.Store({
strict: true,
plugins: [
createPersistedState()
],
state: {
token: null,
user: null,
isUserLoggedIn: false
},
mutations: {
setToken (state, token) {
state.token = token
if (token) {
state.isUserLoggedIn = true
} else {
state.isUserLoggedIn = false
}
},
setUser (state, user) {
state.user = user
}
},
actions: {
setToken ({commit}, token) {
commit('setToken', token)
},
setUser ({commit}, user) {
commit('setUser', user)
}
}
})

How to pass input value to form onSubmit without using state in component that renders multiple forms?

This is a bit of a longwinded problem and giving me a ton of headache to solve.
I'm making a voting app. On the page there will be a list of polls on which you can vote. Each poll is a form consisting of input radio buttons representing the different options available for that poll.
What I was doing previously was saving the option you choose to component state in this.state.value and then passing it as an argument to an action creator when the form is submitted.
Problem with this approach is that if I click an option of one poll, and then click submit on another poll, I've actually submitted the wrong option to the wrong poll.
Is there a way to pass input value to form onSubmit without storing it in component state?
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Loading from '../Loading';
class MyPolls extends Component {
constructor(props) {
super(props);
this.state = {
skip: 0,
isLoading: true,
isLoadingMore: false,
value: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.fetchMyPolls(this.state.skip)
.then(() => {
setTimeout(() => {
this.setState({
skip: this.state.skip + 4,
isLoading: false
});
}, 1000);
});
}
sumVotes(acc, cur) {
return acc.votes + cur.votes
}
loadMore(skip) {
this.setState({ isLoadingMore: true });
setTimeout(() => {
this.props.fetchMyPolls(skip)
.then(() => {
const nextSkip = this.state.skip + 4;
this.setState({
skip: nextSkip,
isLoadingMore: false
});
});
}, 1000);
}
handleSubmit(title, e) {
// console.log(e.target);
e.preventDefault();
const vote = {
title,
option: this.state.value
};
console.log(vote)
}
handleChange(event) {
this.setState({ value: event.target.value });
}
renderPolls() {
return this.props.polls.map(poll => {
return (
<div
className='card'
key={poll._id}
style={{ width: '350px', height: '400px' }}>
<div className='card-content'>
<span className='card-title'>{poll.title}</span>
<p>
Total votes: {poll.options.reduce((acc, cur) => { return acc + cur.votes }, 0)}
</p>
<form onSubmit={e => this.handleSubmit(poll.title, e)}>
{poll.options.map(option => {
return (
<p key={option._id}>
<input
name={poll.title}
className='with-gap'
type='radio'
id={option._id}
value={option.option}
onChange={this.handleChange}
/>
<label htmlFor={option._id}>
{option.option}
</label>
</p>
)
})}
<button
type='text'
className='activator teal btn waves-effect waves-light'
style={{
position: 'absolute',
bottom: '10%',
transform: 'translateX(-50%)'
}}
>
Submit
<i className='material-icons right'>
send
</i>
</button>
</form>
</div>
<div className='card-reveal'>
<span className='card-title'>{poll.title}
<i className='material-icons right'>close</i>
</span>
<p>
dsfasfasdf
</p>
</div>
</div>
)
})
}
render() {
return (
<div className='center-align container'>
<h2>My Polls</h2>
{this.state.isLoading ? <Loading size='big' /> :
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-evenly',
alignItems: 'center',
alignContent: 'center'
}}>
{this.renderPolls()}
</div>}
<div className='row'>
{this.state.isLoadingMore ? <Loading size='small' /> :
<button
className='btn red lighten-2 wave-effect waves-light' onClick={() => this.loadMore(this.state.skip)}>
Load More
</button>}
</div>
</div>
);
}
}
function mapStateToProps({ polls }) {
return { polls }
}
export default connect(mapStateToProps, actions)(MyPolls);
App demo: https://voting-app-drhectapus.herokuapp.com/
(use riverfish#gmail.com and password 123 to login)
Github repo: https://github.com/drhectapus/voting-app
I'm open to any suggestions. Thanks!
The more "React'ish" pattern would be to break it down to more components.
a Poll is a component, a PollOption could be a component as well.
Where each can handle the state internally.
This will allow you to keep global state in your App or some other state manager like redux that will hold all of your polls and each can reference to the selected option (id).
Another thing worth pointing, is that you tend to pass a new function reference on each render call.
For example:
onSubmit={e => this.handleSubmit(poll.title, e)}
This is considered as bad practice because you can interfere with the Reconciliation and The Diffing Algorithm of react.
When you break it down to components that each can fire back a callback with its
props, then you don't need to pass the handler this way.
Here is a small example with your data:
const pollsFromServer = [
{
_id: "5a0d308a70f4b10014994490",
title: "Cat or Dog",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-16T06:30:34.855Z",
options: [
{ option: "Cat", _id: "5a0d308a70f4b10014994492", votes: 0 },
{ option: "Dog", _id: "5a0d308a70f4b10014994491", votes: 0 }
]
},
{
_id: "5a0c7941e655c22b8cce43d7",
title: "Blonde or Brunette?",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:28:33.909Z",
options: [
{ option: "Blonde", _id: "5a0c7941e655c22b8cce43d9", votes: 0 },
{ option: "Brunette", _id: "5a0c7941e655c22b8cce43d8", votes: 0 }
]
},
{
_id: "5a0c7924e655c22b8cce43d4",
title: "Coke or Pepsi",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:28:04.119Z",
options: [
{ option: "Coke", _id: "5a0c7924e655c22b8cce43d6", votes: 0 },
{ option: "Pepsi", _id: "5a0c7924e655c22b8cce43d5", votes: 0 }
]
},
{
_id: "5a0c78c2e655c22b8cce43d0",
title: "Favourite german car?",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:26:26.724Z",
options: [
{ option: "BMW", _id: "5a0c78c2e655c22b8cce43d3", votes: 0 },
{ option: "Mercedes", _id: "5a0c78c2e655c22b8cce43d2", votes: 0 },
{ option: "Audi", _id: "5a0c78c2e655c22b8cce43d1", votes: 0 }
]
}
];
class Poll extends React.Component {
onSubmit = optionId => {
const { pollId, onSubmit } = this.props;
onSubmit(pollId, optionId);
};
render() {
const { title, options, selectedOption } = this.props;
return (
<div>
<h3>{title}</h3>
<ul>
{options.map((o, i) => {
return (
<PollOption
isSelected={selectedOption === o._id}
onClick={this.onSubmit}
name={o.option}
optionId={o._id}
/>
);
})}
</ul>
</div>
);
}
}
class PollOption extends React.Component {
onClick = () => {
const { optionId, onClick } = this.props;
onClick(optionId);
};
render() {
const { name, isSelected } = this.props;
const selectedClass = isSelected ? "selected" : '';
return (
<li
className={`poll-option ${selectedClass}`}
onClick={this.onClick}
>
{name}
</li>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
polls: pollsFromServer,
submittedPolls: []
};
}
onPollSubmit = (pollId, optionId) => {
this.setState({
submittedPolls: {
...this.state.submittedPolls,
[pollId]: optionId
}
});
};
render() {
const { polls, submittedPolls } = this.state;
return (
<div>
{polls.map((p, i) => {
const selectedPoll = submittedPolls[p._id];
return (
<Poll
selectedOption={selectedPoll}
pollId={p._id}
onSubmit={this.onPollSubmit}
title={p.title}
options={p.options}
/>
);
})}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.poll-option{
cursor: pointer;
display: inline-block;
box-shadow: 0 0 1px 1px #333;
padding: 15px;
}
.selected{
background-color: green;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>