Вопрос или проблема
У меня есть довольно сложная ситуация, когда состояние приложения изменяется в ответ на множество разных событий, происходящих одновременно, поэтому классический setState(state)
не работает, и мне приходится использовать setState(state => { return {...state + extra } })
, что работает на нескоммбитированном состоянии (это означает, что другие события могут вносить изменения до его использования для следующего перерисовки), но одновременно мне нужно вернуть некоторые результаты через промис, КОГДА СОСТОЯНИЕ БУДЕТ СКОММИТИРОВАНО (после того, как все параллельные изменения будут применены к нему), так что я делаю:
function applyChangesToState(param1, param2) {
return new Promise(resolve => {
this.setState(state => {
// некоторые вычисления здесь
const extra = calculateSomeExtra(state, param1, param2);
return {...state, extra };
, () => {
// КАК Я МОГУ ДОСТУПИТЬСЯ К СКОММИТИРОВАННОМУ (САМЫМ ПОСЛЕДНЕМ И ФИНАЛЬНОМ) СОСТОЯНИЮ ЗДЕСЬ?
const { extra } = state; // <-- ОТКУДА Я ДОЛЖЕН ЧИТАТЬ СКОММИТИРОВАННОЕ СОСТОЯНИЕ?
// ЧТОБЫ Я МОГ ПОСТАВИТЬ ЭТО В RESOLVE:
resolve(extra);
// ЕСЛИ Я ЧИТАЮ ИЗ this.state ЗДЕСЬ, УБЕДИТЕЛЬНО ЛИ, ЧТО
// ВСЕ ПАРАЛЛЕЛЬНЫЕ ИЗМЕНЕНИЯ УЖЕ БЫЛИ ПРИМЕНЕНЫ ПЕРЕД
// СЛЕДУЮЩЕЙ ПЕРЕРИСОВКОЙ?
});
});
}
В основном, мой вопрос заключается в Как, инициируя изменение состояния, я могу получить самое окончательное состояние перед его отправкой на перерисовку через обратный вызов?
Помните, что React является фреймворком пользовательского интерфейса. Свойства и состояние вашего компонента являются единственными авторитетами, когда речь идет о том, какие данные доступны для текущего прохода рендеринга: изменяйте их только тогда, когда пользователь должен увидеть что-то новое, а не слепо “когда у вас есть новые данные” =)
В этом случае вам абсолютно не нужно обновлять state
только потому, что ваши источники данных отправили вам новые данные, вы хотите отслеживать все эти изменения вне React, так что можете обновлять состояние вашего компонента только так быстро, как это имеет смысл для людей.
Например:
export class Debouncer() {
state = {};
lastRun = 0;
constructor(initialState, setState, interval = 500) {
this.setState = setState;
this.interval = interval;
this.update(initialState);
}
update(data) {
// зафиксируйте изменение в этом дебаунсере и сбросьте таймер дебаунса
clearTimeout(this.debounce);
Object.assign(this.state, data);
// Затем проверьте, нужно ли нам вызвать setState сейчас или позже:
const now = Date.now();
const delta = now - this.lastRun;
if (delta > this.interval) {
return this.send();
} else {
this.debounce = setTimeout(
() => this.send(),
this.interval
);
}
}
send() {
this.lastRun = Date.now();
this.setState(this.state);
}
}
И затем вы импортируете это и используете как обработчик данных, а не перегружаете setState
данными.
import { useState } from "react";
import { Debouncer } from "./wherever.js";
export function MyComponent(props) {
const [state, setState] = useState({});
useEffect(() => {
const debouncer = new Debouncer(state, setState, 250);
// даже если это создаёт обновления каждую миллисекунду,
// наш компонент не будет обновляться каждую миллисекунду,
// потому что мы не обновляем состояние компонента, мы
// обновляем состояние дебаунсера.
highFrequencyUpdatingAPI.realtime.listen(
`обновления цен акций или что-то подобное`,
(data) => debouncer.update(data)
);
}, []);
return <div>{вещи, основанные на состоянии}</div>;
}
Ответ или решение
Чтобы получить окончательное состояние React-компонента после изменения состояния с помощью метода setState
, необходимо учитывать определенные нюансы, связанные с асинхронностью и управлением состоянием в React. В вашем случае, где необходимо учитывать конкурирующие изменения состояния и получить окончательно согласованное состояние, можно использовать несколько стратегий. Рассмотрим каждую из них, опираясь на ваш вопрос.
1. Использование setState
с колбеком
При вызове setState
, который принимает функцию, возвращающую новое состояние, вы можете гарантировать, что вы работаете с предыдущим состоянием. Однако, для получения окончательного состояния после ререндеринга, вам нужно будет использовать колбек после setState
. Вот как это делается:
function applyChangesToState(param1, param2) {
return new Promise(resolve => {
this.setState(state => {
const extra = calculateSomeExtra(state, param1, param2);
return { ...state, extra };
}, () => {
// Здесь мы можем получить согласованное состояние
const committedState = this.state; // Это гарантирует, что состояние обновлено
const { extra } = committedState;
resolve(extra);
});
});
}
В этом коде, когда вы вызываете resolve(extra)
, вы используете this.state
, чтобы получить текущее состояние — это гарантирует, что все изменения, сделанные до вызываемой функции, будут отражены.
2. Использование useReducer
для сложных состояний
Если ваш компонент имеет сложное состояние, и вы хотите избежать конфликтов состояния, лучше использовать хук useReducer
, который предоставляет более предсказуемый способ управления состоянием.
import React, { useReducer } from 'react';
const initialState = { extra: 0 };
function reducer(state, action) {
switch (action.type) {
case 'update':
return { ...state, extra: state.extra + action.payload };
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
function applyChangesToState(param1, param2) {
return new Promise(resolve => {
// Комбинируем вычисления
const extra = calculateSomeExtra(state, param1, param2);
// Проводим обновление состояния
dispatch({ type: 'update', payload: extra });
// Здесь можно использовать setTimeout или Promise, чтобы дождаться ререндеринга
setTimeout(() => {
resolve(state.extra); // Убедитесь, что состояние обновлено
}, 0);
});
}
return <div>{state.extra}</div>;
}
3. Обработчик состояний на стороне приложения
Если ваше приложение взаимодействует с множеством внешних событий (например, с WebSocket, API и т.д.), то стоит рассмотреть возможность создания централизованного обработчика состояний. Используя такие подходы как Redux или MobX, вы сможете более предсказуемо управлять состоянием вашего приложения и решать проблемы конкурентного доступа.
4. Использование React Context
Когда состояние вашего компонента зависит от других уровней или компонентов, вы можете также использовать React Context для совместного использования состояния. Это поможет избежать конфликтов и централизовать управление данными.
Заключение
Получение окончательного состояния React-компонента после применения setState
может быть осуществлено несколькими способами. Как правило, лучший способ — это использовать колбеки после setState
и подходы управления состоянием, такие как useReducer
или Redux, для более сложных сценариев. Правильное управление состоянием поможет вам избежать возможных конфликтов и неожиданных результатов в вашем приложении.