Displaying number of correct answers for quiz app - javascript

I'm currently stuck on trying to display the number of correct answers once the quiz is finished.
Basically, I have created a state that keeps track of the number of correct answers shown within the QuizItem component. If the user selected answer matches the correct answer, then the user selected answer turns to green and it will increase the state of correctCount (as seen in the code) to 1. This new value is then passed to the parent component of QuizItem which is QuizList.
/* eslint-disable react/prop-types */
import React from "react";
import AnswerButton from "../UI/AnswerButton";
import classes from "./QuizItem.module.css";
export default function QuizItem(props) {
const [correctCount, setCorrectCount] = React.useState(0)
function addToCorrectCount() {
setCorrectCount(correctCount + 1)
}
props.onSaveCorrectCountData(correctCount)
console.log(correctCount);
return (
<div>
<div key={props.id} className={classes.quizlist__quizitem}>
<h3 className={classes.quizitem__h3}>{props.question}</h3>
{props.choices.map((choice) => {
const styles = {
backgroundColor: choice.isSelected ? "#D6DBF5" : "white",
};
// React.useEffect(() => {
// if (choice.isSelected && choice.choice === choice.correct) {
// addToCorrectCount();
// }
// }, [choice.isSelected, choice.correct]);
function checkAnswerStyle() {
/* this is to indicate that the selected answer is right, makes button go green*/
if (choice.isSelected && choice.choice === choice.correct) {
addToCorrectCount()
return {
backgroundColor: "#94D7A2",
color: "#4D5B9E",
border: "none",
};
/* this is to indicate that the selected answer is wrong, makes button go red*/
} else if (choice.isSelected && choice.choice !== choice.correct) {
return {
backgroundColor: "#F8BCBC",
color: "#4D5B9E",
border: "none",
};
/* this is to highlight the right answer if a selected answer is wrong*/
} else if (choice.choice === choice.correct) {
return {
backgroundColor: "#94D7A2",
color: "#4D5B9E",
border: "none",
};
/* this is to grey out the incorrect answers*/
} else {
return {
color: "#bfc0c0",
border: "1px solid #bfc0c0",
backgroundColor: "white",
};
}
}
return (
<AnswerButton
key={choice.id}
onClick={() => {
props.holdAnswer(choice.id);
}}
style={props.endQuiz ? checkAnswerStyle() : styles}
>
{choice.choice}
</AnswerButton>
);
})}
</div>
</div>
);
}
// create a counter, and for every correct answer (green button), increase the counter by 1.
In the QuizList component, I have set another state to receive the incoming value from the QuizItem component and use this new value to display the number of correct answers once the check answers button has been clicked.
import React from "react";
import { nanoid } from "nanoid";
import QuizItem from "./QuizItem";
import Button from "../UI/Button";
import Card from "../UI/Card";
import classes from "./QuizList.module.css";
export default function QuizList(props) {
const [quiz, setQuiz] = React.useState([]);
const [endQuiz, setEndQuiz] = React.useState(false);
// const [newGame, setNewGame] = React.useState(false);
const [noOfCorrectAnswers, setNoOfCorrectAnswers] = React.useState()
function addCorrectCountHandler(correctCount) {
setNoOfCorrectAnswers(correctCount)
}
React.useEffect(() => {
/* This function turns HTML element entities into normal words */
function decodeHtml(html) {
const txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
fetch(
"https://opentdb.com/api.php?amount=5&category=9&difficulty=medium&type=multiple"
)
.then((res) => res.json())
.then((data) => {
const dataArray = data.results;
const newDataArray = dataArray.map((item) => {
return {
question: decodeHtml(item.question),
choices: [
{
choice: decodeHtml(item.correct_answer),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
{
choice: decodeHtml(item.incorrect_answers[0]),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
{
choice: decodeHtml(item.incorrect_answers[1]),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
{
choice: decodeHtml(item.incorrect_answers[2]),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
].sort(() => 0.5 - Math.random()),
id: nanoid(),
};
});
return setQuiz(newDataArray);
});
}, []);
// console.log(quiz);
function finishQuiz() {
setEndQuiz((prevEndQuiz) => !prevEndQuiz);
}
// function startNewGame() {
// setNewGame(true);
// }
function holdAnswer(quizId, choiceId) {
setQuiz((oldQuiz) =>
oldQuiz.map((quiz) => {
if (quiz.id !== quizId) return quiz;
return {
...quiz,
choices: quiz.choices.map((choice) =>
choice.id === choiceId
? // If the choice selected is the current choice, toggle its selected state
{ ...choice, isSelected: !choice.isSelected }
: // Otherwise, deselect the choice
{ ...choice, isSelected: false }
),
};
})
);
}
const quizItemComponents = quiz.map((item) => {
return (
<QuizItem
key={item.id}
question={item.question}
choices={item.choices}
holdAnswer={(id) => holdAnswer(item.id, id)}
endQuiz={endQuiz}
correct={quiz.correct}
onSaveCorrectCountData={addCorrectCountHandler}
/>
);
});
return (
<Card className={classes.quizlist}>
{quizItemComponents}
{!endQuiz && <Button onClick={finishQuiz}>Check Answers</Button>}
{endQuiz && (
<div className={classes.result}>
<p>You scored {noOfCorrectAnswers}/5 answers</p>
<Button onClick={startNewGame}>Play Again</Button>
</div>
)}
</Card>
);
}
The error that I was getting is that there were too many re-renders, so I tried using useEffect on the setCorrectCount state within my QuizItem component (this can be seen in my code and greyed out) but it would not tally up the count.
Is there a good workaround to this problem? Any help or advice would be appreciated.
Link to the code via Stackblitz:
https://stackblitz.com/edit/quizzical

Related

How do I write 'input[type="radio"]:checked + label' inside state in React?

I have the following selectors in my CSS file:
input[type="radio"]:checked + label.true {
background: #94D7A2;
border: none
}
input[type="radio"]:checked + label.false {
background: #F8BCBC;
border: none;
opacity: 0.5;
}
But I need to change the background on the click of another button (not the inputs), so I wanted to put this inside my component using state, like so:
.... other code here ....
const [styling, setStyling] = useReact({})
function handleClick() {
setStyling({
input[type="radio"]:checked + label.true {
background: #94D7A2;
border: none
},
input[type="radio"]:checked + label.false {
background: #F8BCBC;
border: none;
opacity: 0.5;
}
})
}
.... other code here ....
<input type="radio" id="radio1" style={${correct_answer === answer ? "true" : "false"}}>
<label htmlFor="radio1">Choice 1</label> // this is what I want to update
<button onClick={handleClick}>Check</button> // this is the button I want to activate the change of the state
Does this make sense? Basically what I need is to:
conditionally update the element with a .true or .false class
but I only want that styling to be visible once I click the "Check" button, so I need to change the state on click
This all works great when the styling is in my separate CSS file, but since I need to update it on click, I need it to be inside my component.
Maybe this can give you a start, to see how to use module css look here: https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/
import { useRef, useState } from "react"
const questionList = [
{
id: 1,
title: "What is the correct answer?",
options: [
{ text: "Wrong", correct: false },
{ text: "Right", correct: true },
{ text: "Wrong", correct: false }
]
},
{
id: 2,
title: "And now which is the correct one?",
options: [
{ text: "Right", correct: true },
{ text: "Wrong", correct: false },
{ text: "Wrong", correct: false }
]
},
{
id: 3,
title: "Can you choose the right option?",
options: [
{ text: "Wrong", correct: true },
{ text: "Wrong", correct: false },
{ text: "Right", correct: true }
]
}
]
export const Option = ({ option, showCorrectAnswers, questionId, answers, index }) => {
const correctAnswerStyle = { background: "#94D7A2" }
const wrongAnswerStyle = {
background: "#F8BCBC",
opacity: 0.5
}
const inputRef = useRef();
const id = `question_${questionId}_option${index}`;
const getStyle = () => {
if (!showCorrectAnswers) return {};
if (inputRef.current.checked && option.correct) return correctAnswerStyle;
return wrongAnswerStyle;
}
const handleClick = () => {
answers[questionId] = 1;
}
return <div style={getStyle()}>
<input id={id} onClick={handleClick} ref={inputRef} type="radio" name={`question_${questionId}`} />
<label htmlFor={id}>{option.text}</label>
</div>;
}
export const Question = ({ question, showCorrectAnswers, answers }) => {
return <div>
<p>{question.title}</p>
{question.options.map((option, index) =>
<Option key={`question_${question.id}_${index}`}
questionId={question.id}
option={option}
showCorrectAnswers={showCorrectAnswers}
answers={answers}
index={index} />)
}
</div>
}
export const Quiz = ({ questionList }) => {
const [showCorrectAnswers, setShowCorrectAnswers] = useState(false);
const [answers] = useState({});
const errorMsgRef = useRef();
const handleCheck = () => {
if (Object.keys(answers).length != questionList.length) {
errorMsgRef.current.innerHTML = "Please, answer all questions";
return;
}
errorMsgRef.current.innerHTML = "";
setShowCorrectAnswers(true);
}
return <div>
{questionList.map(question =>
<Question key={`question_${question.id}`}
question={question}
showCorrectAnswers={showCorrectAnswers}
answers={answers} />)
}
<button onClick={handleCheck} >Check</button>
<p ref={errorMsgRef}></p>
</div>
}
export default function App() {
return <Quiz questionList={questionList} />;
}

How to build a react button that stores the selection in an array

I am trying to create a list of buttons with values that are stored in a state and user is only allowed to use 1 item (I dont want to use radio input because I want to have more control over styling it).
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState([]);
const clickTask = (item) => {
setTask([...task, item.id]);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
const chosenTask = (item) => {
if (task.find((v) => v.id === item.id)) {
return true;
}
return false;
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={chosenTask(item) ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-cvhivt?file=/src/App.js
I am trying to only allow 1 item to be added to the state at all the time, but I dont know how to do this?
Example output is to have Easy and Fast in task state and is selected. If user click on Easy and Cheap, select that one and store in task state and remove Easy and Fast. Only 1 item can be in the task state.
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState();
const clickTask = (item) => {
setTask(item);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={item.id === task?.id ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
Is this what you wanted to do?
Think of your array as a configuration structure. If you add in active props initialised to false, and then pass that into the component you can initialise state with it.
For each task (button) you pass down the id, and active state, along with the text and the handler, and then let the handler in the parent extract the id from the clicked button, and update your state: as you map over the previous state set each task's active prop to true/false depending on whether its id matches the clicked button's id.
For each button you can style it based on whether the active prop is true or false.
If you then need to find the active task use find to locate it in the state tasks array.
const { useState } = React;
function Tasks({ config }) {
const [ tasks, setTasks ] = useState(config);
function handleClick(e) {
const { id } = e.target.dataset;
setTasks(prev => {
// task.id === +id will return either true or false
return prev.map(task => {
return { ...task, active: task.id === +id };
});
});
}
// Find the active task, and return its text
function findSelectedItem() {
const found = tasks.find(task => task.active)
if (found) return found.text;
return 'No active task';
}
return (
<section>
{tasks.map(task => {
return (
<Task
key={task.id}
taskid={task.id}
active={task.active}
text={task.text}
handleClick={handleClick}
/>
);
})};
<p>Selected task is: {findSelectedItem()}</p>
</section>
);
}
function Task(props) {
const {
text,
taskid,
active,
handleClick
} = props;
// Create a style string using a joined array
// to be used by the button
const buttonStyle = [
'taskButton',
active && 'active'
].join(' ');
return (
<button
data-id={taskid}
className={buttonStyle}
type="button"
onClick={handleClick}
>{text}
</button>
);
}
const taskConfig = [
{ id: 1, text: 'Easy and Fast', active: false },
{ id: 2, text: 'Easy and Cheap', active: false },
{ id: 3, text: 'Cheap and Fast', active: false }
];
ReactDOM.render(
<Tasks config={taskConfig} />,
document.getElementById('react')
);
.taskButton { background-color: palegreen; padding: 0.25em 0.4em; }
.taskButton:not(:first-child) { margin-left: 0.25em; }
.taskButton:hover { background-color: lightgreen; cursor: pointer; }
.taskButton.active { background-color: skyblue; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

React - how to access other elements in handleClick?

I am iterating over an array, and for each element, I pass it to a handleClick function. The question is, inside that handleClick function, how do I access the rest of the elements?
const listOfAnswers = questions[questionNumber].possibleAnswers.map((obj, i, arr) => {
return (
<Button
key={i}
style={
{
margin: '15px 0',
}
}
variant='contained'
onClick={e => handleClick(obj, e, arr)}
>
{obj.answer}
</Button>
)
})
const handleClick = async (obj, e, arr) => {
const { isCorrect, answer } = obj
if (isCorrect) {
setScore(score + 1)
e.target.style.backgroundColor = 'green'
await delay(100)
e.target.style.backgroundColor = ''
} else {
e.target.style.backgroundColor = 'red'
await delay(100)
e.target.style.backgroundColor = ''
}
nextQuestion()
}
What I am trying to do is: when a user clicks on the right answer, that button turns green. This is straightforward to implement. When a user clicks the wrong answer, it turns red. Also simple to implement. But what I want is: when a user clicks on the wrong answer, I want the right answer to turn green. For this I think I need to be able to access the rest of the elements, because in the handleClick function, you only have access to a single, individual element.
const [clicked, setClicked] = useState(-1);
const listOfAnswers = questions[questionNumber].possibleAnswers.map((obj, i, arr) => {
return (
<Button
key={i}
style={
{
margin: '15px 0',
color: clicked == -1
? "#ffffff"
: clicked == i && obj.isCorrect
? "#00ff00"
: cliked != i
? "#ffffff"
: "#ff0000"
}
}
variant='contained'
onClick={e => handleClick(i)}
>
{obj.answer}
</Button>
)
})
const handleClick = async (i) => {
setClicked(i)
nextQuestion()
}
const [clicked, setClicked] = useState(-1);
const listOfAnswers = questions[questionNumber].possibleAnswers.map((obj, i, arr) => {
return (
<Button
key={i}
style={
{
margin: '15px 0',
color: clicked == -1
? "#ffffff"
: clicked == i && obj.isCorrect
? "#00ff00"
: cliked != i
? "#ffffff"
: "#ff0000"
}
}
variant='contained'
onClick={handleClick}
>
{obj.answer}
</Button>
)
})
const handleClick = (e) => {
setClicked(e.target.key)
}
There are various way to do it:
Add a answer to the state (initialized to undefined). Your handleClick will set this state to the answer the user selected. Then if answer is defined, pass a green backgroundColor in the style of the correct button (next to your margin). And on the button whose obj.answer === answer, if obj.isCorrect is false, set a red backgroundColor. (note you would need to reset answer state to undefined in your nextQuestion)
Add a ref to the correct answer Button (https://reactjs.org/docs/refs-and-the-dom.html), and on handleClick, you can set ref.current.style.background.
UPDATE with example of ref
The following assumes that there is a unique correct answer (link to codesandbox: https://codesandbox.io/s/hungry-swirles-7q6wz2?file=/src/App.js:0-2575):
import "./styles.css";
import { useRef, useState } from "react";
import { Button } from "#mui/material";
export default function App() {
return (
<div className="App">
<h1>Quiz time!</h1>
<Questions />
</div>
);
}
const Questions = () => {
const [score, setScore] = useState(0);
const [questionNumber, setQuestionNumber] = useState(0);
const correctAnswerRef = useRef(null);
// If all questions were answered we display the score
if (questionNumber >= questions.length) {
const reset = () => {
setScore(0);
setQuestionNumber(0);
};
return (
<div>
Score: {score} <Button onClick={reset}>Take the quiz again</Button>
</div>
);
}
const handleClick = async (obj, e) => {
const { isCorrect } = obj;
// always set current answer to green
if (correctAnswerRef.current) {
correctAnswerRef.current.style.backgroundColor = "green";
}
if (isCorrect) {
setScore(score + 1);
} else {
// if wrong answer was selected, put a red background
e.target.style.backgroundColor = "red";
}
// just to simulate `delay` since I don't have that util
await new Promise((resolve) => setTimeout(resolve, 1000));
e.target.style.backgroundColor = "";
if (correctAnswerRef.current) {
correctAnswerRef.current.style.backgroundColor = "";
}
// too lazy to implement nextQuestion, so I just increment the question number
setQuestionNumber(questionNumber + 1);
};
const question = questions[questionNumber];
const listOfAnswers = question.possibleAnswers.map((obj) => {
return (
<div>
<Button
key={obj.answer} // ideally each answer should have one (and you should never use index!)
ref={obj.isCorrect ? correctAnswerRef : undefined}
style={{
margin: "15px 0"
}}
variant="contained"
onClick={(e) => handleClick(obj, e)}
>
{obj.answer}
</Button>
</div>
);
});
return (
<div>
<div>{question.question}</div>
{listOfAnswers}
</div>
);
};
const questions = [
{
question: "Is this working?",
possibleAnswers: [
{
answer: "Yes",
isCorrect: true
},
{
answer: "No",
isCorrect: false
}
]
},
{
question: "Is this a good question?",
possibleAnswers: [
{
answer: "Nope",
isCorrect: false
},
{
answer: "Yes, it is!",
isCorrect: true
}
]
}
];

Why is array.findIndex returning not a fucntion. (mozilla says it basically is?)

changing state.findIndex to state.ids.findIndex resolved this issue.
However when calling the function onClick it automatically updates ALL of the active values to true. rather than waiting for the click event.
I am trying to set up an onClick method/function that will find the active: true and set it to false and then find the object with the matching id: xxxxxx and set that object's active: to true.
According to developer.mozilla.org and several other sites, my method should theoretically work, or, more likely I'm misinterpreting it.
This is the error it's returning
This is the function I'm trying to write - why does it return that findindex is not a function?
function changeActiveField(im) {
const i = state.findIndex((obj) => obj.active === true);
state[i].active = false;
const index = state.findIndex((obj) => obj.id === im);
state[index].active = true;
}
Here the const i finds the index of the obj{ active: true} and changes it to obj{ active: false}
then const index finds the obj with the obj{id: value} that matches the im which is passed in from the clicked component...
<div className="thumbs">
{state.ids.map((i) => (
<Image
className="carouselitem"
rounded
fluid
onClick={changeActiveField(i.id)}
src={"http://img.youtube.com/vi/" + i.id + "/hqdefault.jpg"}
size="small"
/>
))}
</div>
Go to https://test.ghostrez.net and navigate to the Services page to see the video menu. The small thumbnails are the "clickable" items that should trigger the changeActiveField function in turn setting the clicked id to active: true and changing the activevid div
here is the full page code.
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import { Embed, Image, Loader } from "semantic-ui-react";
import "./Player.css";
var state = {
ids: [
{
id: "iCBvfW08jlo",
active: true,
},
{
id: "qvOcCQXZVg0",
active: false,
},
{
id: "YXNC3GKmjgk",
active: false,
},
],
};
function firstActiveId(ids) {
for (var i = 0; i < ids.length; i++) {
if (ids[i].active) {
return ids[i].id;
}
}
}
function changeActiveField(im) {
const i = state.findIndex((obj) => obj.active === true);
state[i].active = false;
const index = state.findIndex((obj) => obj.id === im);
state[index].active = true;
}
export default class Player extends React.Component {
constructor(props) {
super(props);
this.state = {
state: false,
};
}
render() {
return (
<div className="carouselwrap">
<div className="activevid">
<Embed
active
autoplay={false}
color="white"
hd={false}
id={firstActiveId(state.ids)}
iframe={{
allowFullScreen: true,
style: {
padding: 10,
},
}}
placeholder={
"http://img.youtube.com/vi/" +
firstActiveId(state.ids) +
"/hqdefault.jpg"
}
source="youtube"
/>
</div>
<div className="thumbs">
{state.ids.map((i) => (
<>
<Image
className="carouselitem"
rounded
fluid
onClick={changeActiveField(i.id)}
src={"http://img.youtube.com/vi/" + i.id + "/hqdefault.jpg"}
size="small"
/>
<h2>
{/*this is for testing purposes only*/}
{i.id} {i.active ? "true" : "false"}
</h2>
</>
))}
</div>
</div>
);
}
}
the problem was that the state was not the array, rather ids i the array... so changing my statements to state.ids.findIndex solved that problem
function changeActiveField(im) {
const i = state.findIndex((obj) => obj.active === true);
state[i].active = false;
const index = state.findIndex((obj) => obj.id === im);
state[index].active = true;
}

Unable to update state

I am using MERN and Redux.
I have a clickHandler function that calls a findAuthor function which is imported from my actions. This finds a user by their id and returns it. I have added the user to the global state. I want to then retrieve the user and add their name to local state but i can't get this working. I keep getting this error TypeError: this.props.subAuthor is undefined. What am i missing here? When i try just printing to console i get no object showing until the second click. How do i get it t update straight away?
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
import { getUser } from "../../actions/authActions";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchSubjects();
this.props.fetchComments();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects
// description, summary and comments all invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
name: "",
};
}
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
// and make the description and comments visible
const { viewDescription } = this.state;
this.setState({ viewDescription: viewDescription === id ? -1 : id });
// add relevant comments to the state
var i;
var temp = [];
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
this.setState({
comments: temp,
});
// 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);
//testing getUser
this.findAuthor(id); // this updates the tempUser in state
this.setState({ name: this.props.subAuthor.name });
};
// 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) => {
const subject = this.props.subjects.find((subject) => subject._id === id);
// when no subject was found, the updateSubject won't be called
subject &&
this.props.updateSubject(id, rate, subject.noOfVotes, subject.rating);
alert("Thank you for rating this subject.");
};
// take in the id of the subject
// find it in the props
// get its author id
// call the getUser passing the author id
findAuthor(id) {
console.log("Hitting findAuthor function");
const subject = this.props.subjects.find((subject) => subject._id === id);
const authorId = subject.author;
console.log(authorId);
this.props.getUser(authorId);
}
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.name;
return (
<div key={subject._id}>
<div className="subjectTitle">
<p
className="title"
onClick={() => this.clickHandler(subject._id)}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
{subject.title}
</p>
<p className="rate">
Rate this subject:
<button onClick={() => this.rateHandler(subject._id, 1)}>
1
</button>
<button onClick={() => this.rateHandler(subject._id, 2)}>
2
</button>
<button onClick={() => this.rateHandler(subject._id, 3)}>
3
</button>
<button onClick={() => this.rateHandler(subject._id, 4)}>
4
</button>
<button onClick={() => this.rateHandler(subject._id, 5)}>
5
</button>
</p>
<p className="rating">
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="subjectAuthor">
<p className="author">
Subject created by: {subjectAuthor}
<br /> {subject.date}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links:</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.author}
</p>
</div>
);
})}
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
updateSubject: PropTypes.func.isRequired,
getUser: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
subAuthor: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
newComment: state.comments.item,
subAuthor: state.auth.tempUser[0],
});
// export default Subject;
export default connect(mapStateToProps, {
fetchSubjects,
fetchComments,
updateSubject, // rate subject
getUser, // used for getting author name
})(Subject, Comment);
I'd like to offer an alternative solution to the current code you have been writing so far. I know this is not codereview (and it wouldn't be on topic there, unless it is actually working code), but still, I would like to show you a different way of dividing up your components.
From what I see, you have many components, currently all jampacked in to one very large component. This can complicate things on the long run, and if you can, you should avoid it.
As I see it from the code you have posted, you really have several components, which I divided in:
Subject
Comment
User
Rating
RatingViewer
By dividing your now large component, you are making it easier to handle the data for one component at a later time and reuse the components you are making. You might want to reuse some of these components.
For the purpose of an alternative solution, I created a very quick and basic demo on how you might refactor your code. This is only a suggestion, in the hope that it will also solve your current problem.
The problem you are having is that you want to load that data, and use it directly. Any fetch operation is however asynchronous, so after you have called this.props.getUser(authorId); your author gets added somewhere in your state, but it will not be available until fetching has been completed and your component gets re-rendered.
I hope the information in the demo can give you some insight, it might not be exactly matching your scenario, but it should give you an indication of what you could do differently.
// imports
const { Component } = React;
const { Provider, connect } = ReactRedux;
const { render } = ReactDOM;
const { createStore, combineReducers } = Redux;
// some fake db data
const db = {
comments: [
{ id: 1, subject: 2, user: 2, comment: 'Interesting book' },
{ id: 2, subject: 2, user: 3, comment: 'Is interesting the only word you know, you twit' }
],
subjects: [
{
id: 1,
title: 'Some interesting title',
summary: 'Some interesting summary / plot point',
author: 2,
rate: 0,
noOfVotes: 0
},
{
id: 2,
title: 'Some less interesting title',
summary: 'Some more interesting summary / plot point',
author: 1,
rate: 5,
noOfVotes: 2
}
],
authors: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' }
],
users: [
{ id: 1, name: 'user 1' },
{ id: 2, name: 'user 2' },
{ id: 3, name: 'user 3' }
]
};
// reducers
const authorReducer = ( state = {}, action ) => {
switch (action.type) {
case 'author/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const userReducer = ( state = {}, action ) => {
switch (action.type) {
case 'user/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const subjectReducer = ( state = {}, action ) => {
switch (action.type) {
case 'subject/retrieved':
return Object.assign( {}, ...action.payload.map( subject => ({ [subject.id]: subject }) ) );
case 'subject/add':
return { ...state, [action.payload.id]: action.payload };
case 'subject/update':
const { id } = action.payload;
return { ...state, [id]: action.payload };
default:
return state;
}
};
const commentReducer = ( state = [], action ) => {
switch (action.type) {
case 'comment/retrieved':
return action.payload.slice();
case 'comments/add':
return [...state, action.payload ];
default:
return state;
}
};
// create the store
const store = createStore( combineReducers({
users: userReducer,
authors: authorReducer,
comments: commentReducer,
subjects: subjectReducer
}) );
// some promise aware fetch methods
const fakeFetch = (entity, filter = null) => {
const entities = db[entity];
return Promise.resolve( (filter ? entities.filter( filter ) : entities).map( e => ({...e}) ) );
}
const fakeUpdate = (entity, id, updatedValue ) => {
const targetEntity = db[entity].find( e => e.id === id );
if (!targetEntity) {
return Promise.reject();
}
Object.assign( targetEntity, updatedValue );
return Promise.resolve( { ...targetEntity } );
}
// separate components
class App extends Component {
render() {
return <Subjects />;
}
}
// subjects component
// cares about retrieving the subjects and displaying them
class SubjectsComponent extends Component {
componentDidMount() {
this.props.fetchSubjects();
}
render() {
const { subjects } = this.props;
if (!subjects || !subjects.length) {
return <div>Loading</div>;
}
return (
<div>
{ subjects.map( subject => <Subject key={subject.id} subject={subject} /> ) }
</div>
);
}
}
// subject component
// displays a subject and fetches the comments for "all" subjects
// this should probably only fetch its own comments, but then reducer has to be changed aswell
// to be aware of that
class SubjectComponent extends Component {
componentDidMount() {
this.props.fetchComments();
}
render() {
const { subject } = this.props;
return (
<div className="subject">
<h1>{ subject.title }<RateView subject={subject} /></h1>
<p>{ subject.summary }</p>
<Rate subject={subject} />
<h2>Comments</h2>
{ this.props.comments && this.props.comments.map( comment => <Comment key={comment.id} comment={comment} /> ) }
</div>
);
}
}
// Just displays a comment and a User component
const Comment = ({ comment }) => {
return (
<div className="comment">
<p>{ comment.comment }</p>
<User id={comment.user} />
</div>
);
}
// User component
// fetches the user in case he hasn't been loaded yet
class UserComponent extends Component {
componentDidMount() {
if (!this.props.user) {
this.props.fetchUser( this.props.id );
}
}
render() {
return <span className="user">{ this.props.user && this.props.user.name }</span>;
}
}
// shows the current rating of a post
const RateView = ({ subject }) => {
if (subject.noOfVotes === 0) {
return <span className="rating">No rating yet</span>;
}
const { rate, noOfVotes } = subject;
return <span className="rating">Total rating { (rate / noOfVotes).toFixed(1) }</span>;
}
// enables voting on a subject, can be triggered once per rendering
// this should truly be combined with the user who rated the subject, but it's a demo
class RateComponent extends Component {
constructor() {
super();
this.onRateClicked = this.onRateClicked.bind( this );
this.state = {
hasRated: false,
rateValue: -1
};
}
onRateClicked( e ) {
const userRate = parseInt( e.target.getAttribute('data-value') );
const { subject } = this.props;
this.setState({ hasRated: true, rateValue: userRate }, () => {
this.props.updateRate( { ...subject, rate: subject.rate + userRate, noOfVotes: subject.noOfVotes + 1 } );
});
}
render() {
if (this.state.hasRated) {
return <span className="user-rate">You rated this subject with { this.state.rateValue }</span>;
}
return (
<div>
{ [1, 2, 3, 4, 5].map( value => <button type="button" onClick={ this.onRateClicked } data-value={value} key={value}>{ value }</button> ) }
</div>
);
}
}
// connecting all the components to the store, with their states and dispatchers
const Subjects = connect(
state => ({ subjects: Object.values( state.subjects ) }),
dispatch => ({
fetchSubjects() {
return fakeFetch('subjects').then( result => dispatch({ type: 'subject/retrieved', payload: result }) );
}
}))( SubjectsComponent );
// ownProps will be used to filter only the data required for the component that it is using
const Subject = connect(
(state, ownProps) => ({ comments: state.comments.filter( comment => comment.subject === ownProps.subject.id ) }),
dispatch => ({
fetchComments() {
return fakeFetch('comments' ).then( result => dispatch({ type: 'comment/retrieved', payload: result }) );
}
}))( SubjectComponent );
const User = connect(
(state, ownProps) => ({ user: state.users[ownProps.id] }),
dispatch => ({
fetchUser( id ) {
return fakeFetch('users', user => user.id === id).then( result => dispatch({ type: 'user/add', payload: result[0] }) );
}
}))( UserComponent );
const Rate = connect( null, dispatch => ({
updateRate( updatedSubject ) {
return fakeUpdate('subjects', updatedSubject.id, updatedSubject).then( updated => dispatch({ type: 'subject/update', payload: updated }) );
}
}))( RateComponent );
// bind it all together and run the app
const targetElement = document.querySelector('#container');
render( <Provider store={store}><App /></Provider>, targetElement );
.user {
font-style: italic;
font-size: .9em;
}
.comment {
padding-left: 10px;
background-color: #efefef;
border-top: solid #ddd 1px;
}
h1, h2 {
font-size: .8em;
line-height: .9em;
}
.rating {
padding: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" integrity="sha512-SUJujhtUWZUlwsABaZNnTFRlvCu7XGBZBL1VF33qRvvgNk3pBS9E353kcag4JAv05/nsB9sanSXFbdHAUW9+lg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" integrity="sha512-SYsXmAblZhruCNUVmTp5/v2a1Fnoia06iJh3+L9B9wUaqpRVjcNBQsqAglQG9b5+IaVCfLDH5+vW923JL5epZA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.1/react-redux.min.js" integrity="sha512-Ae6lzX7eAwqencnyfCtoAf2h3tQhsV5DrHiqExqyjKrxvTgPHwwOlM694naWdO2ChMmBk3by5oM2c3soVPbI5g==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha512-P36ourTueX/PrXrD4Auc1kVLoTE7bkWrIrkaM0IG2X3Fd90LFgTRogpZzNBssay0XOXhrIgudf4wFeftdsPDiQ==" crossorigin="anonymous"></script>
<div id="container"></div>

Categories

Resources