Você quer solicitar um recurso ou relatar um bug ?
erro
Qual é o comportamento atual?
Não posso confiar nos dados da API de contexto usando (useContext hook) para evitar rerenders desnecessários com React.memo
Se o comportamento atual for um bug, forneça as etapas para reproduzir e, se possível, uma demonstração mínima do problema. Cole o link para o seu exemplo JSFiddle (https://jsfiddle.net/Luktwrdm/) ou CodeSandbox (https://codesandbox.io/s/new) abaixo:
React.memo(() => {
const [globalState] = useContext(SomeContext);
render ...
}, (prevProps, nextProps) => {
// How to rely on context in here?
// I need to rerender component only if globalState contains nextProps.value
});
Qual é o comportamento esperado?
Devo ter de alguma forma acesso ao contexto no retorno de chamada do segundo argumento React.memo para evitar a renderização
Ou devo ter a possibilidade de retornar uma instância antiga do componente de reação no corpo da função.
Quais versões do React e quais navegador / sistema operacional são afetados por esse problema?
16.8.4
Isso está funcionando conforme o planejado. Há uma discussão mais longa sobre isso em https://github.com/facebook/react/issues/14110 se você estiver curioso.
Digamos que por algum motivo você tenha AppContext
cujo valor tem uma propriedade theme
, e deseja apenas renderizar novamente algumas ExpensiveTree
em appContextValue.theme
alterações.
TLDR é que, por enquanto, você tem três opções:
Se precisarmos apenas de appContextValue.theme
em muitos componentes, mas o próprio appContextValue
muda com muita frequência, poderíamos dividir ThemeContext
de AppContext
.
function Button() {
let theme = useContext(ThemeContext);
// The rest of your rendering logic
return <ExpensiveTree className={theme} />;
}
Agora, qualquer mudança de AppContext
não renderizará novamente ThemeContext
consumidores.
Esta é a solução preferida. Então você não precisa de nenhum resgate especial.
memo
entreSe por algum motivo você não pode separar contextos, você ainda pode otimizar a renderização dividindo um componente em dois e passando acessórios mais específicos para o interno. Você ainda renderizaria o externo, mas deve ser barato, pois não faz nada.
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme; // Your "selector"
return <ThemedButton theme={theme} />
}
const ThemedButton = memo(({ theme }) => {
// The rest of your rendering logic
return <ExpensiveTree className={theme} />;
});
useMemo
dentroFinalmente, poderíamos tornar nosso código um pouco mais detalhado, mas mantê-lo em um único componente envolvendo o valor de retorno em useMemo
e especificando suas dependências. Nosso componente ainda seria reexecutado, mas o React não renderizaria novamente a árvore filha se todas as entradas useMemo
fossem iguais.
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme; // Your "selector"
return useMemo(() => {
// The rest of your rendering logic
return <ExpensiveTree className={theme} />;
}, [theme])
}
Pode haver mais soluções no futuro, mas é o que temos agora.
Ainda assim, observe que a opção 1 é preferível - se algum contexto mudar com muita frequência, considere dividi-lo .
Ambas as opções irão escapar da renderização dos filhos se o tema não tiver mudado.
@gaearon Os botões são filhos ou os botões são renderizados? Estou faltando algum contexto de como eles são usados.
Usar a opção 2 unstable_Profiler
2 ainda acionará onRender
callbacks, mas não chamará a lógica de renderização real. Talvez eu esteja fazendo algo errado? ~ https://codesandbox.io/s/kxz4o2oyoo~ https://codesandbox.io/s/00yn9yqzjw
Eu atualizei o exemplo para ficar mais claro.
Usar a opção 2 do unstable_Profiler ainda acionará retornos de chamada onRender, mas não chamará a lógica de renderização real. Talvez eu esteja fazendo algo errado? https://codesandbox.io/s/kxz4o2oyoo
Esse é exatamente o objetivo dessa opção. :-)
Talvez uma boa solução para isso seja ter a possibilidade de "pegar" o contexto e renderizar novamente o componente apenas se o retorno de chamada for verdadeiro, por exemplo:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));
O principal problema com os ganchos que realmente encontrei é que não podemos gerenciar de dentro de um gancho o que é retornado por um componente - para evitar a renderização, retornar valor memorizado etc.
Se pudéssemos, não seria combinável.
https://overreacted.io/why-isnt-xa-hook/#not -a-hook-usebailout
Opção 4: não use contexto para propagação de dados, mas assinatura de dados. Use useSubscription (porque é difícil escrever para cobrir todos os casos).
Existe outra maneira de evitar a renderização novamente.
"Você precisa mover o JSX um nível acima do componente de re-renderização, então ele não será recriado a cada vez"
Talvez uma boa solução para isso seja ter a possibilidade de "pegar" o contexto e renderizar novamente o componente apenas se o retorno de chamada for verdadeiro, por exemplo:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));
O principal problema com os ganchos que realmente encontrei é que não podemos gerenciar de dentro de um gancho o que é retornado por um componente - para evitar a renderização, retornar valor memorizado etc.
Em vez de verdadeiro / falso aqui ... poderíamos fornecer uma função baseada em identidade que nos permitisse subdividir os dados do contexto?
const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])
onde useContext não apareceria neste componente, a menos que o resultado do seletor fn fosse diferente do resultado anterior da mesma função.
descobriram que essa biblioteca pode ser a solução para o Facebook se integrar aos ganchos https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/
Existe outra maneira de evitar a renderização novamente.
"Você precisa mover o JSX um nível acima do componente de re-renderização, então ele não será recriado a cada vez"
O problema é que pode ser caro reestruturar a árvore de componentes apenas para evitar uma nova renderização de cima para baixo.
@fuleinist Em
@gaearon Não sei se estou faltando alguma coisa, mas tentei a sua segunda e terceira opções e elas não estão funcionando corretamente. Não tenho certeza se isso é apenas reagir ao bug da extensão do Chrome ou se há outro problema. Aqui está meu exemplo simples de formulário, onde vejo renderizar novamente as duas entradas. No console, vejo que o memo está fazendo seu trabalho, mas o DOM é renderizado o tempo todo. Eu tentei 1000 itens e o evento onChange é muito lento, é por isso que acho que memo () não está funcionando com o contexto corretamente. Obrigado por qualquer conselho:
Aqui está uma demonstração com 1000 itens / caixas de texto. Mas, nessa demonstração, as ferramentas de desenvolvimento não mostram uma nova renderização. Você deve baixar fontes locais para testá-lo: https://codesandbox.io/embed/zen-firefly-d5bxk
import React, { createContext, useState, useContext, memo } from "react";
const FormContext = createContext();
const FormProvider = ({ initialValues, children }) => {
const [values, setValues] = useState(initialValues);
const value = {
values,
setValues
};
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};
const TextField = memo(
({ name, value, setValues }) => {
console.log(name);
return (
<input
type="text"
value={value}
onChange={e => {
e.persist();
setValues(prev => ({
...prev,
[name]: e.target.value
}));
}}
/>
);
},
(prev, next) => prev.value === next.value
);
const Field = ({ name }) => {
const { values, setValues } = useContext(FormContext);
const value = values[name];
return <TextField name={name} value={value} setValues={setValues} />;
};
const App = () => (
<FormProvider initialValues={{ firstName: "Marr", lastName: "Keri" }}>
First name: <Field name="firstName" />
<br />
Last name: <Field name="lastName" />
</FormProvider>
);
export default App;
Por outro lado, essa abordagem sem contexto funciona corretamente, ainda em depuração é mais lenta do que eu esperava, mas pelo menos re-renderizar está ok
import React, { useState, memo } from "react";
import ReactDOM from "react-dom";
const arr = [...Array(1000).keys()];
const TextField = memo(
({ index, value, onChange }) => (
<input
type="text"
value={value}
onChange={e => {
console.log(index);
onChange(index, e.target.value);
}}
/>
),
(prev, next) => prev.value === next.value
);
const App = () => {
const [state, setState] = useState(arr.map(x => ({ name: x })));
const onChange = (index, value) =>
setState(prev => {
return prev.map((item, i) => {
if (i === index) return { name: value };
return item;
});
});
return state.map((item, i) => (
<div key={i}>
<TextField index={i} value={item.name} onChange={onChange} />
</div>
));
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
@marrkeri Não vejo nada de errado no primeiro trecho de código. O componente destacado nas ferramentas de desenvolvimento é o Field
que usa o contexto, não o TextField
que é um componente memo e implementa a função areEqual.
Acho que o problema de desempenho no exemplo codesandbox vem dos 1000 componentes que usam o contexto. Refatore-o para um componente que usa o contexto, digamos Fields
, e retorne desse componente (com um mapa) um TextField
para cada valor.
@marrkeri Não vejo nada de errado no primeiro trecho de código. O componente destacado nas ferramentas de desenvolvimento é o
Field
que usa o contexto, não oTextField
que é um componente memo e implementa a função areEqual.Acho que o problema de desempenho no exemplo codesandbox vem dos 1000 componentes que usam o contexto. Refatore-o para um componente que usa o contexto, digamos
Fields
, e retorne desse componente (com um mapa) umTextField
para cada valor.
Como você disse, eu estava pensando que
Eu não entendi seu segundo ponto sobre a refatoração para um componente
@marrkeri Eu estava sugerindo algo assim: https://codesandbox.io/s/little-night-p985y.
É esta a razão pela qual o react-redux teve que parar de usar os benefícios da API de contexto estável e passar o estado atual para o contexto quando eles migraram para os Ganchos?
Então parece que há
Pode ser a única opção se você não controlar os usos (necessário para as opções 2-3) e não puder enumerar todos os seletores possíveis (necessário para a opção 1), mas ainda deseja expor uma API Hooks
const MyContext = createContext()
export const Provider = ({children}) => (
<MyContext.provider value={{subscribe: listener => ..., getValue: () => ...}}>
{children}
</MyContext.provider>
)
export const useSelector = (selector, equalityFunction = (a, b) => a === b) => {
const {subscribe, getValue} = useContext(MyContext)
const [value, setValue] = useState(getValue())
useEffect(() => subscribe(state => {
const newValue = selector(state)
if (!equalityFunction(newValue, value) {
setValue(newValue)
}
}), [selector, equalityFunction])
}
@Hypnosphi : paramos de passar o estado da loja no contexto (a implementação da v6) e voltamos para as assinaturas diretas da loja (a implementação da v7) devido a uma combinação de problemas de desempenho e a incapacidade de escapar das atualizações causadas pelo contexto (que o tornou impossível criar uma API de ganchos React-Redux baseada na abordagem v6).
Para mais detalhes, veja meu post A História e Implementação do React-Redux .
Eu li o tópico, mas ainda fico pensando - são as únicas opções disponíveis hoje para renderizar condicionalmente na mudança de contexto, "Opções 1 2 3 4" listadas acima? Há algo mais em andamento para resolver isso oficialmente ou as "4 soluções" são consideradas aceitáveis o suficiente?
Eu escrevi no outro segmento, mas apenas no caso. Aqui está uma solução alternativa _não oficial_.
useContextSelector proposta e use-context-selector library no userland.
honestamente, isso me faz pensar que a estrutura simplesmente não está pronta para entrar totalmente em funções e ganchos como se estivéssemos sendo encorajados. Com classes e métodos de ciclo de vida você tinha uma maneira organizada de controlar essas coisas e agora com ganchos é uma sintaxe muito pior e menos legível
A opção 3 não parece funcionar. Estou fazendo algo errado? https://stackblitz.com/edit/react-w8gr8z
@Martin , não concordo com isso.
O padrão de gancho pode ser lido com documentação e código bem organizados
estrutura e classes React e ciclo de vida são todos substituíveis por
equivalentes.
Infelizmente, o padrão reativo não pode ser alcançado pelo React ao longo da
classes React ou ganchos.
No sábado, 11 de janeiro de 2020 às 9h44 Martin Genev [email protected]
escreveu:
honestamente, isso me faz pensar que a estrutura não está pronta para funcionar
totalmente em funções e ganchos como se estivéssemos sendo encorajados. Com aulas
e métodos de ciclo de vida, você tinha uma maneira organizada de controlar essas coisas e
agora com ganchos é uma sintaxe muito pior e menos legível-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/facebook/react/issues/15156?email_source=notifications&email_token=AAI4DWUJ7WUXMHAR6F2KVXTQ5D25TA5CNFSM4G7UEEO2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEIVN6OI#issuecomment-573235001 ,
ou cancelar
https://github.com/notifications/unsubscribe-auth/AAI4DWUCO7ORHV5OSDCE35TQ5D25TANCNFSM4G7UEEOQ
.
@mgenev Opção 3 impede a nova renderização do filho ( <ExpensiveTree />
, o nome fala por si)
@Hypnosphi obrigado, isso estava correto.
https://stackblitz.com/edit/react-ycfyye
Eu reescrevi e agora os componentes reais de renderização (exibição) não renderizam novamente, mas todos os contêineres (contexto conectado) renderizam em qualquer mudança de suporte no contexto, não importa se está em uso neles. Agora, a única opção que posso ver é começar a dividir os contextos, mas algumas coisas são realmente globais e envolvem no nível mais alto e uma mudança em qualquer suporte nelas fará com que todos os contêineres sejam acionados em todo o aplicativo, eu não não entendo como isso pode ser bom para o desempenho ...
Existe algum lugar onde haja um bom exemplo de como fazer isso com bom desempenho? Os documentos oficiais são realmente limitados
Você pode tentar minha opção 4, que é basicamente o que o react-redux faz internamente
https://github.com/facebook/react/issues/15156#issuecomment -546703046
Para implementar a função subscribe
, você pode usar algo como Observables ou EventEmitter, ou apenas escrever uma lógica de assinatura básica você mesmo:
function StateProvider({children}) {
const [state, dispatch] = useReducer(reducer, initialState)
const listeners = useRef([])
const subscribe = listener => {
listeners.current.push(listener)
}
useEffect(() => {
listeners.current.forEach(listener => listener(state)
}, [state])
return (
<DispatchContext.Provider value={dispatch}>
<SubscribeContext.Provider value={{subscribe, getValue: () => state}}>
{children}
</SubscribeContext.Provider>
</DispatchContext.Provider>
);
}
Para aqueles que podem ter interesses, aqui está a comparação de várias bibliotecas um tanto relacionadas a este tópico.
https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode
Meu último esforço é verificar o suporte da ramificação de estado com useTransition
que seria importante no próximo Modo Simultâneo.
Basicamente, se usarmos o estado e contexto React de uma forma normal, está tudo bem. (caso contrário, precisamos de um truque, por exemplo, este .)
Obrigado @ dai-shi, realmente gosto dos seus pacotes e estou pensando em adotá-los.
@ dai-shi oi, Acabei de encontrar seu react-tracked
lib e parece muito bom se resolve problemas de desempenho com contextos como promete. Ainda é real ou melhor usar outra coisa? Aqui eu vejo um bom exemplo de seu uso também mostra como fazer o nível de middleware com use-reducer-async
https://github.com/dai-shi/react-tracked/blob/master/examples/12_async/src/store .ts Obrigado por isso. Atualmente eu fiz algo semelhante usando useReducer
, envolvendo dispatch
com o meu próprio para middleware assíncrono e usando Context
mas me preocupando com futuros problemas de desempenho de renderização devido ao envolvimento de contextos.
@bobrosoft react-tracked
é bastante estável, eu diria (como um produto de desenvolvedor com certeza). O feedback é muito bem-vindo e é assim que uma biblioteca seria melhorada. Atualmente, ele usa internamente um recurso não documentado do React, e espero que possamos substituí-lo por um primitivo melhor no futuro. use-reducer-async
é quase como um açúcar de sintaxe simples que nunca daria errado.
Este HoC funcionou para mim:
`` ``
import React, {useMemo, ReactElement, FC} de 'react';
importar reduzir de 'lodash / reduzir';
tipo Seletor = (contexto: qualquer) => qualquer;
interface SelectorObject {
}
const withContext = (
Componente: FC,
Contexto: qualquer,
seletores: SelectorObject,
): FC => {
return (props: any): ReactElement => {
const Consumer = ({context}: any): ReactElement => {
const contextProps = reduzir (
seletores,
(acc: any, selector: Selector, key: string): any => {
valor const = seletor (contexto);
acc [chave] = valor;
return acc;
},
{},
);
return useMemo (
(): ReactElement =>
[... Object.values (props), ... Object.values (contextProps)],
);
};
Retorna (
{(contexto: qualquer): ReactElement =>
);
};
};
exportação padrão com Contexto;
`` ``
exemplo de uso:
export default withContext(Component, Context, {
value: (context): any => context.inputs.foo.value,
status: (context): any => context.inputs.foo.status,
});
isso pode ser visto como o contexto equivalente de redux mapStateToProps
Fiz um hoc quase muito parecido com connect () no redux
const withContext = (
context = createContext(),
mapState,
mapDispatchers
) => WrapperComponent => {
function EnhancedComponent(props) {
const targetContext = useContext(context);
const { ...statePointers } = mapState(targetContext);
const { ...dispatchPointers } = mapDispatchers(targetContext);
return useMemo(
() => (
<WrapperComponent {...props} {...statePointers} {...dispatchPointers} />
),
[
...Object.values(statePointers),
...Object.values(props),
...Object.values(dispatchPointers)
]
);
}
return EnhancedComponent;
};
Implementação:
const mapActions = state => {
return {};
};
const mapState = state => {
return {
theme: (state && state.theme) || ""
};
};
export default connectContext(ThemeContext, mapState, mapActions)(Button);
Atualização: por fim, mudei para EventEmitter para dados granulares de mudança rápida com ouvintes dinâmicos (no movimento do mouse). Percebi que era a melhor ferramenta para o trabalho. O contexto é ótimo para compartilhar dados em geral, mas não em altas taxas de atualização.
Não se trata de uma assinatura declarativa ou não de um contexto? Encapsular condicionalmente um componente em outro que use useContext ().
O principal requisito é que o componente interno não possa ter estado, já que efetivamente será uma instância diferente por causa da ramificação. Ou talvez você possa pré-renderizar o componente e usar cloneElement para atualizar os adereços.
Alguns componentes não têm nada a ver com o contexto em sua renderização, mas precisam "apenas ler um valor" a partir dele. o que estou perdendo?
O Context._currentValue é seguro para uso na produção?
Estou tentando inscrever componentes em contextos que são tão baratos para renderizar quanto possível. Mas então me descobri passando adereços como antigamente com um código sphagetti ou usando memo para evitar a assinatura de atualizações quando não há nada lógico com as atualizações. As soluções de memorando são boas para quando a renderização de seu componente depende de contextos, mas do contrário ...
@ vamshi9666 você usa muito isso? você percebeu os prós / contras de sua abordagem? Eu gosto muito. Eu gosto da semelhança com redux, mas também como ele libera você para escrever gerenciamento de estado de aplicativo e lógica livremente como um contexto
Achei https://recoiljs.org/ uma boa solução para esse problema. Acho que seria incrível se você integrá-lo ao React.
@ vamshi9666 você usa muito isso? você percebeu os prós / contras de sua abordagem? Eu gosto muito. Eu gosto da semelhança com redux, mas também como ele libera você para escrever gerenciamento de estado de aplicativo e lógica livremente como um contexto
Usei-o apenas em um lugar e meu objetivo inicial é isolar o gerenciamento de estado do jsx, mas também não criar um não padrão. então, quando eu estendi a funcionalidade de mutações, parecia muito semelhante ao redutor do redux e ao padrão de criador de ação. O que é ainda melhor. mas não vejo motivo para reinventar algo, quando na verdade já existe.
Comentários muito úteis
Isso está funcionando conforme o planejado. Há uma discussão mais longa sobre isso em https://github.com/facebook/react/issues/14110 se você estiver curioso.
Digamos que por algum motivo você tenha
AppContext
cujo valor tem uma propriedadetheme
, e deseja apenas renderizar novamente algumasExpensiveTree
emappContextValue.theme
alterações.TLDR é que, por enquanto, você tem três opções:
Opção 1 (preferencial): Divida contextos que não mudam juntos
Se precisarmos apenas de
appContextValue.theme
em muitos componentes, mas o próprioappContextValue
muda com muita frequência, poderíamos dividirThemeContext
deAppContext
.Agora, qualquer mudança de
AppContext
não renderizará novamenteThemeContext
consumidores.Esta é a solução preferida. Então você não precisa de nenhum resgate especial.
Opção 2: Divida seu componente em dois, coloque
memo
entreSe por algum motivo você não pode separar contextos, você ainda pode otimizar a renderização dividindo um componente em dois e passando acessórios mais específicos para o interno. Você ainda renderizaria o externo, mas deve ser barato, pois não faz nada.
Opção 3: um componente com
useMemo
dentroFinalmente, poderíamos tornar nosso código um pouco mais detalhado, mas mantê-lo em um único componente envolvendo o valor de retorno em
useMemo
e especificando suas dependências. Nosso componente ainda seria reexecutado, mas o React não renderizaria novamente a árvore filha se todas as entradasuseMemo
fossem iguais.Pode haver mais soluções no futuro, mas é o que temos agora.
Ainda assim, observe que a opção 1 é preferível - se algum contexto mudar com muita frequência, considere dividi-lo .