I'm trying to render a closable tab bar using some Material UI components, and I'm having trouble implementing the onDelete method for when the user wants to close a tab. I'm passing the data set, an array of objects, as a prop called dataSet. I want to update it whenever the user closes a tab but it doesn't re-render; all tabs still appear. When I console.log this.state.dataSet on each click however, I see that the tabs are getting deleted. What am I doing wrong?
class ClosableTabs extends Component {
state = {
tabIndex: 0,
dataSet: this.props.dataSet,
};
onDelete = id => {
this.setState(prevState => {
const updatedDataSet = prevState.dataSet.filter(tab => tab.id !== id);
return {
dataSet: updatedDataSet,
};
}, console.log(this.state.dataSet);
};
renderTabs = dataSet => {
return dataSet.map(data => {
return (
<Tab
key={data.id}
label={
<span>
{data.title}
</span>
<Button
icon="close"
onClick={() => this.onDelete(data.id)}
/>
}
/>
);
});
};
render() {
const { value, dataSet, ...rest } = this.props;
return (
<TabBar value={this.state.tabIndex} onChange={this.onChange} {...rest}>
{this.renderTabs(dataSet)}
</TabBar>
);
}
}
export default Tabs;
and here is my data set that I pass as props when I use <ClosableTabs />
const dataSet = [
{
id: 1,
title: 'title 1',
},
{
id: 2,
title: 'title 2',
},
{
id: 3,
title: 'title 3',
},
];
When you render dataSet, you use the array you get from props (which never changes):
render() {
const { value, dataSet, ...rest } = this.props; // dataSet comes from props
return (
<TabBar value={this.state.tabIndex} onChange={this.onChange} {...rest}>
{this.renderTabs(dataSet)} // renderTabs renders this.props.dataSet
</TabBar>
);
}
}
instead, render dataSet which comes from your state (you should use different naming for this.props.dataSet and this.state.dataSet to avoid this kind of mistakes):
render() {
const { value, ...rest } = this.props;
const { dataSet } = this.state; // dataSet now comes from state
return (
<TabBar value={this.state.tabIndex} onChange={this.onChange} {...rest}>
{this.renderTabs(dataSet)} // renderTabs renders this.state.dataSet
</TabBar>
);
}
}
The problem is you are rendering the component with props instead of state.
Your render function should look likes this:
render() {
const { value, dataSet, ...rest } = this.props;
return (
<TabBar value={this.state.tabIndex} onChange={this.onChange} {...rest}>
{this.renderTabs(this.state.dataSet)}
</TabBar>
);
}
}
Related
I set button onClick as a parameter in the child component and drag and use onClick in the parent component.
The child component Room .
type Props = {
items?: [];
onClick?: any;
}
const Room = ({ onClick, items: [] }: Props) => {
return (
<div>
{items.length ? (
<>
{items.map((item: any, index: number) => {
return (
<>
<button key={index} onClick={() => { console.log('hi'); onClick }}>{item.name}</button>
</>
)
}
</>
)
</div>
)
}
This is the parent component.
const LoadingRoom = () => {
const handleWaitingRoomMove = (e: any) => {
e.preventDefault();
console.log('Hello Move')
}
return (
<>
<Room
items={[
{
name: "Waiting Room",
onClick: {handleWaitingRoomMove}
},
{
name: "Host Room",
},
]}
>
</Room>
</>
)
}
I want to call parent component's onClick handleWaitingRoomMove but it's not getting called.
However, console.log('hi') on the button is called normally whenever the button is clicked. Only button is not called. Do you know why?
onlick is a attribute to the child element. so move it outside of the items array
<Room
onClick={handleWaitingRoomMove}
items={[
{
name: "Waiting Room",
},
{
name: "Host Room",
},
]}
>
In the child, missing () for onClick
onClick={(ev) => { console.log('hi'); onClick(ev) }}
Demo
You are passing onClick in items, not direct props
<button key={index} onClick={item.onClick}>{item.name}</button>
so your component will be
type Props = {
items?: [];
}
const Room = ({ items: [] }: Props) => {
return (
<div>
{items.length ? (
<>
{items.map((item: any, index: number) => {
return (
<>
<button key={index} onClick={item.onClick}>{item.name}</button>
</>
)
}
</>
)
</div>
)
}
It would probably be more advantageous to have one handler that does the work, and use that to identify each room by type (using a data attribute to identify the rooms). That way you keep your data and your component logic separate from each other. If you need to add in other functions at a later stage you can.
const { useState } = React;
function Example({ data }) {
// Handle the room type by checking the value
// of the `type` attribute
function handleClick(e) {
const { type } = e.target.dataset;
switch (type) {
case 'waiting': console.log('Waiting room'); break;
case 'host': console.log('Host Room'); break;
case 'guest': console.log('Guest Room'); break;
default: console.log('Unknown room'); break;
}
}
// Keep the room data and the handler separate
return (
<div>
{data.map(obj => {
return (
<Room
key={obj.id}
data={obj}
handleClick={handleClick}
/>
);
})}
</div>
);
}
// Apply a data attribute to the element
// you're clicking on, and just call the handler
// `onClick`
function Room({ data, handleClick }) {
const { type, name } = data;
return (
<button
data-type={type}
onClick={handleClick}
>{name}
</button>
);
}
const data = [
{ id: 1, type: 'waiting', name: 'Waiting Room' },
{ id: 2, type: 'host', name: 'Host Room' },
{ id: 3, type: 'guest', name: 'Guest Room' }
];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
button:not(:last-child) { margin-right: 0.25em; }
<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>
I'm having trouble updating my state using setState.
I have a parent component where I define my state, as well as a handleClick function which should update the state.
export default class Main extends PureComponent {
state = {
selectedName: '',
selectedTasks: [],
data: [
{
name: 'John',
tasks: ['vacuum', 'cut grass']
},
{
name: 'Jack',
tasks: ['cook breakfast', 'clean patio']
}
]
handleClick = e => {
console.log('Name: ', e.name);
console.log('Tasks', this.state.data.find(el => el.name === e.name).tasks)
const { selectedName, selectedTasks } = this.state;
this.setState({
selectedName: e.name
selectedTasks: this.state.data.find(el => el.name === e.name).tasks
});
console.log('stateAfter', this.state)
};
render() {
const { data } = this.state;
return (
<div>
<Child
data={data}
handleClick={this.handleClick.bind(this)}
/>
</div>
)
}
}
I have a child component that takes the handleClick as props.
export default class Child extends PureComponent {
constructor(props) {
super(props);
}
render() {
console.log('data', this.props.data);
return (
<ResponsiveContainer width="100%" height={500}>
<PieChart height={250}>
<Pie
data={this.props.data}
onClick={this.props.handleClick}
/>
</PieChart>
</ResponsiveContainer>
);
}
}
When I execute the handleClick function the first time, nothing gets updated. But when I execute it the second time, the state does get updated. What am I missing to make state get updated the first time?
this.setState is asynchronous. If you wish to console.log your state after setState then you'll have to do this via the callback.
this.setState({
selectedName: e.name
selectedTasks: this.state.data.find(el => el.name === e.name).tasks
},
() => {
console.log('stateAfter', this.state)
});
I am experimenting with React context api,
Please check someComponent function where I am passing click event (updateName function) then state.name value update from GlobalProvider function
after updated state.name it will reflect on browser but not getting updated value in console ( I have called console below the line of click function to get updated value below )
Why not getting updated value in that console, but it is getting inside render (on browser) ?
Example code
App function
<GlobalProvider>
<Router>
<ReactRouter />
</Router>
</GlobalProvider>
=== 2
class GlobalProvider extends React.Component {
state = {
name: "Batman"
};
render() {
return (
<globalContext.Provider
value={{
name: this.state.name,
clickme: () => { this.setState({ name: "Batman 2 " }) }
}}
>
{this.props.children}
</globalContext.Provider>
);
}
}
export default GlobalProvider;
=== 3
const SomeComponent = () => {
const globalValue = useContext(globalContext);
const updateName = ()=> {
globalValue.clickme();
console.log(globalValue.name ) //*** Here is my concern - not getting updated value here but , getting updated value in browser
}
return (
<div onClick={(e)=> updateName(e) }>
{globalValue.name}//*** In initial load display - Batman, after click it display Batman 2
</div>) }
React state isn't an observer like Vue or Angular states which means you can't get updated values exactly right after changing them.
If you want to get the updated value after changing them you can follow this solution:
class A extends Component {
state = {
name: "Test"
}
updateName = () => {
this.setState({name: "Test 2"}, () => {
console.log(this.state.name) // here, name has been updated and will return Test 2
})
}
}
So, you need to write a callback function for the clickme and call it as below:
class GlobalProvider extends React.Component {
state = {
name: "Batman"
};
render() {
return (
<globalContext.Provider
value={{
name: this.state.name,
clickme: (callback) => { this.setState({ name: "Batman 2 " }, () => callback(this.state.name)) }
}}
>
{this.props.children}
</globalContext.Provider>
);
}
}
export default GlobalProvider;
And for using:
const SomeComponent = () => {
const globalValue = useContext(globalContext);
const updateName = ()=> {
globalValue.clickme((name) => {
console.log(name) // Batman 2
});
}
return (
<div onClick={(e)=> updateName(e) }>
{globalValue.name}//*** In initial load display - Batman, after click it display Batman 2
</div>)
}
I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!
I am trying to setState to an event category for display inside of handleCategoryChange. The categories are rendered from the getCategories fetch point. I need to send a different value to the action fetch call in createEventHandler. The set state only happens once though and omits the second to send the first value of the state. Is there a work-around for this? or is this a limitation of react?
//... styles and imports
class NewEvent extends Component {
constructor(props) {
super(props);
this.state = {
event: {
category: ''
}
};
this.createEventHandler = this.createEventHandler.bind(this);
this.handleCategoryChange = this.handleCategoryChange.bind(this);
}
handleCategoryChange(evnt) {
this.setState({
event: {
...this.state.event,
category: evnt.target.value
}
});
}
componentWillMount() {
this.props.getCategories();
}
renderStepOne() {
const { event } = this.state;
const { categories } = this.props;
return (
<div style={styles.flexColumn}>
<Typography variant="title">Event</Typography>
<Select
value={event.category}
onChange={this.handleCategoryChange}
error={categoryError.length > 0}
>
{categories.map(category => (
<MenuItem key={category.id} value={category.name}>
{category.name}
</MenuItem>
))}
</Select>
</div>
);
}
createEventHandler() {
const { event } = this.state;
if (!error) {
let categoryId = this.props.categories.filter(e => {
if (e.name === event.category) {
return e;
}
});
categoryId = categoryId[0].id;
this.setState({
event: {
...event,
category: categoryId
}
});
this.props.createEvent(event, this.props.history);
}
}
render() {
const { step } = this.state;
const { isFetching, user, categories } = this.props;
return (
<ViewContainer title="New Event" isFetching={isFetching}>
<Paper style={styles.paper}>
<div style={styles.body}>{this.renderStepOne()}</div>
<MobileStepper
type="dots"
steps={0}
position="static"
nextButton={
<Button
variant="raised"
color="primary"
onClick={this.createEventHandler}
disabled={isFetching}
>
Submit
<KeyboardArrowRight />
</Button>
}
/>
</Paper>
</ViewContainer>
);
}
}
const mapStateToProps = state => ({
categories: state.events.categories
});
const mapDispatchToProps = dispatch => ({
createEvent: (event, history) => dispatch(createEvent(event, history)),
getCategories: () => dispatch(getCategories())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(NewEvent));
You could try using functional setState like so:
this.setState(() => ({
event: {
...this.state.event,
category: evnt.target.value
})
});
So that everything involving a setting of state happens together.