Unexpected behavior when updating component status

Asked

Viewed 303 times

-1

I’m working on a project with many components, and most of them need to share the same state, do it through props works, but is difficult to maintain, if a child component needs any more value, it is necessary to change all parent components up to the data source

Alternatively, there are Contexts, but they are bad, to use more than one Context in the same component, you must add a Comsumer in the method render, but access to it will only be within this method, in my case I need to access multiple contexts throughout the component, plus there are places I need to access before the render be called then it is bad to expect to pass from within the method to a component property

A third alternative is the use of Redux, its operation is exactly as accurate, a place that stores all the states and makes available to the components. However, it is a bit complicated, requires a certain mapping of the state to a property, in addition, one of the prerequisites of the project is to be as small as possible, so it is preferable to use as few libraries as possible

In the end, I created a small simple utility that kept a global state within the components always updated:

  • A file contains the global state variable and provides three methods to create and change a "substate" (not to keep everything in the same state, so organize better) and a Systener to warn when there is a change of state, this file is used only by the utility, not by the whole project
const globalState = {};

const listeners = [];

function createState(name, initialValue = {}) {
    globalState[name] = initialValue;

    callListeners();
}

function setState(name, state) {
    globalState[name] = { ...globalState[name], ...state };

    callListeners();
}

function onChange(listener) {
    listeners.push(listener);
}

function callListeners() {
    for (const listener of listeners)
        listener(globalState);
}

const store = { createState, setState, onChange };

export default store;
  • A class that must be used to create the state, by extension, in it is defined a unique name for the substation and its value, which will be set in the global state (global[name] = value)
import store from './store';

export default class State {

    name;

    state = {};

    constructor(name) {
        this.name = name;

        store.createState(name);
    }

    setState(state) {
        this.state = { ...this.state, ...state };

        store.setState(this.name, this.state);
    }

}
  • Finally, an abstract component to be extended in place of React.Component, it adds a Listener in the first file, when there is a change in the global state, updates the state of the component
import { Component as ReactComponent } from 'react';

import store from './store';

export default class Component extends ReactComponent {

    mounted = false;

    state = {
        global: { }
    };

    constructor(props) {
        super(props);

        store.onChange(state => {
            if (this.mounted)
                this.setState({ global: state });
            else
                this.state = { ...this.state, global: state };
        });
    }

    componentDidMount() {
        this.mounted = true;
    }

}

Its use is simple, to create a state, just create a class that extends State and instantiate. To use, just extend component of Component, the global state will always keep up to date on this.state.global:

import React from 'react';

import states from './components/states';

// Defini o subestado
class MyState extends states.State {

    state = { myValue: Math.random(), change: () => this.change() };

    constructor() {
        super('myState');

        this.setState(this.state);
    }

    change() {
        this.setState({
            myValue: Math.random()
        });
    }

}

export default class App extends states.Component {

    constructor(props) {
        super(props);

        // Adiciona ao estado global
        new MyState();
    }

    render() {
        // Utiliza através de this.state.global
        return (<>
            <pre>{ JSON.stringify(this.state.global) }</pre>

            <button onClick={() => this.state.global.myState.change()}>change</button>
        </>);
    }

}

Until then everything worked, however, at a given moment, I had to observe the changes in the global state, and make some actions when there is a change in a certain property:

componentDidUpdate(prevProps, prevState) {
    if (prevState.global.myState.myValue !== this.state.global.myState.myValue)
        // ...
}

When there is a change in the overall state, the componentDidUpdate is called and this.state.global.myState.myValue is changed by the new value, but prevState.global.myState.myValue is also changed by the new value instead of keeping the old one. No other changes are made so that the componentDidUpdate is called for another reason than the change in myState.myValue, the comparisons JSON.stringify(prevState) === JSON.stringify(this.state) and JSON.stringify(prevProps) === JSON.stringify(this.props) return true. So why the variable that should keep the previous state is changed to the new state?

const globalState = {};

const listeners = [];

function createState(name, initialValue = {}) {
    globalState[name] = initialValue;

    callListeners();
}

function setState(name, state) {
    globalState[name] = { ...globalState[name], ...state };

    callListeners();
}

function onChange(listener) {
    listeners.push(listener);
}

function callListeners() {
    for (const listener of listeners)
        listener(globalState);
}

const store = { createState, setState, onChange };

class State {

    name;

    state = {};

    constructor(name) {
        this.name = name;

        store.createState(name);
    }

    setState(state) {
        this.state = { ...this.state, ...state };

        store.setState(this.name, this.state);
    }

}

class Component extends React.Component {

    mounted = false;

    state = {
        global: store.globalState
    };

    constructor(props) {
        super(props);

        store.onChange(state => {
            if (this.mounted)
                this.setState({ global: state });
            else
                this.state = { ...this.state, global: state };
        });
    }

    componentDidMount() {
        this.mounted = true;
    }

}

class MyState extends State {

    state = { myValue: Math.random(), change: () => this.change() };

    constructor() {
        super('myState');

        this.setState(this.state);
    }

    change() {
        this.setState({
            myValue: Math.random()
        });
    }

}

class App extends Component {

    constructor(props) {
        super(props);

        new MyState();
    }

    componentDidUpdate(prevProps, prevState) {
        console.log(JSON.stringify(prevProps) === JSON.stringify(this.props))
        console.log(JSON.stringify(prevState) === JSON.stringify(this.state))
        console.log(prevState.global.myState.myValue, this.state.global.myState.myValue)
    }

    render() {
        return (<div>
            <pre>{ JSON.stringify(this.state.global) }</pre>

            <button onClick={() => this.state.global.myState.change()}>change</button>
        </div>);
    }

}

ReactDOM.render(<App />, document.getElementById('root'));
<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>
<div id="root"></div>

  • perhaps because it has already updated.

  • you who created all the code type a context or Redux ???

  • @Virgilionovic also thought about it, but there is no other amendment that can call the componentDidUpdate, as it is called can only be because of the changing global state. Yes, that would be just a prototype, to see if it works, but for now it’s not working 100%

  • so that’s it, I study react you who made the code, maybe (I’m just thinking you) did not have the state next to the react, may be a change only in the value of the variable and not in the state. that the code is too large have not tested

  • Mount with code in your question I find it better to do it in my https://answall.com/questions/463410/problema-na-l%C3%b3gica-React-n%C3%a3o-calcula/463417#463417 can be tested

  • 1

    @Virgilionovic to change the state of the component I use the setState React, so I should change it normally. I added an executable, thanks for the tip

Show 1 more comment

1 answer

0


After a few more tests, I discovered the reason, the variable globalState is an object, and therefore is always passed as reference, when updating the value of the substate (Mystate, class inherited from State), the variable of the state of the component was changed, but not its "real state" (which is rendered), similar to what happens when you change the state of the component outside the constructor directly (this.state = { ... }), instead of using setState, but React doesn’t notice and there was no exception warning the error, after the component’s state variable is changed, the actual state of the component is changed (no store.onChange(state => { ...) and the componentDidUpdate is called, but the previous state that is passed had already been changed, as it is a reference. This can be noticed with some changes...

The component shall change the status only the first time, so that the method change is set in the global state, so check if it has already been set, if yes, does not update

_setted = false;

constructor(props) {
    super(props);

    store.onChange(state => {
        // Se o estado já foi definido, não atualiza o estado
        if (this._setted)
            return;

        // Verifica se o estado já foi definido
        if (state.myState && state.myState.change)
            this._setted = true;

        if (this.mounted)
            this.setState({ global: state });
        else
            this.state = { ...this.state, global: state };
    });
}

In the test component add a second button to log the value of the component status variable

<pre>{ JSON.stringify(this.state.global) }</pre>

<button onClick={() => this.state.global.myState.change()}>change</button>

<button onClick={() => console.log(this.state.global.myState)}>log</button>

If you click on "change" and then "log", you will realize that this.state.global.myState.myValue has changed, even though the real state has not been changed in the system, and therefore the screen has not changed either

const globalState = {};

const listeners = [];

function createState(name, initialValue = {}) {
    globalState[name] = initialValue;

    callListeners();
}

function setState(name, state) {
    globalState[name] = { ...globalState[name], ...state };

    callListeners();
}

function onChange(listener) {
    listeners.push(listener);
}

function callListeners() {
    for (const listener of listeners)
        listener(globalState);
}

const store = { createState, setState, onChange };

class State {

    name;

    state = {};

    constructor(name) {
        this.name = name;

        store.createState(name);
    }

    setState(state) {
        this.state = { ...this.state, ...state };

        store.setState(this.name, this.state);
    }

}

class Component extends React.Component {

    mounted = false;

    state = {
        global: store.globalState
    };

    _setted = false;

    constructor(props) {
        super(props);

        store.onChange(state => {
            // Se o estado já foi definido, não atualiza o estado
            if (this._setted)
                return;

            // Verifica se o estado já foi definido
            if (state.myState && state.myState.myValue)
                this._setted = true;

            if (this.mounted)
                this.setState({ global: state });
            else
                this.state = { ...this.state, global: state };
        });
    }

    componentDidMount() {
        this.mounted = true;
    }

}

class MyState extends State {

    state = { myValue: Math.random(), change: () => this.change() };

    constructor() {
        super('myState');

        this.setState(this.state);
    }

    change() {
        this.setState({
            myValue: Math.random()
        });
    }

}

class App extends Component {

    constructor(props) {
        super(props);

        new MyState();
    }

    componentDidUpdate(prevProps, prevState) {
        console.log(JSON.stringify(prevProps) === JSON.stringify(this.props))
        console.log(JSON.stringify(prevState) === JSON.stringify(this.state))
        console.log(prevState.global.myState.myValue, this.state.global.myState.myValue)
    }

    render() {
        return (<div>
            <pre>{ JSON.stringify(this.state.global) }</pre>

            <button onClick={() => this.state.global.myState.change()}>change</button>

            <button onClick={() => console.log(this.state.global.myState)}>log</button>
        </div>);
    }

}

ReactDOM.render(<App />, document.getElementById('root'));
<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>
<div id="root"></div>

The solution is simple, when the component is updating the state, it must create a new variable, instead of passing the object reference:

store.onChange(state => {
    if (this.mounted)
        this.setState({ global: { ...state } });
    else
        this.state = { ...this.state, global: { ...state } };
});

Browser other questions tagged

You are not signed in. Login or sign up in order to post.