Adding an image into a ToggleButtonGroup - reactjs

I want to implement this design:
And I'm currently kind of stuck at the images-answers part.
I have the idea, but there is a problem.
import { useState } from 'react';
import { ToggleButtonGroup, ToggleButton } from 'react-bootstrap';
import './css/GameAnswers.css';
import GameImage from './GameImage';
type GameAnswersProps = {
arrayAnswers: Array<string>,
arrayAnswersImages?: Array<string>,
correctAnswer: string,
setFinishedGameHandler: Function
}
function GameAnswers(props: GameAnswersProps) {
const [selectedAnswer, setSelectedAnswer] = useState("");
const handleAnswerChange = function (e: React.ChangeEvent<HTMLInputElement>) {
setSelectedAnswer(e.target.value);
}
const determineButtonVariant = function (answer: string) {
if (answer !== selectedAnswer) {
return "primary";
} else if (answer === props.correctAnswer) {
props.setFinishedGameHandler(true);
return "success";
} else {
props.setFinishedGameHandler(false);
return "danger";
}
}
return (
<div className="game-answers">
{
props.arrayAnswersImages === undefined ?
<GameImage className="game-details-image" images={[{ imgSrc: "/Untitled.png", imgAlt: 'test' }]} /> :
""
}
<ToggleButtonGroup type="radio" name="answers">
{props.arrayAnswers.map(function (answer, index) {
return <div>
{props.arrayAnswersImages !== undefined ?
<GameImage className="game-togglebutton" images={[{ imgSrc: props.arrayAnswersImages[index], imgAlt: 'test' }]} /> : ""
}
<ToggleButton
id={answer}
value={answer}
onChange={handleAnswerChange}
variant={determineButtonVariant(answer)}
>{answer}</ToggleButton>
</div>
})}
</ToggleButtonGroup>
</div>
);
}
export default GameAnswers;
Because the map function returns a div, my ToggleButton doesn't work properly. If I will click on the buttons, nothing will happen.
If I remove the div wrapping, I will get an error because I return two components.
If I also remove the GameImage component, everything will work fine, but I will have no image above the text.
So, how can I make my code work properly while I still have the image above the ToggleButton?

In react you can use Fragments, it's a common way to exclude that extra node when e.g. returing a list.
Change your wrapping <div></div> tag into ->
<React.Fragment></React.Fragment>.
A shorter syntax for this is <></>.

Related

loading components twice, probably because of useEffect wrong set-up

I have built a ToDo React App (https://codesandbox.io/s/distracted-easley-zjdrkv) that does the following:
User write down an item in the input bar
User hit "enter"
Item is saved into the list below (local storage, will update later)
There is some logic to parse the text and identify tags (basically if the text goes "#tom:buy milk" --> tag=tom, text=buy milk)
The problem I am facing are:
useEffect runs twice at load, and I don't understand why
After the first item gets saved, if I try saving a second item, the app crashes. Not sure why, but I feel it has to do with the point above...and maybe the event listener "onKeyDown"
App
import { useState, useEffect } from 'react'
import './assets/style.css';
import data from '../data/data.json'
import InputBar from "./components/InputBar/InputBar"
import NavBar from "./components/NavBar/NavBar"
import TabItem from "./components/Tab/TabItem"
function App() {
const [dataLoaded, setDataLoaded] = useState(
() => JSON.parse(localStorage.getItem("toDos")) || data
)
useEffect(() => {
localStorage.setItem("toDos", JSON.stringify(dataLoaded))
console.log('update')
}, [dataLoaded])
function deleteItem(id){
console.log(id)
setDataLoaded(oldData=>{
return {
...oldData,
"items":oldData.items.filter(el => el.id !== id)
}
})
}
return (
<div className='container'>
<NavBar/>
<InputBar
setNewList = {setDataLoaded}
/>
{
//Items
dataLoaded.items.map(el=>{
console.log(el)
return <TabItem item={el} key={el.id} delete={deleteItem}/>
})
}
</div>
)
}
export default App
InputBar
import { useState, useEffect } from 'react'
import { nanoid } from 'nanoid'
import '../../assets/style.css';
export default function InputBar(props){
const timeElapsed = Date.now();
const today = new Date(timeElapsed);
function processInput(s) {
let m = s.match(/^(#.+?:)?(.+)/)
if (m) {
return {
tags: m[1] ? m[1].slice(1, -1).split('#') : ['default'],
text: m[2],
created: today.toDateString(),
id:nanoid()
}
}
}
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(e.target.value), ...oldData.items]
}
}
)
e.target.value=""
}
}
return(
<div className="main-input-div">
<input type="text" onKeyDown={(e) => handleKeyDown(e)}/>
</div>
)
}
Tab
import { useState } from 'react'
import "./tab-item.css"
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTrash } from "#fortawesome/free-solid-svg-icons";
export default function TabItem(props) {
return (
<div className="tab-item">
<div className="tab-item-text">{props.item.text}</div>
<div className="tab-item-actions">
<FontAwesomeIcon icon={faTrash} onClick={()=>props.delete(props.item.id)}/>
</div>
<div className="tab-item-details">
<div className="tab-item-details-tags">
{
props.item.tags.map(el=><div className="tab-item-details-tags-tag">{el}</div>)
}
</div>
</div>
<div className="tab-item-date">{props.item.created}</div>
</div>
)
}
The above answer is almoost correct. I am adding more info to the same concepts.
useEffect running twice:
This is most common ask in recent times. It's because the effect runs twice only in development mode & this behavior is introduced in React 18.0 & above.
The objective is to let the developer see & warn of any bugs that may appear due to a lack of cleanup code when a component unmounts. React is basically trying to show you the complete component mounting-unmounting cycle. Note that this behavior is not applicable in the production environment.
Please check https://beta-reactjs-org-git-effects-fbopensource.vercel.app/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed for a detailed explanation.
App crashes on second time: It's probably because you are trying to update the input value from event.target.value if you want to have control over the input value, your input should be a controlled component meaning, your react code should handle the onChange of input and store it in a state and pass that state as value to the input element & in your onKeyDown handler, reset the value state. That should fix the crash.
export default function InputBar(props){
const [inputVal, setInputVal] = useState("");
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(e.target.value), ...oldData.items]
}
}
)
setInputVal("")
}
}
return(
<div className="main-input-div">
<input
type="text"
value={inputVal}
onChange={(e) => {setInputVal(e.target.value)}}
onKeyDown={(e) => handleKeyDown(e)}
/>
</div>
)
}
Hope this helps. Cheers!
Your app is using strict mode, which in a development mode renders components twice to help detect bugs (https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects).
root.render(
<StrictMode>
<App />
</StrictMode>
);
As for the crash, I think it's happening due to props.setNewList being an asynchronous call and the resetting of e.target.value - something like this seemed to fix it for me:
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
const inputVal = e.target.value;
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(inputVal), ...oldData.items]
}
}
)
e.target.value=""
}
}
I will add, that using document.querySelector to get values isn't typical usage of react, and you might want to look into linking the input's value to a react useState hook.
https://reactjs.org/docs/forms.html#controlled-components

My drop down doesn't work when I click on the second time on ReactJS

I need my drop-down button to show the text below if I press one, and another one for hiding. I tried to use the useState method, but it also doesn't work, and same with the non-useState method. Here is my code, hope someone can fix this, thanks
function AnnouncementCard(props) {
const { announcement } = props;
const triangleDown = <img src={DropDown} alt="No-img" />;
const triangleUp = <img src={DropDownActive} alt="No-img" />;
function Readmore() {
console.log("inject", currentTriangle);
if (currentTriangle == triangleDown) {
DisplayText(null);
ChangeTriangle(triangleUp);
console.log(2, currentTriangle);
} else if (currentTriangle == triangleUp) {
DisplayText(<Text>{announcement.detail}</Text>);
ChangeTriangle(triangleDown);
console.log(1, currentTriangle);
}
}
const [currentTriangle, ChangeTriangle] = useState(triangleUp);
const [currentText, DisplayText] = useState(null);
return (
<Card>
<CardBody>
<PictureBox>
<Picture src={announcement.img} alt="no-img" />
</PictureBox>
<TextBox>
<Title>{announcement.title}</Title>
<Date>{announcement.date}</Date>
<ReadMore onClick={() => {Readmore();}}>
Read more
<Triangle>{currentTriangle}</Triangle>
</ReadMore>
</TextBox>
</CardBody>
<Textarea>{currentText}</Textarea>
</Card>
);
}
export default AnnouncementCard;
You cannot simply compare JSX elements using == operator (currentTriangle == triangleDown). To make it simple, I would use a boolean instead of saving the element in the state, and depending on that you can show up and down buttons.
Replace the ChangeTriangle state hook with the following.
const [toggleValue, setToggleValue] = useState(true);
Update the ReadMore function as below.
function Readmore() {
if (toggleValue === false) {
DisplayText(null);
setToggleValue(true);
} else if (toggleValue === true) {
DisplayText(<Text>{announcement.detail}</Text>);
setToggleValue(false);
}
}
Set the triangle button as below using a ternary operator.
<Triangle>{toggleValue ? triangleUp : triangleDown}</Triangle>

Converting Material-UI to a class in react

I am trying to use the 'Stepper' react material-ui component, but I am having difficulty using it in a class fashion, rather than function as they have in their previews.
Here is what I have so far, and it does load but with some problems:
The text that appears is 'unknown step' meaning that the function 'getStepContent' does not gets called properly
Every time I am hitting the 'next' button, it gives me an error saying: 'Cannot read property 'has' of undefined' seems like almost all of my function calls are messed up..
Here is my code:
import React, { Component } from "react";
import "./CharacterCreate.css";
import PropTypes from 'prop-types';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import Typography from '#material-ui/core/Typography';
import Box from '#material-ui/core/Box';
import { makeStyles } from '#material-ui/core/styles';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step';
import StepLabel from '#material-ui/core/StepLabel';
import Button from '#material-ui/core/Button';
export default class CharacterCreate extends Component {
constructor(props) {
super(props);
this.state = {
activeStep: 0,
skipped :new Set()
};
this.handleNext = this.handleNext.bind(this);
this.isStepSkipped = this.isStepSkipped.bind(this);
}
getSteps() {
return ['Select campaign settings', 'Create an ad group', 'Create an ad'];
}
getStepContent(step) {
switch (step) {
case 0:
return 'Select campaign settings...';
case 1:
return 'What is an ad group anyways?';
case 2:
return 'This is the bit I really care about!';
default:
return 'Unknown step';
}
}
isStepOptional(step) {
return step === 1;
}
isStepSkipped(step) {
return this.state.skipped.has(step);
}
handleNext() {
let newSkipped = this.skipped;
if (this.isStepSkipped(this.activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(this.activeStep);
}
this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setState({skipped: this.skipped});
}
handleBack() {
this.setState({activeStep: prevActiveStep => prevActiveStep - 1})
}
handleSkip() {
if (!this.isStepOptional(this.activeStep)) {
// You probably want to guard against something like this,
// it should never occur unless someone's actively trying to break something.
throw new Error("You can't skip a step that isn't optional.");
}
this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setSkipped(prevSkipped => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(this.activeStep);
return newSkipped;
});
}
handleReset() {
this.setState({activeStep: 0})
}
render() {
const steps = this.getSteps();
return (
<div className="root">
<Stepper activeStep={this.activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (this.isStepOptional(index)) {
labelProps.optional = <Typography variant="caption">Optional</Typography>;
}
if (this.isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
<div>
{this.activeStep === steps.length ? (
<div>
<Typography className="instructions">
All steps completed - you&apos;re finished
</Typography>
<Button onClick={this.handleReset} className="button">
Reset
</Button>
</div>
) : (
<div>
<Typography className="instructions">{this.getStepContent(this.activeStep)}</Typography>
<div>
<Button disabled={this.activeStep === 0} onClick={this.handleBack} className="button">
Back
</Button>
{this.isStepOptional(this.activeStep) && (
<Button
variant="contained"
color="primary"
onClick={this.handleSkip}
className="button"
>
Skip
</Button>
)}
<Button
variant="contained"
color="primary"
onClick={this.handleNext}
className="button"
>
{this.activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}
}
I know it's a lot, but I'm simply trying to use the same example code from material-ui website as a class instead of a function..
Thank you for your help!
I think you're wiping out this.state.skipped here, since this.skipped doesn't appear to be declared anywhere.
this.setState({skipped: this.skipped});
After this call, this.state.skipped is undefined, so calling this.state.skipped.has(...) blows up.
I suspect you meant to use this.state.skipped.
Another source of trouble might be a scope issue that arises from the way your click handlers are declared and attached, e.g. onClick={this.handleNext}.
TLDR: Try onClick={() => this.handleNext()} instead.
In javascript the scope (what this refers to) inside a method call is generally set to the object on which it was called.
So if you call this.handleNext(), references to this inside handleNext will be your component, as you expect.
However, if instead you do:
const {handleNext} = this;
handleNext();
The this reference may not be what you expect, because the method wasn't invoked as a method on your component. It was invoked as a standalone function, detached from your component. And this is effectively what happens when you pass an event handler down to another component. Inside the child component (the button, for example), your handler is just a function passed in as a prop, detached from your component:
// inside the button component
const {onClick} = this.props;
onClick(); // no scope; detached from your component
There are several ways to fix this, but the two most straightforward are:
Declare a new function that invokes the handler on the component:
onClick={ () => this.handleNext() }
Make your handler an arrow function, because arrow functions automatically adopt the parent scope where they're declared. So instead of this:
handleNext() {
let newSkipped = this.skipped;
...
Do this:
handleNext = () => {
let newSkipped = this.skipped;
Hope this helps. Sorry it's so long. Give it a shot and let me know.
Side note: you can do both of these in a single call:
this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setState({skipped: this.skipped});
this.setState({
activeStep: prevActiveStep => prevActiveStep + 1,
skipped: this.state.skipped
})

React: useState filter array not updating state

Edit:
My error occured because I passed an array as a second parameter to useEffect. Even though the values inside the array stayed the same, the reference changed constantly, therefore useEffect was called constantly and reset my checkbox values. That array was created by an useState call. I replaced useState by useReducer (reducer only changes the object reference if the object is actually changed) and updated some missing dependencies higher up the component tree.
Original question:
I have trouble updating a state in a functional component.
My question is somewhat similiar to this one:
React SetState doesn't call render
I'm already copying my state object (by using array.filter) instead of referencing it; but my state still doesn't update.
In order to track down the problem, I tried re-creating the problem in a minimal example:
jsfiddle
But in my minimal example, everything works as expected. I'm unable to reproduce the error.
Here is my example where the state doesn't update:
configCheckboxGroup.tsx:
import classNames from "classnames";
import React, { useState, useEffect } from "react";
import { Component } from "../../model";
import CheckboxPanel from "./panels/checkboxPanel";
interface configCheckboxGroupProps {
className?: string;
choices: Array<Component>;
selected: Array<string>;
addToCart: (items: Array<Component>) => void;
}
const ConfigCheckboxGroup: React.SFC<configCheckboxGroupProps> = ({
className,
choices,
selected,
addToCart,
}) => {
const [ selectedComp, setSelectedComp ] = useState<Array<string>>(selected);
// device loads later, selected has to be updated
useEffect(() => {
setSelectedComp(selected);
}, [selected]);
const handleOnChange = (ev: React.FormEvent, id: string) => {
console.debug(id);
console.debug(selectedComp.filter(el => el !== id));
if (selectedComp.includes(id)) {
// was already checked || this line is not working!
setSelectedComp(selectedComp.filter(el => el !== id));
} else {
// was not checked
setSelectedComp([...(selectedComp), id]);
}
const selected = choices.filter(el => selectedComp.includes(el.reference._id));
addToCart(selected);
};
return (
<div className={classNames("panellist", className)}>
{
choices.map(el => {
return (
<CheckboxPanel
image={ el.reference.picture ? el.reference.picture : undefined }
name={ el.reference.name }
id={ el.reference._id }
price={ el.reference.price ? el.reference.price :
el.price ? el.price : 0 }
key={ el._id }
checked={ selectedComp.includes(el.reference._id) }
onChange={ handleOnChange }
/>
)
})
}
<span>
{ selectedComp }
</span>
</div>
)
}
export default ConfigCheckboxGroup;
And checkboxPanel.tsx:
import classNames from "classnames";
import React from "react";
import "./checkboxPanel.scss";
import noImage from "../../../resources/images/errors/no-image.svg";
interface PanelProps {
className?: string;
image?: string;
name: string;
id: string;
price: number;
checked: boolean;
onChange: (ev: React.FormEvent, id: string) => void;
}
const CheckboxPanel: React.SFC<PanelProps> = ({
className,
image,
name,
id,
price,
checked,
onChange,
}) => {
const getImage = () => {
if (image) {
return image;
} else {
return noImage;
}
}
return (
<div className={classNames("panel", "checkbox-panel", className)}>
<div className="top">
<div className="image">
<img alt="Product" src={getImage()} />
</div>
<div className="name">
{name}
</div>
</div>
<div className="bottom">
<div className="category">
{ Number(price).toFixed(2) } €
</div>
<div>
<input type="checkbox"
checked={ checked }
onChange={ (e) => onChange(e, id) }
/>
</div>
</div>
</div>
)
};
export default CheckboxPanel;
The only difference between the examples is that in the second one, I call the handle function inside a child component. But I do the same thing on other occasions as well: I have a very similar Component configRadioGroup with radio buttons instead of checkboxes where everything works fine.
I tried playing around by manually filtering the array and trying a lot of other things, but nothing seemed to help. This is why, as a last try, I ask here (although I know that this question is not a good one due to it being very specific).
Changing the prop selected will reset selectedComp if you put a console log in your useEffect you may find that that is resetting it every time.
You need to track down where selected comes from (redux?) and how it's set (addToCart?).
A dirty fix could be to only set selectedComp when component mounts, this is dirty and will/should cause react-hooks/exhaustive-deps lint to trigger:
useEffect(() => {
setSelectedComp(selected);
}, []);
But better to track down what's going wrong with selected, if it comes from redux then maybe just use selected instead and forget about selectedComp since that is just a copy.

Flow - missing type annotation

Pretty new to flow and trying to fix my code to include flow. This is my code at the moment and I've added flow type check and now getting errors so I need to annotate my code properly:
// #flow
import React, { Component } from 'react';
import { Manager, Reference, Popper } from 'react-popper';
import cx from 'classnames';
import css from './Tooltip.css';
import animationsCss from './TooltipAnimations.css';
type Props = {
theme: string,
eventsEnabled: boolean,
}
class Tooltip extends Component<Props> {
static defaultProps = {
theme: 'light',
eventsEnabled: true
};
firstOrderPlacement(placement) {
if (!placement) return null;
return placement.split('-')[0];
}
arrowDirectionClass(firstOrderPlacement) {
switch (firstOrderPlacement) {
case 'right':
return css.arrowLeft;
case 'left':
return css.arrowRight;
case 'top':
return css.arrowDown;
case 'bottom':
return css.arrowUp;
default:
return css.arrowUp;
}
}
render() {
const { placement, className, children, fadeIn, theme, eventsEnabled } = this.props;
return (
<Popper placement={placement} eventsEnabled={eventsEnabled}>
{({ ref, style, placement }) => {
const firstOrderPlacement = this.firstOrderPlacement(placement);
const arrowDirectionClass = this.arrowDirectionClass(firstOrderPlacement);
const subContainerStyle = {
display: 'flex',
flexDirection:
firstOrderPlacement === 'top' || firstOrderPlacement === 'bottom' ? 'column' : 'row',
};
const childrenContainerClassName = cx(
css.childrenContainer,
css.wrapper,
theme === "dark" ? css.dark : css.light
);
const content = <div className={childrenContainerClassName}>{children}</div>;
const subContainerClassName = fadeIn ? cx(animationsCss.fadeIn, className) : className;
return (
<div
ref={ref}
className={cx(css.container, css.mobileTooltip)}
style={style}
data-placement={placement}
>
<div className={subContainerClassName} style={subContainerStyle}>
{(firstOrderPlacement === 'left' || firstOrderPlacement === 'top') && content}
<div>
<div className={cx(css.arrow, arrowDirectionClass)} />
</div>
{(firstOrderPlacement === 'right' || firstOrderPlacement === 'bottom') && content}
</div>
</div>
);
}}
</Popper>
);
}
}
export { Manager as TooltipManager, Reference as TooltipReference, Tooltip };
I believe I need to add these to my props. Are these correct?
placement: string,
className?: string,
children?: any,
fadeIn: any,
I'm missing type annotation for these two which I'm not sure how to proceed:
firstOrderPlacement(placement) {..}
arrowDirectionClass(firstOrderPlacement) {..}
Any help?
Annotate Props like:
type Props = {
...
placement: string,
className?: string,
children?: any,
fadeIn: any,
...
}
Placement parameter is string firstOrderPlacement(placement) {..} and return value of function is null or string, so you can use maybe type for annotation:
firstOrderPlacement(placement: string): ?string => {...}
Or with union type because maybe type covers undefined.
type FirstOrder = string | null;
Result of firstOrderPlacement function is parameter of arrowDirectionClass. So type of parameter:
arrowDirectionClass(firstOrderPlacement: ?string): string => {...}
Or:
arrowDirectionClass(firstOrderPlacement: FirstOrder): string => {...}
Are these correct?
Try them and find out!
Keep in mind, though, that using any is a shortcut that basically just turns off typing. For children where you are expecting react elements you should usually just use React.node. This will work if you basically want arbitrary nested DOM-like content like react elements, arrays of elements, strings of text, etc. fadeIn looks like it's probably a string, but it's kind of hard to tell without seeing where it comes from.
I'm missing type annotation for these two which I'm not sure how to proceed:
Flow wants you to type the arguments to the functions. Something like:
firstOrderPlacement(placement: string) {
or whatever.

Resources