I am trying to implement a function that sets a property of the state, "changedMarkup" on a button click event.
Constructor
constructor() {
super();
this.state = {
value: 0,
changedMarkup: 0
};
}
Render
render() {
const { classes } = this.props;
return (
<Paper className={styles.root}>
<Tabs
value={this.state.value}
onChange={this.handleChange}
variant="fullWidth"
indicatorColor="primary"
textColor="primary"
aria-label="icon label tabs example"
>
<Tab onClick={() => this.changeMarkup(0)} icon={<TrendingUpIcon />} label="TRENDING" />
<Tab onClick={() => this.changeMarkup(1)} icon={<ScheduleIcon />} label="NEW" />
<Tab onClick={() => this.changeMarkup(2)} icon={<WhatshotIcon />} label="HOT" />
</Tabs>
</Paper>
);
}
changeMarkup function
changeMarkup = (markup) => {
this.setState({
changedMarkup: markup
})
console.log("markup", this.state.changedMarkup);
}
Expected behavior
Log statement when the first tab is clicked: markup 0
Log statement when the second tab is clicked: markup 1
Log statement when the third tab is clicked: markup 2
Resulting behaviour
The "changeMarkup" property produces unexpected values. I can't seem to find an exact common pattern but it seems to be increment from 0 to 2 and decrements back to 0 with continuous clicks irrespective of the tab clicked
Any help is appreciated.
See: https://reactjs.org/docs/react-component.html#setstate
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState is an async operation, it won't be complete at the time you do your console logging. You can supply a callback after it has been updated:
this.setState({changedMarkup: markup}, () => {
// Do your logging here!
});
Because this.setState({}) is async operation so if you want updated value log than you can do it in two ways.
1. use callback function like this
this.setState({
//set your state
}, () => {
console.log('your updated state value')
})
2. in render function like this
render(){
console.log('your state')
return()
}
Related
I am experimenting around with React to get used to its whole ecosystem. Here's the code that I am currently working on to test how forms work:
function TextInput({
name,
value = '',
required = false,
...props
}) {
console.log('RENDERING');
const [_value, setValue] = React.useState(value);
console.log('_value', _value);
function changeHandler(e) {
setValue(e.target.value);
console.log('change', e.target.value);
}
function blurHandler(e) {
setValue(e.target.value);
console.log('blur', e.target.value);
}
function focusHandler(e) {
setValue(e.target.value);
console.log('focus', e.target.value);
}
return <div className="form-group">
<input type="text" name={name} value={_value} onBlur={blurHandler} onChange={changeHandler} onFocus={focusHandler} {...props}></input>
<div className="form-error"></div>
</div>
}
function App() {
return <>
<h1>Simple forms / validation</h1>
<TextInput name="name" placeholder="simple"></TextInput>
</>
}
ReactDOM.render(<App />, document.getElementById('root'));
I have read that React doesn't rerender a given component upon a state change if the previous state value and the new state value are both the same. This makes perfect sense and so does the inspections that I do in the code.
However, I don't really understand as to why does React re-render the given component when the blur event occurs. Technically, when the blur event fires, the setValue(e.target.value) call makes the state change and since the previous state value is the same as this new one, there ideally shouldn't be any rerender.
But the console logs shows that there is a re-render. I want to know why does this happen, or is it some kind of a bug.
I am using Material-UI within my ReactJS app to create a table that, when clicked, expands to show more detailed info (a new row just beneath the clicked row). As example, here is a minimal toy example:
https://codesandbox.io/s/material-collapse-table-forked-t6thz
The code relevant to the problem is:
<Collapse
in={open}
timeout="auto"
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
}}
mountOnEnter
unmountOnExit
>
<div>
{/* actual function calls here; produces JSX output */}
{console.log("This should not execute before expanding!")}
Hello
</div>
</Collapse>;
Do note that the console.log() statement is just a simple replacement for my actualy functionality, which involves some API calls that are made when a row is clicked, and the corresponding info is displayed. So instead of console.log() I would actually call some other function.
I find that the console.log() statement executed on initial page render itself, even though in=false initially. How can I prevent this? Such that the function calls take place only when the Collapse is expanded. I initially thought this would be automatically handled by using mountOnEnter and unmountOnExit, but that does not seem to be the case. Any help would be appreciated, that could fix this problem in the sample example above.
I am working on an existing open source project, and therefore do not have the flexibility to restructure the existing codebase a lot. I would ideally have loved to implement this differently, but don't have that option. So posting here to know what options I might have given the above scenario. Thanks.
Problem
The children are rendered on initial load because they're defined within the Row component.
Solution
Move the Collapse children to its own React component. This won't render the children until the Collapse is opened. However, it'll re-render the child component when Collapse is closed. So depending on how you're making the API call and how other state interacts with this component, you may want to pass open to this component and use it as an useEffect dependency.
For example:
const Example = ({ open }) => {
React.useEffect(() => {
const fetchData = async () => {...};
if(open) fetchData();
}, [open]);
return (...);
}
Demo
Code
A separate React component:
const Example = ({ todoId }) => {
const [state, setState] = React.useState({
error: "",
data: {},
isLoading: true
});
const { data, error, isLoading } = state;
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`
);
if (res.status !== 200) throw String("Unable to locate todo item");
const data = await res.json();
setState({ error: "", data, isLoading: false });
} catch (error) {
setState({ error: error.toString(), data: {}, isLoading: false });
}
};
fetchData();
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
return (
<div
style={{
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : isLoading ? (
<p>Loading...</p>
) : (
<>
<div>
<strong>Id</strong>: {data.id}
</div>
<div>
<strong>Title</strong>: {data.title}
</div>
<div>
<strong>Completed</strong>: {data.completed.toString()}
</div>
</>
)}
</div>
);
};
The Example component being used as children to Collapse (also see supported Collapse props):
<Collapse
in={open}
timeout="auto"
// mountOnEnter <=== not a supported prop
// unmountOnExit <=== not a supported prop
>
<Example todoId={todoId + 1} />
</Collapse>
Other Thoughts
If the API data is static and/or doesn't change too often, I'd recommend using data as a dependency to useEffect (similar to the open example above). This will limit the need to constantly query the API for the same data every time the same row is expanded/collapsed.
Firstly, huge thanks to Matt for his detailed explanation. I worked through his example, and expanded on it to work for me as required. The main takeaway for me was: "Move the Collapse children to its own React component."
The solution posted by Matt above, I felt, didn't completely solve the problem for me. E.g. if I add a console.log() statement to the render() of the new child component (<Example>), I still see it being executed before it is mounted.
Adding mountOnEnter and unmountOnExit solved this problem:
But as Matt mentioned, the number of times the children were getting rendered was still a problem. So I slightly changed some bits (also simplified the code a bit):
Essentially, I do this now:
My child component is:
function Example(props) {
return (
<div
style={{
fontSize: 100,
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{props.flag && console.log("This should not execute before expanding!")}
{props.value}
</div>
);
}
and I call it from the parent component as:
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" mountOnEnter unmountOnExit>
<Example value={row.name} flag={open} />
</Collapse>
</TableCell>
</TableRow>
Note that the parameter flag is essential to avoid the function execution during closing of the <Collapse>.
I have a React component which displays a modal. Displays fine the first time. But when I close and reopen it "holds" the input value: The code for the render method is:
render() {
return (
<>
<Button
type="primary"
onClick={() => {
this.setState({
topup: 0,
modalVisible: true
});
}}
>
Replace
</Button>
<Modal
visible={this.state.modalVisible}
onOk={this.onOk}
onCancel={() => {
this.setState({
topup: 0,
modalVisible: false
});
}}
>
<Form>
<Row gutter={24}>
<Col span={24}>
<Descriptions>
<Descriptions.Item label="Topup">
<Input
type="number"
defaultValue={this.state.topup}
onChange={value => {
console.log('value.target.value is ', value.target.value);
if (Number(value.target.value)) {
this.setState({
...this.state,
topup: Number(value.target.value)
});
}
}}
/>
</Descriptions.Item>
</Descriptions>
</Col>
</Row>
</Form>
</Modal>
</>
);
}
When it first displays, the value in the topup Input is 0, which is correct. If I change to 10, then close the modal, I reset the state so the topup is 0. When I click the button, somehow, the Input still has 10. I can see in the state the topup property is 0. What could possible be going on?
It might be because that Input is uncontrolled, as in you only provide default value, so maybe try using value instead of defaultValue if you want value to always come from state.
This is irrelevant to your problem, but if you're using this.setState method of class component, you don't need to spread this.state, because whatever you pass into this.setState will be merged with current state. Also using old state to create new state like that can lead to bugs, so if you ever need to use old state to generate new one (for example, you want to increment state variable), you should pass a function into this.setState:
this.setState((prevState)=>{
return {
value: prevState.value + 1
};
});
That way React will handle your setState properly and guarantee prevState is actual current state at the moment setState is running.
I'm using gatsby and have a functional component that loops through some data to create radio button group with an onchange event and checked item. When i update the state whole page component rerenders. i though adding memo was meant to stop this but it doesn't seem to work.
here is the code
const BikePage = React.memo(({ data }) => {
console.log("page data", data)
const [selectedColor, setColor] = useState(data.bike.color[0])
const onColorChange = e => {
setColor(e.target.value)
}
return (
<div>
{data.treatment.price.map((value, index) => {
return (
<div>
<input
id={`bike-option-${index}`}
name="treatment"
type="radio"
value={value}
checked={selectedColor === value}
onChange={e => onColorChange(e)}
/>
<label
htmlFor={`treatment-option-${index}`}
>
{value}
</label>
</div>
)
})}
<Link
to="/book"
state={{
bike: `${data.bike.title}-${selectedColor}`,
}}
className="c-btn"
>
Book Now
</Link>
</div>
)
});
If you update the state the component will re-render, that's fundamentally how react works. the memoised data prop is coming from outside of the component.
"If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result" react.memo
You're not changing the incoming props though, you're changing the state
Side note: i imagine that on changing this value you probably want to be changing the state of the data on the server through some means also ( REST POST / graphql mutation). Subsequent refetches of this data would re-render this component as well. It depends what you're trying to ultimately achieve.
I have created the following component:
type ToggleButtonProps = { title: string, selected: boolean }
export default class ToggleButton extends Component<ToggleButtonProps>{
render(){
return (
<TouchableWithoutFeedback {...this.props}>
<View style={[style.button, this.props.selected ? style.buttonSelected : style.buttonDeselected]}>
<Text style={[style.buttonText, this.props.selected ? style.buttonTextSelected : style.buttonTextDeselected]}>{this.props.title}</Text>
</View>
</TouchableWithoutFeedback>
);
}
}
The styles are simple color definitions that would visually indicate whether a button is selected or not. From the parent component I call (item is my object):
item.props.selected = true;
I've put a breakpoint and I verify that it gets hit, item.props is indeed my item's props with a selected property, and it really changes from false to true.
However, nothing changes visually, neither do I get render() or componentDidUpdate called on the child.
What should I do to make the child render when its props change? (I am on React Native 0.59.3)
You can't update the child component by literally assigning to props like this:
item.props.selected = true;
However, there are many ways to re-render the child components. But I think the solution below would be the easiest one.
You want to have a container or smart component which will keep the states or data of each toggle buttons in one place. Because mostly likely, this component will potentially need to call an api to send or process that data.
If the number of toggle buttons is fixed you can simply have the state like so:
state = {
buttonOne: {
id: `buttonOneId`,
selected: false,
title: 'title1'
},
buttonTwo: {
id: `buttonTwoId`,
selected: false,
title: 'title2'
},
}
Then create a method in the parent which will be called by each child components action onPress:
onButtonPress = (buttonId) => {
this.setState({
[buttonId]: !this.state[buttonId].selected // toggles the value
}); // calls re-render of each child
}
pass the corresponding values to each child as their props in the render method:
render() {
return (
<View>
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonOne} />
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonTwo} />
...
finally each child can use the props:
...
<TouchableWithoutFeedback onPress={() => this.props.onPressFromParent(this.props.dataFromParent.id)}>
<View style={[style.button, this.props.dataFromParent.selected ? style.buttonSelected : style.buttonDeselected]}>
...
I left the title field intentionally for you to try and implement.
P.S: You should be able to follow the code as these are just JS or JSX.
I hope this helps :)
Because children do not rerender if the props of the parent change, but if its STATE changes :)
Update child to have attribute 'key' equal to "selected" (example based on reactjs tho')
Child {
render() {
return <div key={this.props.selected}></div>
}
}