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:
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;
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);
}
}
React.Component
, ele adiciona um listener no primeiro arquivo, quando houver uma mudança no estado global, atualiza o estado do componenteimport { 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>
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
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP