O problema que estamos resolvendo
O contexto em react pode conter muitos valores e diferentes consumidores do contexto podem usar apenas parte dos valores. No entanto, quando qualquer valor muda do contexto, todos os consumidores (em particular, todos os componentes que usam useContext
) serão renderizados novamente , mesmo se eles não dependerem da parte alterada dos dados. O problema é bastante discutido e tem muitas soluções diferentes. Aqui estão alguns deles. Criei este exemplo para demonstrar o problema. Basta abrir o console e pressionar os botões.
propósito
Nossa solução deve alterar as bases de código existentes ao mínimo. Quero criar meu próprio gancho personalizado useSmartContext
com a mesma assinatura de useContext
, mas que apenas renderizará novamente o componente quando a parte usada do contexto for alterada.
Ideia
Descubra o que está sendo usado pelo componente envolvendo o useSmartContext
valor de retorno em um Proxy.
Implementação
Passo 1.
Criamos nosso próprio gancho.
const useSmartContext(context) {
const usedFieldsRef = useRef(new Set());
const proxyRef = useRef(
new Proxy(
{},
{
get(target, prop) {
usedPropsRef.current.add(prop);
return context._currentValue[prop];
}
}
)
);
return proxyRef.current;
}
Criamos uma lista na qual armazenaremos os campos de contexto usados. Criamos um proxy com uma get
armadilha na qual preenchemos essa lista. Target
não importa para nós, então passei um objeto vazio como o primeiro argumento {}
.
Passo 2.
Você precisa obter o valor do contexto quando ele for atualizado e comparar o valor dos campos da lista usedPropsRef
com os valores anteriores. Se algo mudou, acione uma nova renderização. useContext
Não podemos usá-lo dentro de nosso gancho, caso contrário, nosso gancho também começará a causar uma nova renderização para todas as alterações. Aqui começam as danças com um pandeiro. Originalmente, esperava subscrever as mudanças de contexto com context.Consumer
. Nomeadamente assim:
React.createElement(context.Consumer, {}, (newContextVakue) => {/* handle */})
. . - , , , .
React
, useContext
. , , , . - . _currentValue
. , undefined
. ! Proxy , . Object.defineProperty
.
let val = context._currentValue;
let notEmptyVal = context._currentValue;
Object.defineProperty(context, "_currentValue", {
get() {
return val;
},
set(newVal) {
if (newVal) {
// !
}
val = newVal;
}
});
! : useSmartContext
Object.defineProperty
. useSmartContext
createContext
.
export const createListenableContext = () => {
const context = createContext();
const listeners = [];
let val = context._currentValue;
let notEmptyVal = context._currentValue;
Object.defineProperty(context, "_currentValue", {
get() {
return val;
},
set(newVal) {
if (newVal) {
listeners.forEach((cb) => cb(notEmptyVal, newVal));
notEmptyVal = newVal;
}
val = newVal;
}
});
context.addListener = (cb) => {
listeners.push(cb);
return () => listeners.splice(listeners.indexOf(cb), 1);
};
return context;
};
, . ,
const useSmartContext = (context) => {
const usedFieldsRef = useRef(new Set());
useEffect(() => {
const clear = context.addListener((prevValue, newValue) => {
let isChanged = false;
usedFieldsRef.current.forEach((usedProp) => {
if (!prevValue || newValue[usedProp] !== prevValue[usedProp]) {
isChanged = true;
}
});
if (isChanged) {
//
}
});
return clear;
}, [context]);
const proxyRef = useRef(
new Proxy(
{},
{
get(target, prop) {
usedFieldsRef.current.add(prop);
return context._currentValue[prop];
}
}
)
);
return proxyRef.current;
};
3.
. useState
, . , . - ?
// ...
const [, rerender] = useState();
const renderTriggerRef = useRef(true);
// ...
if (isChanged) {
renderTriggerRef.current = !renderTriggerRef.current;
rerender(renderTriggerRef.current);
}
, . . useContext
->useSmartContext
createContext
->createListenableContext
.
, !
, . .
Ao escrever este artigo, me deparei com outra biblioteca que resolve o mesmo problema com a otimização de redesenho ao usar o contexto. A solução desta biblioteca, em minha opinião, é a mais correta que já vi. Suas fontes são muito mais legíveis e eles me deram algumas idéias sobre como preparar nossa produção de exemplo sem mudar a forma de uso. Se eu receber uma resposta positiva sua, escreverei sobre a nova implementação.
Obrigado a todos pela atenção.