React Events - askbid-notes/notes-react GitHub Wiki
- Intro
- State
setState()- Using Callbacks to Pass Information
- Accessing
eventData and Event Pooling - Updating State
- Nested State - Deep Merge
- Setting state is not synchronous and
previousState
React makes use of basic HTML events by wrapping them in something called SyntheticEvents.
This wrapper allows React to make sure events are handled the same way across all browsers by a consistent API.
class Tickler extends React.Component {
tickle = () => {
console.log('Tee hee!')
}
render() {
return (
<button onClick={this.tickle}>Tickle me!</button>
)
}
}The handler name is always comprised of on, and the event name itself (i.e. click). These are joined together and camel-cased. As you know, the value of the events are callbacks (either a reference to a function or an inline function).
You will commonly see React component methods defined with arrow functions. This is because we often want to access the this keyword within the methods themselves. By using an arrow function, we avoid creating a new scope with a different value of this.
component with multiple events:
render() {
return (
<canvas
onMouseMove={this.handleMouseMove}
onClick={() => {toggleCycling()}}
onKeyDown={this.handleKeyDown}
width='900'
height='600
tabIndex="0">
</canvas>
)
}Handling mouse coordinates with onMouseMove
handleMouseMove = (event) => {
drawChromeBoiAtCoords(event.clientX, event.clientY);
}props are dificult to change without recreating the component passing from the Parent giving the props again. For this reason for changing features in the componenet we use this.state. State is for values that are expected to change.
class MyComp extends React.Component {
// we use the constructor to set the INITIAL STATE
constructor() {
super()
this.state = {
count: 0
}
}
// our increment method makes use of the 'setState' method, which is what we use to alter state
increment = () => {
const newCount = this.state.count + 1
this.setState({
count: newCount
})
}
render() {
return (
<div onClick={this.increment}>
{this.state.count}
</div>
)
}
}We set initial state in the constructor because it runs first for React Component Lifecycle reasons. In ES7, it is possible to initialize state by simply doing the following inside of your component.
class App extends React.Component {
state = {
key: "value"
}
}as opposed of the most common:
class App extends React.Component {
constructor() {
super()
this.state = {
key: "value"
}
}
}Note: Bear in mind that we call super so that we can execute the constructor function that is inherited from React.Component while adding our own functionality.
It is possible to use the constructor to set an initial state that is dependent upon props like so:
constructor(props) {
super(props);
this.state = {
color: props.initialColor
};
}
//source: https://reactjs.org/docs/react-component.html#constructorNote that in contrast to the previous example, we take props as an argument to the constructor. This is because we are making use of the props to set an initial state - if we aren't using props to do this, then we need not include props as an argument to the constructor.
setState() sets state asynchronously.
class App extends Component {
constructor() {
super()
this.state = {
count: 0
}
}
increment = () => {
console.log(`before setState: ${this.state.count}`)
this.setState({
count: this.state.count + 1
})
console.log(`after setState: ${this.state.count}`)
}
render() {
return (
<div onClick={this.increment}>
{this.state.count}
</div>
)
}
}
// click
//=> before setState: 0
//=> after setState: 0)
// <div>1</div>"After setState:" should have given 1 instead returned 0. Because of asyncronicity.
While component state is a very powerful feature, it should be used as sparingly as possible.
State adds (sometimes unnecessary) complexity and can be very easy to lose track of. The more state we introduce in our application, the harder it will be to keep track of all of the changes in our data.
Remember: state is for values that are expected to change during the components life.
In React, props are used to pass information down the component tree, from parents to children. In order to propagate information in the opposite direction, we can use callback functions, also passed down as props from parent components to children. However, because these functions are defined in the parent, they will still be in that context if called from a child component.
This allows the callback to be owned by a different component than the one invoking it. Once invoked, the callback can effect change in the component that owns it, instead of the component that called it.
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import learnSymbol from './data.js'
import Matrix from './Matrix.js'
ReactDOM.render(<Matrix values={learnSymbol} />, document.getElementById('root'));
//Matrix.js
import React, { Component } from 'react';
import learnSymbol from './data.js'
import Cell from './Cell.js'
import ColorSelector from './ColorSelector.js'
export default class Matrix extends Component {
constructor() {
super()
this.state = {
selectedColor: '#FFF'
}
}
setSelectedColor = (newColor) => {
this.setState({
selectedColor: newColor
})
}
genRow = (vals) => (
vals.map((val, idx) => <Cell key={idx} color={val} selectedColor={this.state.selectedColor} />)
)
genMatrix = () => (
this.props.values.map((rowVals, idx) => <div key={idx} className="row">{this.genRow(rowVals)}</div>)
)
render() {
return (
<div id="app">
<ColorSelector setSelectedColor={this.setSelectedColor} />
<div id="matrix">
{this.genMatrix()}
</div>
</div>
)
}
}
Matrix.defaultProps = {
values: learnSymbol
}
//ColorSelector.js
import React, { Component } from 'react';
export default class ColorSelector extends Component {
makeColorSwatches = () => (
["#F00", "#F80", "#FF0", "#0F0", "#00F", "#508", "#90D", "#FFF", "#000"].map((str, idx) => {
let callback = () => this.props.setSelectedColor(str)
return <div onClick={callback} key={idx} className="color-swatch" style={{backgroundColor: str}}/>
})
)
render() {
return (
<div id="colorSelector">
{this.makeColorSwatches()}
</div>
)
}
}
//Cell.js
import React, { Component } from 'react';
export default class Cell extends Component {
constructor(props) {
super(props)
this.state = {
color: this.props.color
}
}
handleClick = () => {
this.setState({
color: this.props.selectedColor
})
}
render() {
return (
<div onClick={this.handleClick} className="cell" style={{backgroundColor: this.state.color}}>
</div>
)
}
}setSelectedColor is the function that gets passed to the child 1 so that even though launched in ColorSelector it will trigger in Matrix where it will set this.state.selectedColor that is passed as prop to child 2 (Cell) where a onClick event will change the Cell this.state.color to the this.props.selectedColor passed from Matrix.
not that different from what commonly happens:
export default class Clicker extends React.Component {
handleClick = (event) => {
console.log(event.type); // prints 'click'
}
render() {
return (
<button onClick={this.handleClick}>Click me!</button>
);
}
}One important thing to know though is event pooling.
If we click the button of our Clicker component and then inspect the logged out object in our console, we notice that all properties are null again. By the time we inspect the object in our browser, the event object will have already been returned to the pool. This means that we can't access event data in an asynchronous manner by saving it in the state, or running a timeout and then accessing the event again.
You usually don't need to access your event data in an asynchronous manner like described above, but if you do, there are two options: you either store the data you need in a variable (e.g. const target = event.target), or we can make the event persistent by calling that method: event.persist().
Event pooling means that whenever an event fires, its event data (an object) is sent to the callback. The object is then immediately cleaned up for later use. This is what we mean by 'pooling': the event object is in effect being sent back to the pool for use in a later event. It's something that trips up a lot of people, and you might have run into it yourself when inspecting SyntheticEvent in the browser.
take this code:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import DelayedButton from './DelayedButton';
ReactDOM.render(
<div>
<DelayedButton onDelayedClick={ event => console.log(event) } delay={1500} />
</div>,
document.getElementById('global')
);
import React, { Component } from 'react';
// DelayedButton.js
class DelayedButton extends Component {
callback = (event) => {
// here the event properties are NOT null,
// the event isn't yet returned to the pool.
setTimeout(() => {
// here the event properties are null,
// the event is returned to the pool.
this.props.onDelayedClick(event);
}, this.props.delay);
}
render() {
return (<button onClick={this.callback}>DelayedButton</button>)
}
}
export default DelayedButton;When onDelayedClick will log the event this will have null properties because the event is returned to the pool already.
That is why we need to use event.persist()
// DelayedButton.js
import React, { Component } from 'react';
class DelayedButton extends Component {
callback = (event) => {
// here the event properties are NOT null,
// the event isn't yet returned to the pool.
event.persist()
setTimeout(() => {
// here the event properties are NOT null,
// the event is NOT returned to the pool.
this.props.onDelayedClick(event);
}, this.props.delay);
}
render() {
return (<button onClick={this.callback}>DelayedButton</button>)
}
}
export default DelayedButton;Using DelayedButton component above the event properties will not be null.
State is only reserved for data that changes in our component and is visible in the UI.
Instead of directly modifying the state using this.state, we use this.setState(). This is a function available to all React components that use state, and allows us to let React know that the component state has changed. This way the component knows it should re-render, because its state has changed and its UI will most likely also change. Using a setter function like this is very performant. While other frameworks like Angular.js use "dirty checking" (continuously checking for changes in an object) to see if a property has changed, React already knows because we use a built-in function to let it know what changes we'd like to make!
To update our state, we use this.setState() and pass in an object. This object will get merged with the current state. When the state has been updated, our component re-renders automatically. Handy!
// src/components/ClickityClick.js
...
constructor() {
super();
// Define the initial state:
this.state = {
hasBeenClicked: false
};
}
...
...
handleClick = () => {
this.setState({
hasBeenClicked: true
})
}
...this.setState() is a deep merge which means that the merge will happen recursively, leaving any unchanged properties intact.
One super important thing to note is that it only merges things on the first level.
// with this state:
{
theme: 'blue',
addressInfo: {
street: null,
number: null,
city: null,
country: null
},
}
// if you do:
this.setState({
addressInfo: {
city: 'New York City'
}
});
//you will get:
{
theme: 'blue',
addressInfo: {
city: 'New York City',
},
}
//That's why you need a deep merge method to do so:
this.setState({
addressInfo: Object.assign({}, this.state.addressInfo, {
city: 'New York City'
})
});
// this is the RECOMMENDED and cleanest deep merge method:
this.setState({
addressInfo: {
...this.state.addressInfo,
city: 'New York City'
}
});One thing to keep in mind is that setting state is not synchronous. State updates are batched internally and then executed simultaneously whenever React feels it's appropriate. This might result in some unexpected behavior. Going back to our ClickityClick component above, let's log the state after we've set it using this.setState():
...
handleClick = () => {
this.setState({
hasBeenClicked: true
})
console.log(this.state.hasBeenClicked); // prints false
}
...If we want to access our new state after it has been updated, we can optionally add a callback as a second argument to the this.setState() function. This callback will fire once the state has been updated, ensuring that this.state is now the new, shiny updated state. In code:
...
handleClick = () => {
this.setState({
hasBeenClicked: true
}, () => console.log(this.state.hasBeenClicked)) // prints true
}
...As mentioned before, setState is not synchronous — in situations where there are many state changes being made, multiple setState calls may be grouped together into one update. If we use this.state inside a setState, it is possible that the values in state are changed by a different setState just prior to our setState.
One way to deal with this is to handle the logic that involves this.state outside of setState.
handleClick = () => {
// when handleClick is called, newCount is set to whatever this.state.count is plus 1 PRIOR to calling this.setState
let newCount = this.state.count + 1
this.setState({
count: newCount
})
}This works, but React actually provides a built in solution.
That function, when called inside setState will be passed the component state from when that setState was called. This is typically referred to as the previous state.
...
handleClick = () => {
this.setState(previousState => {
return {
count: previousState.count + 1
}
})
}
...or for a boolean value:
...
handleClick = () => {
this.setState(previousState => {
return {
toggled: !previousState.toggled
}
})
}
...Changes in state and/or props will both trigger a re-render of our React component. However, changes in state can only happen internally due to components changing their own state. Thus, a component can trigger changes in its own state. A component cannot change its props. Changes in props can only happen externally, meaning the parent or grandparent component changes the values it passing down to its children.
To recap: Using setState, we can update a component's state. We frequently use events to trigger these updates. setState is called asynchronously and merges the existing state with whatever object is passed in. We can also pass a function to setState, which allows us to write state changes that are based on the existing state values.
Being able to update state can be extremely useful in many situations. We can use state to keep track of incrementing values and toggle-able boolean values. We can also use it to keep track of things like timestamps, user inputs, and in-line style settings. State can store arrays as well, which we now have the ability to update and add to!
When using tags with not closing tag as <input> rather than <div></div> do write it with a / at the end:
<input onKeyUp={this.callback} type="password" name="" id="" />if written as this:
<input onKeyUp={this.callback} type="password" name="" id="">will give error.