Я создаю веб-приложение с использованием React и столкнулся с этой странной проблемой.
Подводя итог, можно сказать, что дочерние элементы, выраженные в виде массива внутри фигурных скобок (например: {[<Element />, <Element />]}, сбрасываются при добавлении или удалении одного из братьев и сестер.
Мой вопрос заключается в том, ожидается ли такое поведение React, и если да, то почему это происходит?
Для иллюстрации я привел два примера. Их код точно такой же, за исключением того, что первый объявляет элементы непосредственно в JSX, а второй объявляет их внутри массива (может быть сгенерирован Array.map):
Ticker — это общий компонент, созданный для демонстрации состояния. DummyElement — это общий компонент без какого-либо состояния. App — корневой компонент.
В первом примере видно, что при переключении между раскладками, то есть при добавлении или удалении DummyElement, состояние Tickers сохраняется. Это поведение, которого я ожидал, учитывая, что реквизиты Tickerskey остаются прежними.
Однако во втором примере состояние Ticker сбрасывается всякий раз, когда вы переключаетесь между макетами. Это дополнительно показано в консоли, которая регистрирует, что Tickers монтируется и размонтируется при каждом изменении макета.
Редактировать:
Я поднял проблема, связанный с вопросом :)



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Вы видите это, потому что вы изменили топологию (как вложены теги и массивы) дерева в примере 2:
Вот модифицированная версия, которая не сбрасывает состояние, я сохраняю узлы массива и не массива в топологии:
_renderLayout1() {
return (
<div>
<div className = "top">
<span>
{[
<Ticker key = "1" name = "1" />,
<Ticker key = "2" name = "2" />
]}
</span>
</div>
<div className = "bottom">
{this._renderButtons()}
</div>
</div>
);
}
_renderLayout2() {
return (
<div>
<div className = "top">
<span>
{[
<Ticker key = "1" name = "1" />,
<Ticker key = "2" name = "2" />
]}
</span>
<DummyElement/>
</div>
<div className = "bottom">
{this._renderButtons()}
</div>
</div>
);
}
https://jsfiddle.net/L1syr347/
Вот еще вариант, сохраняющий топологию, я все поместил в массив:
_renderLayout1() {
return (
<div>
<div className = "top">
{[
<Ticker key = "1" name = "1" />,
<Ticker key = "2" name = "2" />
]}
</div>
<div className = "bottom">
{this._renderButtons()}
</div>
</div>
);
}
_renderLayout2() {
return (
<div>
<div className = "top">
{[
<Ticker key = "1" name = "1" />,
<Ticker key = "2" name = "2" />,
<DummyElement key = "3"/>
]}
</div>
<div className = "bottom">
{this._renderButtons()}
</div>
</div>
);
}
https://jsfiddle.net/L1syr347/1/
Я думаю, что ближе всего к официальным документам по теме вы найдете здесь: reactjs.org/docs/reconciliation.html
Я не знаю... Прочитав это, я пришел к выводу, что компоненты не должны терять свое состояние. Я думаю, что отправлю вопрос на страницу React github... Еще раз спасибо за помощь!
Когда react отображает несколько children, он обрабатывает его как множество дочерних элементов, но когда children является дочерним элементом Один, react будет рассматривать его как один элемент.
В вашем случае интересно видеть, что в первом условии children из <div className = "top"> является array, но на самом деле является одним дочерним «элементом»:
<div className = "top">
{[<Ticker name = "1" />, <Ticker name = "2" />]}
</div>
Если мы посмотрим на него как на элемент реакции, то увидим примерно следующее:
{
type: 'div',
className: 'top',
children: [<Ticker name = "1" />, <Ticker name = "2" />]
}
Но во втором условии у нас есть 2 ребенка:
<div className = "top">
{[<Ticker name = "1" />, <Ticker name = "2" />]}
<DummyElement key = "3" />
</div>
Итак, в основном у нас есть array дочерних элементов, которые содержат другой массив элементов А ТАКЖЕ другого элемента.
Если мы посмотрим на него как на элемент реакции, то увидим примерно следующее:
{
type: 'div',
className: 'top',
children: [
[<Ticker name = "1" />, <Ticker name = "2" />],
<DummyElement key = "3" />
]
}
Таким образом, в обоих случаях type детей является массивом (по совпадению), но тип элементов массива меняется:
В первом случае первым элементом array является элемент Ticker.
Во втором случае первым членом array является другой array
Поэтому, когда реакция выполняет свой процесс примирение, она проверяет следующее:
- Two elements of different types will produce different trees.
- The developer can hint at which child elements may be stable across different renders with a key prop.
Итак, ваш случай падает на первую проверку:
type Ticker -> type Array
Чтобы доказать это, я создал тот же пример, что и ваш, но добавил дополнительный элемент в качестве дочернего элемента, поэтому children всегда будет типом array, таким образом, мы всегда будем получать элемент следующим образом:
{
type: 'div',
className: 'top',
children: [
{type: 'div'},
[<Ticker name = "1" />, <Ticker name = "2" />],
/* DummyElement will be added conditionally */
]
}
Вот рабочий пример (обратите внимание, что я сохраняю положение детей):
class App extends React.Component {
constructor(props) {
super(props);
this.state = { layout: 1 };
}
render() {
let toRender = null;
if (this.state.layout == 1) toRender = this._renderLayout1();
else if (this.state.layout == 2) toRender = this._renderLayout2();
return toRender;
}
_renderLayout1() {
return (
<div>
<div className = "top">
<div>I'm forcing children as array</div>
{[<Ticker name = "1" />, <Ticker name = "2" />]}
</div>
<div className = "bottom">{this._renderButtons()}</div>
</div>
);
}
_renderLayout2() {
return (
<div>
<div className = "top">
<div>I'm forcing children as array</div>
{[<Ticker name = "1" />, <Ticker name = "2" />]}
<DummyElement key = "3" />
</div>
<div className = "bottom">{this._renderButtons()}</div>
</div>
);
}
_renderButtons() {
return (
<React.Fragment>
<button onClick = {() => this.setState({ layout: 1 })}>2x Ticker</button>
<button onClick = {() => this.setState({ layout: 2 })}>
2x Ticker + DummyElement
</button>
</React.Fragment>
);
}
}
class Ticker extends React.Component {
// Display seconds from the moment I'm created.
constructor(props) {
super(props);
this.state = { tickNumber: 0 };
}
componentDidMount() {
console.info(`Mount Ticker "${this.props.name}"`);
this.timerID = setInterval(() => {
this.setState(prevState => ({ tickNumber: prevState.tickNumber + 1 }));
}, 1000);
}
componentWillUnmount() {
console.info(`Unmount Ticker "${this.props.name}"`);
clearInterval(this.timerID);
}
render() {
const displayTick = String(this.state.tickNumber).padStart(4, 0);
const displayStr = `Ticker "${this.props.name}" - ${displayTick}`;
return <div className = "Ticker">{displayStr}</div>;
}
}
function DummyElement() {
return <div className = "DummyElement">Dummy element</div>;
}
ReactDOM.render(<App />, document.querySelector("#root"));.top,
.bottom {
margin: 1em;
}
.Ticker,
.DummyElement {
display: inline-block;
margin-right: 1em;
border: 1px solid black;
}<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), вы можете обернуть их Реагировать. Фрагмент, просто убедитесь, что вы предоставили тот же key. Обратите внимание, что Fragment без key рассматривается как массив, react всегда будет «думать», что это новый хост экземпляра, поэтому он будет воссоздан (и его дочерние элементы).
Вот пример вашего второго примера, но с желаемым поведением:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {layout : 1};
}
render() {
if (this.state.layout == 1)
return this._renderLayout1();
else if (this.state.layout == 2)
return this._renderLayout2();
}
_renderLayout1() {
return (
<div>
<div className = "top">
<React.Fragment key = "1">
{[
<Ticker key = "1" name = "1" />,
<Ticker key = "2" name = "2" />
]}
</React.Fragment>
</div>
<div className = "bottom">
{this._renderButtons()}
</div>
</div>
);
}
_renderLayout2() {
return (
<div>
<div className = "top">
<React.Fragment key = "1">
{[
<Ticker key = "1" name = "1" />,
<Ticker key = "2" name = "2" />
]}
</React.Fragment>
<DummyElement key = "3" />
</div>
<div className = "bottom">
{this._renderButtons()}
</div>
</div>
);
}
_renderButtons() {
return (
<React.Fragment>
<button onClick = { () => this.setState({'layout': 1}) }>
2x Ticker
</button>
<button onClick = { () => this.setState({'layout': 2}) }>
2x Ticker + DummyElement
</button>
</React.Fragment>
);
}
}
class Ticker extends React.Component {
// Display seconds from the moment I'm created.
constructor(props) {
super(props);
this.state = {tickNumber: 0};
}
componentDidMount() {
console.info(`Mount Ticker "${this.props.name}"`);
this.timerID = setInterval(
() => {
this.setState(
prevState => ({tickNumber: prevState.tickNumber + 1})
);
},
1000
);
}
componentWillUnmount() {
console.info(`Unmount Ticker "${this.props.name}"`);
clearInterval(this.timerID);
}
render() {
const displayTick = String(this.state.tickNumber).padStart(4, 0);
const displayStr = `Ticker "${this.props.name}" - ${displayTick}`;
return (
<div className = "Ticker">
{displayStr}
</div>
);
}
}
function DummyElement() {
return (
<div className = "DummyElement">
Dummy element
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#root")).top,
.bottom {
margin: 1em;
}
.Ticker,
.DummyElement {
display: inline-block;
margin-right: 1em;
border: 1px solid black;
}<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>С учетом сказанного, я думаю, что лучшим и более читаемым подходом здесь будет визуализировать все как есть и только условно визуализировать DummyElement.
<div className = "top">
{[<Ticker key = "1" name = "1" />, <Ticker key = "2" name = "2" />]}
{layout === 2 && <DummyElement key = "3" />}
</div>
Но почему это работает так, как ожидалось? Я имею в виду, что в этом случае снова будет предоставлен children в виде нескольких элементов (которые react преобразуются в массив) или одного элемента (который react сгладит его до одного элемента).
Оказывается, когда мы используем оператор &&, react будет использовать либо правую часть (когда условие true), либо null (когда условие false), а null сохранит «дыру» в array. это означает, что мы всегда будем получать array из children.
Таким образом, мы получаем этот элемент:
{
type: 'div',
className: 'top',
children: [
[<Ticker name = "1" />, <Ticker name = "2" />],
null || DummyElement
]
}
Вот работающий пример:
class App extends React.Component {
constructor(props) {
super(props);
this.state = { layout: 1 };
}
render() {
return this._renderLayout();
}
_renderLayout() {
const { layout } = this.state;
return (
<div>
<div className = "top">
{[<Ticker key = "1" name = "1" />, <Ticker key = "2" name = "2" />]}
{layout === 2 && <DummyElement key = "3" />}
</div>
<div className = "bottom">{this._renderButtons()}</div>
</div>
);
}
_renderButtons() {
return (
<React.Fragment>
<button onClick = {() => this.setState({ layout: 1 })}>2x Ticker</button>
<button onClick = {() => this.setState({ layout: 2 })}>
2x Ticker + DummyElement
</button>
</React.Fragment>
);
}
}
class Ticker extends React.Component {
// Display seconds from the moment I'm created.
constructor(props) {
super(props);
this.state = { tickNumber: 0 };
}
componentDidMount() {
console.info(`Mount Ticker "${this.props.name}"`);
this.timerID = setInterval(() => {
this.setState(prevState => ({ tickNumber: prevState.tickNumber + 1 }));
}, 1000);
}
componentWillUnmount() {
console.info(`Unmount Ticker "${this.props.name}"`);
clearInterval(this.timerID);
}
render() {
const displayTick = String(this.state.tickNumber).padStart(4, 0);
const displayStr = `Ticker "${this.props.name}" - ${displayTick}`;
return <div className = "Ticker">{displayStr}</div>;
}
}
function DummyElement() {
return <div className = "DummyElement">Dummy element</div>;
}
ReactDOM.render(<App />, document.querySelector("#root"));.top,
.bottom {
margin: 1em;
}
.Ticker,
.DummyElement {
display: inline-block;
margin-right: 1em;
border: 1px solid black;
}<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"/>
Спасибо за ответ! Я пока не принимаю его, потому что не нашел в документах места, где рассказывается о топологии. Тем не менее, исправления работают отлично :)