TransWikia.com

Comportamento inesperado ao atualizar o estado do componente

Stack Overflow em Português Asked on November 7, 2021

Estou trabalhando num projeto com muitos componentes, e, a maioria deles precisam compartilhar o mesmo estado, fazer isso através das props funciona, mas é de difícil manutenção, se um componente filho precisar de mais algum valor, é preciso alterar todos os componentes pai até a origem do dado

Como alternativa, há os Contexts, porém os mesmos são ruins, para usar mais de um Context no mesmo componente, deve adicionar um Comsumer no método render, mas o acesso a ele será apenas dentro desse método, no meu caso, preciso acessar múltiplos contexts em todo o componente, além disso, há lugares em que preciso acessar antes do render ser chamado, então, é ruim esperar passar de dentro do método para uma propriedade do componente

Uma terceira alternativa é o uso do Redux, seu funcionamento é exatamente como preciso, um local que armazene todos os estados e disponibilize aos componentes. Porém, ele é um pouco complicado, requer um certo mapeamento do estado para uma propriedade, além disso, um dos pré requisitos do projeto é ser o menor possível, sendo assim, é preferível utilizar o mínimo de bibliotecas possível

No final, criei um pequeno utilitário simples que mantinha um estado global dentro dos componentes sempre atualizado:

  • Um arquivo contém a variável do estado global e disponibiliza três métodos para criar e alterar um "subestado" (para não manter tudo no mesmo estado, assim organizar melhor) e um listener para avisar quando houver uma alteração do estado, esse arquivo é utilizado apenas pelo utilitário, não pelo projeto todo
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;
  • Uma classe que deve ser utilizada para criar o estado, através de extensão, nela é definido um nome único para o subestado e seu valor, que será setado no estado global (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);
    }

}
  • Por último, um componente abstrato para ser estendido no lugar de React.Component, ele adiciona um listener no primeiro arquivo, quando houver uma mudança no estado global, atualiza o estado do componente
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;
    }

}

Seu uso é simples, para criar um estado, basta criar uma classe que estenda State e instanciar. Para usar, basta o componente estender de Component, o estado global sempre se manterá atualizado em 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>
        </>);
    }

}

Até aí tudo funcionando, porém, em dado momento, precisei observar as mudanças no estado global, e fazer algumas ações ao ocorrer uma alteração em determinada propriedade:

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

Quando há uma alteração no estado global, o componentDidUpdate é chamado e this.state.global.myState.myValue é alterado pelo novo valor, porém prevState.global.myState.myValue também é alterada pelo novo valor, em vez de manter o antigo. Nenhuma outra alteração é feita para que o componentDidUpdate seja chamado por outro motivo se não a alteração em myState.myValue, as comparações JSON.stringify(prevState) === JSON.stringify(this.state) e JSON.stringify(prevProps) === JSON.stringify(this.props) retornam verdadeiro. Então por que a variável que deveria manter o estado anterior, é alterada pro novo estado?

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>

One Answer

Depois de mais alguns testes, descobri o motivo, a variável globalState é um objeto, e, por isso é passada sempre como referência, ao atualizar o valor do subestado (MyState, classe que herda de State), a variável do estado do componente era alterado, mas não seu "estado real" (o que é renderizado), semelhante ao que ocorre quando você altera o estado do componente fora do construtor diretamente (this.state = { ... }), em vez de usar setState, porém o React não percebe e não houve uma exceção avisando do erro, depois da variável de estado do componente ser alterada, o estado real do componente é alterado (no store.onChange(state => { ...) e o componentDidUpdate é chamado, mas o estado anterior que é passado já tinha sido alterado, por ser uma referência. Isso pode ser percebido com algumas alterações...

O componente deve alterar o estado apenas na primeira vez, para que o método change seja setado no estado global, então verifique se já foi setado, se sim, não atualiza

_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 };
    });
}

No componente de teste adicione um segundo botão para logar o valor da variável de estado do componente

<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>

Se clicar em "change" e depois "log", vai perceber que this.state.global.myState.myValue mudou, mesmo que o estado real não tenha sido alterado no listener, e portanto, a tela também não mudou

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>

A solução é simples, quando o componente for atualizar o estado, deve criar uma nova variável, em vez de passar a referência do objeto:

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

Answered by Costamilam on November 7, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP