I am trying to achive a shared counter between components for example
I have 3 buttons
<Button />
<Button />
<Button />
and each of them has a label that will show a number when its clicked on it
when I click one of them it will start by 1 and only the one i clicked will show that number 1 and others will be 0 or no label at all then if i click other button that button wil show 2 on it
and previous one will stay at 1 and so on when i click third button it will have state of 3
which is a shared state between 3 same component instance
I am trying to achive a gallery image selection by this i will show a label selected items and their order by numbers on it how to achive it with react hooks
Sharing logic between siblings can be handled via a parent component that passes callback functions to each child.
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
interface AppProps { }
interface AppState {
photos: PhotoData[];
}
type PhotoData = { id: number; name: string; }
type GalleryProps = { photos: PhotoData[] }
const Gallery : React.FunctionComponent<GalleryProps> = ({
photos
}) => {
// Create an array to store the unique ID of the selected "photos".
// As items are selected they will be inserted into the array in the order
// they were selected.
const [selected, setSelected] = React.useState<number[]>([]);
// Create a function to handle the selection of a button.
const onSelect = React.useCallback((selectedId?: number) => {
setSelected(currentSelected => {
// If item already in the array, remove it.
if (currentSelected.some(id => id === selectedId)) {
return currentSelected.filter(id => id !== selectedId);
}
// if item not in the array, add it.
return [...currentSelected, selectedId ]
});
}, [setSelected])
return (
<div>
{
!!photos && photos.map(photo => {
// find the selection order using index.
const index = selected.indexOf(photo.id);
// change index to 1 based (or undefined if not selected).
const selectionOrder = index >= 0 ? index + 1 : undefined
return (
<Photo
key={photo.id}
{...photo}
onClick={onSelect}
index={selectionOrder}
/>
);
})
}
</div>
);
}
type PhotoProps = PhotoData & { index?: number; onClick: (selectedId: number) => void; }
const Photo : React.FunctionComponent<PhotoProps> = ({
id,
name,
onClick,
index
}) => {
// Create function to handle button click.
const onButtonClick = React.useCallback(() => {
onClick(id)
}, [onClick, id])
return (
<button
onClick={onButtonClick}
>
{name} {!!index && `(Selected ${index})`}
</button>
);
};
class App extends Component<AppProps, AppState> {
constructor(props) {
super(props);
this.state = {
photos: [
{
id: 1,
name: 'Photo 1'
},
{
id: 2,
name: 'Photo 2'
},
{
id: 3,
name: 'Photo 3'
}
]
};
}
render() {
return (
<div>
<Gallery photos={this.state.photos} />
</div>
);
}
}
render(<App />, document.getElementById('root'));
Runnable Version of the above.
Related
So I have this component called counters where the state looks something like this
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 3 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
And I'm trying to increment the value every time a corresponding button is clicked. This is the function I wrote for it.
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
It doesn't work. And here are some additional observations.
When I console.log the local counters object (copied from state.counters) it returns an additional row at the end with id: -1 and value: NaN
The variable counter (thats being passed as a parameter) is from a child component. It's supposed to return 0 if the first button is clicked, 1 if the second button is clicked and so on. When I console.log it it seems to be returning the correct values.
by the looks of it, the problem seems to lie in the line
const index = counters.indexOf(counter);
As the value of index is always returned as -1.
You can use the index for updating the corresponding record in the array.
const idx = this.state.counters.findIndex((counter) => counter.id === id);
Complete implementation:-
import React from "react";
import "./styles.css";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 3 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
}
handleClick = (id) => {
const idx = this.state.counters.findIndex((counter) => counter.id === id);
const counters = [...this.state.counters];
counters[idx] = { ...counters[idx], value: counters[idx].value++ };
this.setState(counters);
};
render() {
return (
<div>
{this.state.counters.map((counter) => (
<div>
<button onClick={() => this.handleClick(counter.id)}>
Button {counter.id}
</button>
<span>Value {counter.value}</span>
<hr />
</div>
))}
</div>
);
}
}
Codesandbox - https://codesandbox.io/s/musing-black-cippbs?file=/src/App.js
try this for get the index:
const index = counters.findIndex(x => x.id === counter.id);
Adding my answer here just in case another confused soul stumbles upon it.
Although it seemed that the problem lied within line
const index = counters.indexOf(counter);
It actually was in the child component where the function was being invoked. Within the parameter I was passing counters.value whereas the handleincrement function within the parent component was expecting not the value, rather the complete object.
The code in its working condition is as below
Parent Component:
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 3 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
console.log(index);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
Child Component:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import React, { Component } from "react";
class Counter extends Component {
render() {
return (
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm"
>
Increment
</button>
)
}
]
I have a checkbox component, I want my user to be able to check multiple items, and then the items to be saved in the state as an array.
If I select a checkbox my handleChange function seems to set my array to undefined, I'm not sure if it's the way I am sending the data or If I've setup my checkbox wrong, I'm quite new to React.
My main component is
export default class MainForm extends Component {
state = {
eventFormats: []
}
handleChange = input => event => {
this.setState({[input]: event.target.value})
console.log(this.state)
}
render() {
const eventFormat = {eventFormats: this.state.eventFormats}
return <EventFormat
nextStep={this.nextStep}
handleChange={this.handleChange}
values={eventFormat}
}
}
}
My event form component
export default class EventFormat extends Component {
state = {
eventFormats: [
{id: 1, value: 1, label: "Virtual", isChecked: false},
{id: 2, value: 2, label: "Hybrid", isChecked: false},
{id: 3, value: 3, label: "Live", isChecked: false},
]
}
saveAndContinue = (e) => {
e.preventDefault()
}
render() {
return (
<Form>
<h1 className="ui centered">Form</h1>
<Form.Field>
{
this.state.eventFormats.map((format) => {
return (<CheckBox handleChange={this.props.handleChange} {...format} />)
})
}
</Form.Field>
<Button onClick={this.saveAndContinue}>Next</Button>
</Form>
)
}
}
And finally my checkbox component
const CheckBox = (props) => {
return (<Checkbox label={props.label} onChange={props.handleChange('eventFormats')}/>)
}
export default CheckBox
The error is in your handleChange function, which sets state to a dictionary while you said you want the checkbox's value to be added to the eventFormats array in the state.
export default class MainForm extends Component {
state = {
eventFormats: []
}
handleChange = input => event => {
if (event.target.checked) {
this.setState({eventFormats: this.state.eventFormats.concat([event.target.value])});
} else {
const index = this.state.indexOf(event.target.value);
if (index === -1) {
console.error("checkbox was unchecked but had not been registered as checked before");
} else {
this.setState({eventFormats: this.state.eventFormats.splice(index, 1);
}
}
console.log(this.state)
}
render() {
const eventFormat = {eventFormats: this.state.eventFormats}
return <EventFormat
nextStep={this.nextStep}
handleChange={this.handleChange}
values={eventFormat}
}
}
}
There are a few things to fix:
this.setState({[input]: event.target.value})
this will always overwrite the array(eventFormats) with event.target.value.
<CheckBox handleChange={this.props.handleChange} {...format} />
in the above line, you're passing all the properties in each format object
const CheckBox = (props) => {
return (<Checkbox label={props.label} onChange={props.handleChange('eventFormats')}/>)
}
but here you're only using label and handleChange.
Here's a React StackBlitz that implements what you're looking for. I used <input type="checkbox" />, you can replace this with the Checkbox component you want. See the console logs to know how the state looks after toggling any of the checkboxes.
Also, added some comments to help you understand the changes.
const Checkbox = ({ id, checked, label, handleChange }) => {
return (
<>
<input
type="checkbox"
id={id}
value={checked}
// passing the id from here to figure out the checkbox to update
onChange={e => handleChange(e, id)}
/>
<label htmlFor={id}>{label}</label>
</>
);
};
export default class App extends React.Component {
state = {
checkboxes: [
{ id: 1, checked: false, label: "a" },
{ id: 2, checked: false, label: "b" },
{ id: 3, checked: false, label: "c" }
]
};
handleChange = inputsType => (event, inputId) => {
const checked = event.target.checked;
// Functional update is recommended as the new state depends on the old state
this.setState(prevState => {
return {
[inputsType]: prevState[inputsType].map(iT => {
// if the ids match update the 'checked' prop
return inputId === iT.id ? { ...iT, checked } : iT;
})
};
});
};
render() {
console.log(this.state.checkboxes);
return (
<div>
{this.state.checkboxes.map(cb => (
<Checkbox
key={cb.id}
handleChange={this.handleChange("checkboxes")}
{...cb}
/>
))}
</div>
);
}
}
I have a list of buttons and I'm trying to toggle the classname when one is clicked. So that only when I click on a specific button is highlighted. I have a TagList component that looks like this:
const Tags = ({tags, onTagClick}) => {
return (
<div className="tags-container">
{ tags.map(tag => {
return (
<span
key={tag.name}
className="tag"
onClick={() => onTagClick(tag)}
>
{tag.name} | {tag.numberOfCourses}
</span>
)
})
}
</div>
)
}
And this is found in the parent component:
onTagClick = (tag) => {
this.filterCourses(tag)
}
render() {
const { tags, courses } = this.state
return (
<div>
<h1> Course Catalog Component</h1>
<Tags tags={tags} onTagClick={this.onTagClick} />
<Courses courses={courses} />
</div>
)
}
I know how I could toggle the class for a single button but I'm a little confused when it comes to a list of buttons. How can I toggle one specifically from a list of buttons? Am I going to need a seperate Tag component and add state to that one component?
EDIT:
This is what my state currently looks like:
constructor(props) {
super(props)
this.state = {
tags: this.sortedTags(),
courses: courses
}
}
And this is what filterCourses looks like:
filterCourses = (tag) => {
this.setState({
courses: courses.filter(course => course.tags.includes(tag.name))
})
}
To start, you would want to give each tag object you're working with a selected property. That will make it easier for you to toggle the class. During the rendering of that markup.
Here is the working sandbox: https://codesandbox.io/s/stupefied-cartwright-6zpxk
Tags.js
import React from "react";
const Tags = ({ tags, onTagClick }) => {
return (
<div className="tags-container">
{tags.map(tag => {
return (
<div
key={tag.name}
className={tag.selected ? "tag selected" : "tag"}
onClick={() => onTagClick(tag)}
>
{tag.name} | {tag.numberOfCourses}
</div>
);
})}
</div>
);
};
export default Tags;
Then in the Parent component, we simply toggle the selected prop (True/False) when the tag is clicked. That will update the tags-array and it gets passed back down to the Child-component which now has the new selected values.
Parent Component
import React from "react";
import ReactDOM from "react-dom";
import Tags from "./Tags";
import Courses from "./Courses";
import "./styles.css";
class App extends React.Component {
state = {
tags: [
{ id: 1, name: "math", numberOfCourses: 2, selected: false },
{ id: 2, name: "english", numberOfCourses: 2, selected: false },
{ id: 3, name: "engineering", numberOfCourses: 2, selected: false }
],
courses: [
{ name: "Math1a", tag: "math" },
{ name: "Math2a", tag: "math" },
{ name: "English100", tag: "english" },
{ name: "English200", tag: "english" },
{ name: "Engineering101", tag: "engineering" }
],
sortedCourses: []
};
onTagClick = tag => {
const tagsClone = JSON.parse(JSON.stringify(this.state.tags));
let foundIndex = tagsClone.findIndex(tagClone => tagClone.id == tag.id);
tagsClone[foundIndex].selected = !tagsClone[foundIndex].selected;
this.setState(
{
tags: tagsClone
},
() => this.filterCourses()
);
};
filterCourses = () => {
const { tags, courses } = this.state;
const selectedTags = tags.filter(tag => tag.selected).map(tag => tag.name);
const resortedCourses = courses.filter(course => {
return selectedTags.includes(course.tag);
});
this.setState({
sortedCourses: resortedCourses
});
};
render() {
const { tags, sortedCourses, courses } = this.state;
return (
<div>
<h1> Course Catalog Component</h1>
<Tags tags={tags} onTagClick={this.onTagClick} />
<Courses courses={!sortedCourses.length ? courses : sortedCourses} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I have a dynamic form as a functional component which is generated via a class based component. I want to make reset button which clears the input field values and sets the state to null array.
Full code is available here:
https://codesandbox.io/s/beautiful-archimedes-o1ygt
I want to make a reset button, clearing all the input values and initializing the Itemvalues array to null.
Even if I set the values to null, it doesn't clear the input field.
However, the problem I'm facing is that since, it is a dynamic form and a functional component it doesn't have a predefined state for each individual form field making it difficult to set value to null.
Can someone please help, I'm stuck on this from a long time
Here's a codesandbox to show you how to reset the items: https://codesandbox.io/s/romantic-heisenberg-93qi7
I also left a note for you on how to get this to work with your API data, see the comment inside onChangeText()
The problem is that the inputs are not controlled by state as you have deduced. We should create an updated object for each item from your API, giving it a value prop.
index.js
import React from "react";
import ReactDOM from "react-dom";
import Cart from "./Cart";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
Items: [],
itemvalues: [{}]
};
this.onChangeText = this.onChangeText.bind(this);
this.getItems = this.getItems.bind(this);
this.handleReset = this.handleReset.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.findFieldIndex = this.findFieldIndex.bind(this);
this.trimText = this.trimText.bind(this);
}
getItems = () => {
/*if the data is coming from an API, store it in an array then .map() over it.
we can add a value prop to the object like:
so you can do something like:
const newItems = [...apiData].map((item) => {
return {
...item,
value: ""
}
})
this.setState({
Items: newItems
})
*/
this.setState({
Items: [
{
name: "item1",
description: "item1",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item2",
description: "item2",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item3",
description: "item3",
group: "groupB",
dtype: "str",
value: ""
},
{
name: "item4",
description: "item4",
group: "groupB",
dtype: "str",
value: ""
}
]
});
};
onChangeText = e => {
const updatedItems = [...this.state.Items].map(item => {
if (item.name === e.target.name) {
return {
...item,
value: e.target.value
};
} else {
return item;
}
});
const updatedItemValues = [...updatedItems].reduce((obj, curr) => {
if (!obj[curr.group]) {
obj[curr.group] = [];
}
obj[curr.group] = [...obj[curr.group], { [curr.name]: curr.value }];
return obj;
}, {});
this.setState({
...this.state,
Items: updatedItems,
itemvalues: updatedItemValues
});
};
findFieldIndex = (array, name) => {
return array.findIndex(item => item[name] !== undefined);
};
trimText(str) {
return str.trim();
}
handleReset = () => {
const resetedItems = [...this.state.Items].map(item => {
return {
...item,
value: ""
};
});
this.setState(
{
...this.state,
Items: resetedItems,
itemvalues: []
},
() => console.log(this.state)
);
};
handleSubmit = () => {
console.log(this.state.itemvalues);
};
render() {
return (
<div>
{
<Cart
Items={this.state.Items}
getItems={this.getItems}
handleSubmit={this.handleSubmit}
handleReset={this.handleReset}
onChangeText={this.onChangeText}
/>
}
</div>
);
}
}
Cart.js
import React, { useEffect } from "react";
import Form from "./Form";
const Cart = props => {
useEffect(() => {
props.getItems(props.Items);
}, []);
return (
<div>
<Form Items={props.Items} onChangeText={props.onChangeText} />
<button onClick={props.handleSubmit}>Submit</button>
<button onClick={props.handleReset}>Reset</button>
</div>
);
};
export default Cart;
The Cart component can remain mostly the same, we do not need to pass in props.items to useEffect() dependency.
Form.js
import React from "react";
const Form = props => {
return (
<div>
{props.Items.map(item => {
return (
<input
name={item.name}
placeholder={item.description}
data-type={item.dtype}
data-group={item.group}
onChange={e => props.onChangeText(e)}
value={item.value}
/>
);
})}
</div>
);
};
export default Form;
Now in Form component, we provide each input a value prop that is connected to the item our upper-most parent component-state.
That's pretty much all you need to reset the values.
See if that works for you:
Working example on CodeSandbox
Since you were already using hooks in part of your code, I've converted your class into a functional component using hooks (my advice: learn hooks and forget about class components).
I've added a value property to your INITIAL_STATE so it will keep the input value for each inputItem.
Full CODE:
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import FormV2 from "./FormV2";
import "./styles.css";
function App() {
const INITIAL_STATE = [
{
name: "item1",
description: "item1",
group: "groupA",
dtype: "str",
value: "" // ADDED VALUE PROPERTY TO KEEP THE INPUT VALUE
},
{
name: "item2",
description: "item2",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item3",
description: "item3",
group: "groupB",
dtype: "str",
value: ""
},
{
name: "item4",
description: "item4",
group: "groupB",
dtype: "str",
value: ""
}
];
const [inputItems, setInputItems] = useState(INITIAL_STATE);
function handleChange(event, index) {
const newValue = event.target.value;
setInputItems(prevState => {
const aux = Array.from(prevState);
aux[index].value = newValue;
return aux;
});
}
function handleReset() {
console.log("Reseting Form to INITIAL_STATE ...");
setInputItems(INITIAL_STATE);
}
function handleSubmit() {
inputItems.forEach(item =>
console.log(
"I will submit input: " + item.name + ", which value is: " + item.value
)
);
}
return (
<FormV2
handleSubmit={handleSubmit}
handleReset={handleReset}
handleChange={handleChange}
inputItems={inputItems}
/>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
FormV2.js
import React from "react";
function FormV2(props) {
const formInputItems = props.inputItems.map((item, index) => (
<div key={item.name}>
{item.name + ": "}
<input
type="text"
data-type={item.dtype}
data-group={item.group}
placeholder={item.description}
value={item.value}
onChange={event => props.handleChange(event, index)}
/>
</div>
));
return (
<React.Fragment>
<form>{formInputItems}</form>
<button onClick={props.handleSubmit}>Submit</button>
<button onClick={props.handleReset}>Reset</button>
<div>State: {JSON.stringify(props.inputItems)}</div>
</React.Fragment>
);
}
export default FormV2;
In order to control the values of the child components (Items) which I presume are input fields you need to be passing down their values from their parent component. So each of your items will have an item.value which is stored in the parent component's state.
That means that in the parent component you will be able to define a method which clears all of the item values it is storing in its state.
That will probably look something like
resetInputs = () => {
this.setState({
inputFields: this.state.inputFields.map(inputField => {
...inputField,
value: ''
}
})
}
Also you'll need to write what kind of tag you want for your code to work, like input.
So what you'll end up with for the code of the child component you shared is something like:
const Form = (props) => {
return (
<div>
{props.Items.map(item => (
<input
name={item.name}
value={item.value}
placeholder={item.description}
onChange={e => props.onChangeText(e)}
/>
)
)}
</div>
);
}
export default Form
You want to manage the state of unknown number N of items, one way to achieve it is by managing a single object which contains all states, for example, setValuesManager manages N inputs and clicking the button reset its state:
function TextAreaManager() {
const [valuesManager, setValuesManager] = useState([...items]);
return (
<Flexbox>
{valuesManager.map((value, i) => (
<TextBoxItem
key={i}
value={value}
onChange={e => {
valuesManager[i] = e.target.value;
setValuesManager([...valuesManager]);
}}
/>
))}
<PinkButton
onClick={() =>
setValuesManager([...Array(valuesManager.length).fill('')])
}
>
Reset All
</PinkButton>
</Flexbox>
);
}
Demo:
I have some data in arrays. I am getting it by using map, as you see in the below example. Also, i pass that into the button. Now, if, i select a button, it will get selected. But, if i select the next button, the previous button will get unselected and the current button will get selected. I don't want it to happen. I want to select multi buttons, if it all get clicked.
Thanks in advance.
Below is the Solution
import React, { Component } from 'react';
const BUTTONS = [
{id:0, title:'button1'},
{id:1, title:'button2'},
{id:2, title:'button3'}
]
class Map extends Component {
constructor(props){
super(props);
this.state = {
values: []
}
}
handleButton = button => {
let tmp = this.state.values;
if (this.state.values.includes(button)) {
this.setState({
values: this.state.values.filter(el => el !== button)
})
} else {
tmp.push(button);
this.setState({
values: tmp
})
}
}
render () {
return (
<div>
{BUTTONS.map(bt=>(
<button
key={bt.id}
onClick={()=>this.handleButton(bt.id)}
className={this.state.values.includes(bt.id) ? "buttonPressed" : "button"}>
{bt.title}
</button>
))}
</div>
);
}
}
export default Map;
Selecting multiple buttons
you'd better use the state as an array.
this.state = {
values: []
}
and you can push items.
let tmp = this.state.values;
tmp.push(button);
this.setState({
values: tmp
});
in render() you have to check state.values has bt.id
className={this.state.values.includes(bt.id) ? "buttonPressed" : "button"
Toggling multiple buttons
you can check in handleButton() whether that selected button is already selected
handleButton = button => {
if (this.state.values.includes(button)) {
this.setState({
values: this.state.values.filter(el => el !== button)
})
}
const BUTTONS = [
{ id: 0, title: 'button1' },
{ id: 1, title: 'button2' },
{ id: 2, title: 'button3' }
]
class Map extends React.Component {
constructor(props) {
super(props);
this.state = {
values: []
}
}
handleButton = button => {
let tmp = this.state.values;
if (this.state.values.includes(button)) {
this.setState({
values: this.state.values.filter(el => el !== button)
})
} else {
tmp.push(button);
this.setState({
values: tmp
})
}
}
render() {
return (
<div>
{BUTTONS.map(bt => (
<button
key={bt.id}
onClick={() => this.handleButton(bt.id)}
className={this.state.values.includes(bt.id) ? "buttonPressed" : "button"}>
{bt.title}
</button>
))}
</div>
);
}
}
export default Map;
The reason your button is getting deselected is because you're overwriting this.state.value every time you click a button.
If you want multiple selections, you'll need to hold all of the selected items in the state, as an array, and then when rendering, check if the button id is included in that array.
Something like:
import React, { Component } from 'react';
const BUTTONS = [
{id:0, title:'button1'},
{id:1, title:'button2'},
{id:2, title:'button3'}
]
class Map extends Component {
constructor(props){
super(props);
this.state = {
selectedValues: []
}
}
handleButton = buttonId => {
let newSelectedValues = this.state.selectedValues;
newSelectedValues.push(buttonId);
this.setState({
selectedValues: newSelectedValues
})
}
render () {
return (
<div>
{BUTTONS.map(bt => (
<button
key={bt.id}
onClick={()=>this.handleButton(bt.id)}
className={this.state.selectedValues.includes(bt.id) ? "buttonPressed" : "button"}>
{bt.title}
</button>
))}
</div>
);
}
}
export default Map;
if you need multiple selections you need an array:
this.state = {
values:[]
}
and push it on each clicks
Correct way to push into state array