So currently i have a react component. I have declared an array which contains a series of objects for some sample data. As the initial state for 'currentStep' is 0, I expect the <div>Screen 1</div> to render, however, all i get is a blank screen.
Any ideas?
import React, { Component } from 'react';
/**
* sample data to pass through
*/
const contents =
[
{ title: 'First Screen', step: 0, children: <div>Screen 1</div> },
{ title: 'Second Screen', step: 1, children: <div>Screen 2</div> },
{ title: 'Third Screen', step: 2, children: <div>Screen 3</div> },
];
class Wizard extends Component {
state = {
currentStep: 0,
}
Content = () => {
const { currentStep } = this.state;
contents.map((content) => {
if (content.step === currentStep) { return content.children; }
return null;
});
}
render() {
return (
<div>{this.Content()}</div>
);
}
}
export default Wizard;
You need to return your map in Content. Right now you are returning nothing. For ex:
Content = () => {
const { currentStep } = this.state;
return contents.map((content) => {
if (content.step === currentStep) { return content.children; }
return null;
});
}
Your Content function isn't actually returning anything, but your map function is. If you return contents.map(...), then you should get what you are expecting.
Related
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
Iterates on the todos array. Objects inside have the isChecked property. If isChecked === true marks the checkbox, ifisChecked === false the checkbox is uncheckbox. When I click on the checkbox. I can't mark or uncheckbox
Demo here: https://stackblitz.com/edit/react-ds9rsd
class App extends Component {
constructor() {
super();
this.state = {
todos: [
{
name:'A',
id: 1,
isChecked: true
},
{
name:'B',
id: 2,
isChecked: false
},
{
name:'C',
id: 3,
isChecked: true
}
]
};
}
checked = (e) => {
console.log(e.target.checked)
}
render() {
return (
<div>
{this.state.todos.map((todo, index) => {
return <input type="checkbox" checked={todo.isChecked} onChange={(e) => this.checked(e)}/>
})}
</div>
);
}
}
In checked() function you are just logging the value. Instead of that you need to do setState() to save new state.
A possibile solution could be updating the render function like this:
render() {
return (
<div>
{this.state.todos.map((todo, index) => {
return <input label={todo.name} type="checkbox" checked={todo.isChecked}
onChange={(e) => this.checked(todo)}/>
})}
</div>
);
}
and the checked method like this:
checked = (e) => {
this.setState(state => {
const list = state.todos.map((item) => {
if (item.name === e.name) {
return item.isChecked = !item.isChecked;
} else {
return item;
}
});
return {
list,
};
});
}
You will need to add a function and call it for each checkbox
import React, { Component } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
class App extends Component {
constructor() {
super();
this.state = {
todos: [
{
name: "A",
id: 1,
isChecked: true
},
{
name: "B",
id: 2,
isChecked: false
},
{
name: "C",
id: 3,
isChecked: true
}
]
};
}
checked = index => {
/** get the current state */
let _todos = this.state.todos;
/** assign opposite value: true to false or false to true */
_todos[index].isChecked = !_todos[index].isChecked;
/** update state */
this.setState({ todos: _todos });
};
render() {
return (
<div>
{this.state.todos.map((todo, index) => {
/** call the function passing the index value */
return (
<input
label={todo.name}
type="checkbox"
checked={todo.isChecked}
onChange={this.checked.bind(this, index)}
/>
);
})}
</div>
);
}
}
render(<App />, document.getElementById("root"));
I have the following component which I use for navigation:
import React, { Component } from "react";
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
class TabBar extends Component {
constructor(props) {
super(props);
const noTankAvailable = this.props.tank.tankData.noAssignedTank;
console.log("noTankAvailable", noTankAvailable);
if (noTankAvailable === true || noTankAvailable === undefined) {
this.tabs = [
{ label: "Registration", icon: faSimCard, url: "registration" }
];
} else {
this.tabs = [
{ label: "Status", icon: faChartBar, url: "status" },
{ label: "History", icon: faHistory, url: "history" },
{ label: "Properties", icon: faSlidersH, url: "properties" }
];
}
...
}
...
render() {
const { location, match } = this.props;
const { pathname } = location;
return (
<div>
<Tabs
className="tabBar"
contentContainerStyle={tabBarStyles.content}
inkBarStyle={tabBarStyles.ink}
tabItemContainerStyle={tabBarStyles.tabs}
value={pathname}
>
{this.renderTabs(match)}
</Tabs>
</div>
);
}
}
const mapStateToProps = state => ({
...state
});
export default connect(mapStateToProps)(TabBar);
This is my redux reducer:
import {
TANK_REQUEST,
TANK_FAILURE,
TANK_SUCCESS,
} from '../actions/tankActions';
const testState = {
isLoading: false,
currentTank: "",
tankData: {}
};
export default (state = testState, action) => {
switch (action.type) {
case TANK_REQUEST:
return Object.assign({}, state, { isLoading: true });
case TANK_SUCCESS:
if (action.tankData.length > 0) {
const currentTank = action.tankData[0];
const tankData = Object.assign({}, state.tankData, { [currentTank._id]: currentTank, isLoading: false });
return Object.assign({}, state, { currentTank: currentTank._id, tankData });
} else {
const tankData = Object.assign({}, state.tankData, { noAssignedTank: true });
return Object.assign({}, state, { tankData });
}
case TANK_FAILURE:
return Object.assign({}, state, { currentTank: action.id, isLoading: false, error: action.err });
default:
return state
}
}
The following scenario is given: When a user logs in, it fetches an API to get (water) tanks. If the user does not have an assigned tank, the application should redirect to the registration view and the navigation should only show "registration".
So I fetch via an action. In my reducer I check if I got data and if not I will add noAssignedTank: true to my state. I want to check now in my TabBar component if this is true or not and hide/show navigation links depending on that.
My problem is that I would need to wait till the TANK_FETCHING_SUCCESS reducer is resolved to check if noAssignedTank is true.
You can see that the first console output is my console.log("noTankAvailable", noTankAvailable);. So my if/else statement is not working because at first it is undefined before it gets an value.
You have to make this.tabs a state of your component and update it during lifecycle methods of your component.
Retrieving of tankData has been secured by additionnal tests (props.tank && props.tank.tankData).
Initial state is initialized in constructor with the props.
A reference on previous tank is also kept in state (prevTanData) for comparison when props will change (when the asynchronous value in store will be updated, the connected component will be notified by redux and a call to getDerivedStateFromProps will follow).
If prevTankData is the same as nextProps.tank.tankData then we return null to tell React the state does not need to change.
Note that for version React < 16, you wil have to use the instance method componentWillReceiveProps instead of the static getDerivedStateFromProps.
class TabBar extends Component {
constructor(props) {
super(props);
this.state = {
tabs: TabBar.computeTabsFromProps(props),
prevTankData: props.tank && props.tank.tankData,
};
};
static computeTabsFromProps(props) {
const noTankAvailable = props.tank &&
props.tank.tankData &&
props.tank.tankData.noAssignedTank;
console.log("noTankAvailable", noTankAvailable);
if (noTankAvailable === true || noTankAvailable === undefined) {
return [
{
label: "Registration",
icon: faSimCard,
url: "registration"
}
];
} else {
return [
{ label: "Status", icon: faChartBar, url: "status" },
{ label: "History", icon: faHistory, url: "history" },
{ label: "Properties", icon: faSlidersH, url: "properties" }
];
}
}
static getDerivedStateFromProps(nextProps, prevState) {
if ((nextProps.tank && nextProps.tank.tankData) !== prevState.prevTankData) {
return {
prevTankData: nextProps.tank && nextProps.tank.tankData,
tabs: TabBar.computeTabsFromProps(nextProps),
}
}
return null;
}
render() {
...
}
}
When I'm clicking the button, I am getting that all the values are undefined for the "name" and the "value". I am not sure why, my binding seems correct.
I've tried changing the bindings, I've tried calling an anonymous function for the onClick and passing in the item within my map function. No luck.
import React, { Component } from 'react'
const starterItems = [{ id: 1, name: 'longsword', enhancement: 4 },
{ id: 2, name: 'gauntlet', enhancement: 9 },
{ id: 3, name: 'wizard\'s staff', enhancement: 14 }];
export default class Items extends Component {
constructor(props) {
super(props)
this.handleScoreChange = this.handleScoreChange.bind(this);
this.state = {
items: starterItems
}
}
handleScoreChange(e){
let { name, value } = e.target;
const id = name;
const newScore = value++;
const items = this.state.items.slice();
items.forEach((item) => {
if (item[id] === name){
item.enhancement = newScore
}
});
this.setState(items);
};
render() {
return (
<div>
<h3 data-testid="title">Items</h3>
{this.state.items.map(item => (
<div key={item.id}>
<div name={item.name}data-testid="item">{item.name}</div>
<div name={item.enhancement}data-testid="enhancement" value=
{item.enhancement}>{item.enhancement}
</div>
<button onClick={this.handleScoreChange}>Enhance</button>
</div>
))}
</div>
);
};
}
I am expecting the value of the item passed through to +1
e.target is the DOM reference for the button
including name and value as attributes for the div are not necessary
If you want to get the values for the current name and enhancement when clicking you can add a binding
{
this.state.items.map(item => {
const onClick = this.handleScoreChange.bind(this, item.name, item.enhancement)
return (
<div key={item.id}>
<div name={item.name}data-testid="item">{item.name}</div>
<div name={item.enhancement}data-testid="enhancement" value={item.enhancement}>{item.enhancement}</div>
<button onClick={onClick}>Enhance</button>
</div>
)
)
}
...
handleScoreChange(name, enhancement) {
// your logic here
}
<button onClick={()=>this.handleScoreChange(...item)}>Enhance</button>
please try this i am sure this will work for you
thanks
This Solution Worked. Slight modification from what Omar mentioned. Thank you
import React, { Component } from 'react'
const starterItems = [{ id: 1, name: 'longsword', enhancement: 4 },
{ id: 2, name: 'gauntlet', enhancement: 9 },
{ id: 3, name: 'wizard\'s staff', enhancement: 14 }];
export default class Items extends Component {
constructor(props) {
super(props)
this.state = {
items: starterItems
}
}
enhanceItem(passedItem) {
const newScore = passedItem.enhancement + 1;
const items = this.state.items.slice(); /* creates a copy of state that we can change */
items.forEach((item) => {
if (item.id === passedItem.id) {
return item.enhancement = newScore
}
});
this.setState(items);
};
render() {
return (
<div>
<h3 data-testid="title">Items</h3>
{
this.state.items.map(item => {
const onClick = this.enhanceItem.bind(this, item)
/* this line above binds the item with the onClick Function */
/* notice below, I had to put a return since I added the above code */
return (
<div key={item.id}>
<div data-testid="item">{item.name}</div>
<div data-testid="enhancement">{item.enhancement}</div>
{/* We can use the "data=testid" as a way to grab an item, since it won't impact the html any */}
<button onClick={onClick}>Enhance</button>
</div>
)
})};
</div>
);
};
}
I need some advice on testing functions in terms of how and what
say I have some state.
state = {
categories: [this is full of objects],
products: [this is also full of objects]
}
then I have this function:
filterProducts = () => {
return this.state.products.filter((product => (
product.categories.some((cat) => (
cat.title == this.state.chosenCategory
))
)))
}
this function filters the products array by working out if the products are part of the selected category.
how would you test this?
I've tried this
let productsStub = [
{id: 1, title: 'wine01', showDescription: false},
{id: 2, title: 'wine02', showDescription: false},
{id: 3, title: 'wine03', showDescription: false}
]
wrapper = shallow(<Menu
categories={categoriesStub}
products={productsStub}
/>);
it('should filter products when searched for', () => {
const input = wrapper.find('input');
input.simulate('change', {
target: { value: '01' }
});
expect(productsStub.length).toEqual(1);
});
what this test (I think) is saying, when I search for 01, I expect the product state (well the stub of the state) to filter and return only 1 result. however the test fails and says expected: 1 received: 3 i.e. the filtering isn't working.
I know I could also do wrapper.instance.filterProducts() but again, I'm not very comfortable on function testing in jest.
any advice? would be great to chat it through with someone
thanks
I replicated your problem statement, but not sure how are you maintaining the state model (props/state). But this might help. :)
Checkout the working example here: https://codesandbox.io/s/6zw0krx15k
import React from "react";
export default class Hello extends React.Component {
state = {
categories: [{ id: 1 }],
products: this.props.products,
selectedCat: 1,
filteredProducts: []
};
filterProducts = value => {
let filteredVal = this.props.products.filter(
product => product.id === parseInt(value)
);
this.setState({
filteredProducts: filteredVal
});
};
setValue = e => {
this.setState({
selectedCat: e.target.value
});
this.filterProducts(e.target.value);
};
render() {
return (
<div>
Filter
<input value={this.state.selectedCat} onChange={this.setValue} />
</div>
);
}
}
import { shallow } from "enzyme";
import Filter from "./Filter";
import React from "react";
let productsStub = [
{ id: 1, title: "wine01", showDescription: false },
{ id: 2, title: "wine02", showDescription: false },
{ id: 3, title: "wine03", showDescription: false }
];
let wrapper = shallow(<Filter products={productsStub} />);
it("should filter products when searched for", () => {
const input = wrapper.find("input");
input.simulate("change", {
target: { value: "1" }
});
expect(wrapper.state().filteredProducts.length).toEqual(1);
});