0

There's a state here, brushSize which is adjusted using a slider. It goes as props to another component, Board. However, the render function keeps executing before the state is able to change, and Board ends up getting the old value. I was able to confirm this by checking the received props value in Board.js, which is the old value.

According to what I could find, render() is supposed to execute once the state has changed, but that doesn't seem to be happening.

import React from 'react'
import Board from './Board'
import './style.css'

class Container extends React.Component {
    constructor(props) {
        super(props);
        this.state = {brushSize: 2};

        this.brushSizeChange = this.brushSizeChange.bind(this);
    }

    brushSizeChange(e) {
        this.setState((prev) => { return { ...prev, brushSize: e.target.value } });
    }

    render() {
        return (
            <div className="container">
                <label>Brush size: </label>
                <input type="range" min="1" max="10" value={this.state.brushSize} onChange={this.brushSizeChange} />
                <h1>placeholder</h1>
                <div className="board-container">
                    <Board size={this.state.brushSize}/>
                </div>
            </div>
        )
    }
}

export default Container
WiseRohin
  • 15
  • 4

1 Answers1

1

In React, you should only access an event argument inside a synchronous handler. If you do something asynchronous - like provide a state setter callback - and then try to access the event inside the callback, it won't work properly, and you won't be able to access the .value. One possible warning you'll see is:

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https:// fb.me/react-event-pooling for more information.

in Container

While you can extract the value first, so you don't access the event:

const Board = ({ size }) => <div>Brush size: {size}</div>

class Container extends React.Component {
    constructor(props) {
        super(props);
        this.state = {brushSize: 2};

        this.brushSizeChange = this.brushSizeChange.bind(this);
    }

    brushSizeChange(e) {
        const { value } = e.target;
        this.setState((prev) => { return { brushSize: value } });
    }

    render() {
        return (
            <div className="container">
                <label>Brush size: </label>
                <input type="range" min="1" max="10" value={this.state.brushSize} onChange={this.brushSizeChange} />
                <h1>placeholder</h1>
                <div className="board-container">
                </div>
            </div>
        )
    }
}

ReactDOM.render(<Container />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

There's no need for the callback at all because you aren't setting any other state on input change.

const Board = ({ size }) => <div>Brush size: {size}</div>

class Container extends React.Component {
    constructor(props) {
        super(props);
        this.state = {brushSize: 2};

        this.brushSizeChange = this.brushSizeChange.bind(this);
    }

    brushSizeChange(e) {
        const { value } = e.target;
        this.setState({ brushSize: value });
    }

    render() {
        return (
            <div className="container">
                <label>Brush size: </label>
                <input type="range" min="1" max="10" value={this.state.brushSize} onChange={this.brushSizeChange} />
                <h1>placeholder</h1>
                <div className="board-container">
                </div>
            </div>
        )
    }
}

ReactDOM.render(<Container />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

Then you can pass down this.state.brushSize to the <input> and to the <Board>, and they'll see and respond to the state now that it's being changed properly. (If you want the Board to see the value, you should almost certainly be doing something like <Board size={this.state.brushSize} /> - if you don't pass the state down, Board won't be able to see it)

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Were you able to reproduce OP's problem with the _updater_ callback? – Phil Jul 04 '22 at 01:54
  • 1
    Yes, the attempt to access properties of the target fails, so the new state doesn't get set to begin with - an error is thrown. – CertainPerformance Jul 04 '22 at 01:56
  • I guess OP missed the error somehow – Phil Jul 04 '22 at 01:57
  • Oddly enough, I didn't get the warning you talked about (although I still implemented the change, to avoid getting such a warning in the future), and the state does change without error. However, it still doesn't pass the new state down before the component re-renders. – WiseRohin Jul 04 '22 at 02:07
  • 1
    @WiseRohin Works fine here - see edit. The problem is probably in your Board component. Make sure Board is using the prop directly, and isn't, for example, putting the prop into state and then using the state. – CertainPerformance Jul 04 '22 at 02:13