React: Prevención de reproducciones con React.memo y useContext hook.

Creado en 19 mar. 2019  ·  37Comentarios  ·  Fuente: facebook/react

¿Quieres solicitar una función o informar de un error ?

bicho

¿Cuál es el comportamiento actual?

No puedo confiar en los datos de la API de contexto usando (useContext hook) para evitar reproducciones innecesarias con React.memo

Si el comportamiento actual es un error, proporcione los pasos para reproducir y, si es posible, una demostración mínima del problema. Pegue el enlace a su JSFiddle (https://jsfiddle.net/Luktwrdm/) o CodeSandbox (https://codesandbox.io/s/new) ejemplo a continuación:

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

});

¿Cuál es el comportamiento esperado?

Debería tener acceso de alguna manera al contexto en la devolución de llamada del segundo argumento de React.memo para evitar la representación
O debería tener la posibilidad de devolver una instancia antigua del componente react en el cuerpo de la función.

¿Qué versiones de React y qué navegador / sistema operativo se ven afectados por este problema?
16.8.4

Comentario más útil

Esto está funcionando según lo diseñado. Hay una discusión más larga sobre esto en https://github.com/facebook/react/issues/14110 si tiene curiosidad.

Digamos que por alguna razón tienes AppContext cuyo valor tiene una propiedad theme , y solo quieres volver a renderizar algunos ExpensiveTree en appContextValue.theme cambios.

TLDR es que por ahora tienes tres opciones:

Opción 1 (preferida): contextos divididos que no cambian juntos

Si solo necesitamos appContextValue.theme en muchos componentes pero appContextValue cambia con demasiada frecuencia, podríamos dividir ThemeContext de AppContext .

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Ahora, cualquier cambio de AppContext no volverá a representar ThemeContext consumidores.

Esta es la solución preferida. Entonces no necesita ningún rescate especial.

Opción 2: Divida su componente en dos, coloque memo entre

Si por alguna razón no puede dividir los contextos, aún puede optimizar el renderizado dividiendo un componente en dos y pasando más accesorios específicos al interior. Aún renderizaría el exterior, pero debería ser barato ya que no hace 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} />;
});

Opción 3: Un componente con useMemo adentro

Finalmente, podríamos hacer nuestro código un poco más detallado pero mantenerlo en un solo componente envolviendo el valor de retorno en useMemo y especificando sus dependencias. Nuestro componente aún se volvería a ejecutar, pero React no volvería a representar el árbol secundario si todas las entradas useMemo son iguales.

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])
}

Puede que haya más soluciones en el futuro, pero esto es lo que tenemos ahora.

Aún así, tenga en cuenta que la opción 1 es preferible: si algún contexto cambia con demasiada frecuencia, considere dividirlo .

Todos 37 comentarios

Esto está funcionando según lo diseñado. Hay una discusión más larga sobre esto en https://github.com/facebook/react/issues/14110 si tiene curiosidad.

Digamos que por alguna razón tienes AppContext cuyo valor tiene una propiedad theme , y solo quieres volver a renderizar algunos ExpensiveTree en appContextValue.theme cambios.

TLDR es que por ahora tienes tres opciones:

Opción 1 (preferida): contextos divididos que no cambian juntos

Si solo necesitamos appContextValue.theme en muchos componentes pero appContextValue cambia con demasiada frecuencia, podríamos dividir ThemeContext de AppContext .

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Ahora, cualquier cambio de AppContext no volverá a representar ThemeContext consumidores.

Esta es la solución preferida. Entonces no necesita ningún rescate especial.

Opción 2: Divida su componente en dos, coloque memo entre

Si por alguna razón no puede dividir los contextos, aún puede optimizar el renderizado dividiendo un componente en dos y pasando más accesorios específicos al interior. Aún renderizaría el exterior, pero debería ser barato ya que no hace 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} />;
});

Opción 3: Un componente con useMemo adentro

Finalmente, podríamos hacer nuestro código un poco más detallado pero mantenerlo en un solo componente envolviendo el valor de retorno en useMemo y especificando sus dependencias. Nuestro componente aún se volvería a ejecutar, pero React no volvería a representar el árbol secundario si todas las entradas useMemo son iguales.

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])
}

Puede que haya más soluciones en el futuro, pero esto es lo que tenemos ahora.

Aún así, tenga en cuenta que la opción 1 es preferible: si algún contexto cambia con demasiada frecuencia, considere dividirlo .

Ambas opciones evitarán la reproducción de niños si el tema no ha cambiado.

@gaearon ¿Los botones son hijos o los botones representan hijos? Me falta algo de contexto sobre cómo se usan.

El uso de la opción 2 unstable_Profiler seguirá activando onRender devoluciones https://codesandbox.io/s/kxz4o2oyoo~ https://codesandbox.io/s/00yn9yqzjw

Actualicé el ejemplo para que fuera más claro.

El uso de la opción 2 de unstable_Profiler aún activará las devoluciones de llamada de onRender pero no llamará a la lógica de renderización real. ¿Quizás estoy haciendo algo mal? https://codesandbox.io/s/kxz4o2oyoo

Ese es exactamente el punto de esa opción. :-)

Tal vez una buena solución para eso sería tener la posibilidad de "tomar" el contexto y volver a renderizar el componente solo si la devolución de llamada devuelve verdadero, por ejemplo:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

El principal problema con los ganchos que conocí es que no podemos administrar desde el interior de un gancho lo que devuelve un componente, para evitar el renderizado, devolver valor memorizado, etc.

Si pudiéramos, no sería componible.

https://overreacted.io/why-isnt-xa-hook/#not -a-hook-usebailout

Opción 4: no utilice el contexto para la propagación de datos, sino para la suscripción de datos. Utilice useSubscription (porque es difícil escribir para cubrir todos los casos).

Hay otra forma de evitar volver a renderizar.
"Necesita subir el JSX un nivel fuera del componente de renderizado, entonces no se volverá a crear cada vez"

Mas info aqui

Tal vez una buena solución para eso sería tener la posibilidad de "tomar" el contexto y volver a renderizar el componente solo si la devolución de llamada devuelve verdadero, por ejemplo:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

El principal problema con los ganchos que conocí es que no podemos administrar desde el interior de un gancho lo que devuelve un componente, para evitar el renderizado, devolver valor memorizado, etc.

En lugar de un verdadero / falso aquí ... ¿podríamos proporcionar una función basada en la identidad que nos permitiera dividir en subconjuntos los datos del contexto?

const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])

donde useContext no aparecería en este componente a menos que el resultado del selector fn fuera diferente en cuanto a identidad del resultado anterior de la misma función.

encontré esta biblioteca que puede ser la solución para que Facebook se integre con los ganchos https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/

Hay otra forma de evitar volver a renderizar.
"Necesita subir el JSX un nivel fuera del componente de renderizado, entonces no se volverá a crear cada vez"

Mas info aqui

El problema es que puede ser costoso reestructurar el árbol de componentes solo para evitar que se vuelva a renderizar de arriba a abajo.

@fuleinist En

@gaearon No sé si me falta algo, pero probé la segunda y tercera opciones y no funcionan correctamente. No estoy seguro de si esto es solo un error de extensión de Chrome de reacción o si hay otro problema. Aquí está mi ejemplo simple de forma, donde veo volver a reproducir ambas entradas. En la consola, veo que la nota está haciendo su trabajo, pero DOM se vuelve a generar todo el tiempo. Probé 1000 elementos y el evento onChange es realmente lento, por eso creo que memo () no funciona correctamente con el contexto. Gracias por cualquier consejo:

Aquí hay una demostración con 1000 elementos / cuadros de texto. Pero en esa demostración, las herramientas de desarrollo no muestran la repetición. Tienes que descargar fuentes en local para probarlo: 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;

image

Por otro lado, este enfoque sin contexto funciona correctamente, aún en depuración es más lento de lo que esperaba, pero al menos volver a renderizar está bien

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);

image

@marrkeri No veo nada malo en el primer fragmento de código. El componente que se destaca en las herramientas de desarrollo es el Field que usa el contexto, no el TextField que es un componente memo e implementa la función areEqual.

Creo que el problema de rendimiento en el ejemplo de codesandbox proviene de los 1000 componentes que usan el contexto. Refactorice a un componente que use el contexto, digamos Fields , y devuelva de ese componente (con un mapa) un TextField para cada valor.

@marrkeri No veo nada malo en el primer fragmento de código. El componente que se destaca en las herramientas de desarrollo es el Field que usa el contexto, no el TextField que es un componente memo e implementa la función areEqual.

Creo que el problema de rendimiento en el ejemplo de codesandbox proviene de los 1000 componentes que usan el contexto. Refactorice a un componente que use el contexto, digamos Fields , y devuelva de ese componente (con un mapa) un TextField para cada valor.

Como dijiste, estaba bajo el mismo pensamiento que debe volver a reproducirse cada vez, pero ( ) solo cuando se cambia el valor. Pero probablemente solo sean problemas con las herramientas de desarrollo porque agregué relleno y en lugar de eso se vuelve a reproducir. Mira esta imagen
image

image

No he captado su segundo punto sobre la refactorización en un componente . ¿Podrías hacer una instantánea por favor? ¿Y qué piensan sobre el número máximo mostrado de ¿Cuáles están bien sin retrasos? ¿1000 es demasiado?

@marrkeri Estaba sugiriendo algo como esto: https://codesandbox.io/s/little-night-p985y.

¿Es esta la razón por la que react-redux tuvo que dejar de usar los beneficios de la API de contexto estable y pasar el estado actual al contexto cuando migraron a Hooks?

Entonces parece que hay

Opción 4: pasar una función de suscripción como valor de contexto, como en la era del contexto heredado

Podría ser la única opción si no controla los usos (necesarios para las opciones 2-3) y no puede enumerar todos los selectores posibles (necesarios para la opción 1), pero aún desea exponer una API de 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 : dejamos de pasar el estado de la tienda en contexto (la implementación v6) y volvimos a las suscripciones directas a la tienda (la implementación v7) debido a una combinación de problemas de rendimiento y la incapacidad de rescatar las actualizaciones causadas por el contexto (lo que lo hizo imposible crear una API de enlaces React-Redux basada en el enfoque v6).

Para obtener más detalles, consulte mi publicación La historia y la implementación de React-Redux .

Leí el hilo, pero todavía me quedo preguntándome: ¿son las únicas opciones disponibles hoy para volver a renderizar condicionalmente en el cambio de contexto, "Opciones 1 2 3 4" enumeradas anteriormente? ¿Se está trabajando en algo más para abordar oficialmente esto o las "4 soluciones" se consideran suficientemente aceptables?

Escribí en el otro hilo, pero por si acaso. Aquí hay una solución alternativa _ no oficial_.
propuesta de useContextSelector y biblioteca use-context-selector en el área de usuario.

Honestamente, esto me hace pensar que el marco no está listo para entrar completamente en funciones y ganchos como nos están animando. Con las clases y los métodos del ciclo de vida, tenías una forma ordenada de controlar estas cosas y ahora con los ganchos es una sintaxis mucho peor y menos legible.

La opción 3 no parece funcionar. ¿Estoy haciendo algo mal? https://stackblitz.com/edit/react-w8gr8z

@Martin , no estoy de acuerdo con eso.

El patrón de gancho se puede leer con documentación y código bien organizados
La estructura y las clases de React y el ciclo de vida son reemplazables por funcional
equivalentes.

Desafortunadamente, el patrón reactivo no se puede lograr con React a través de
ya sea clases React o hooks.

El sábado 11 de enero de 2020 a las 9:44 a. M. Martin Genev [email protected]
escribió:

Honestamente, esto me hace pensar que el marco no está listo para funcionar.
completamente en funciones y ganchos como nos están animando. Con clases
y métodos del ciclo de vida, tenía una forma ordenada de controlar estas cosas y
ahora con ganchos es una sintaxis mucho peor menos legible

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/facebook/react/issues/15156?email_source=notifications&email_token=AAI4DWUJ7WUXMHAR6F2KVXTQ5D25TA5CNFSM4G7UEEO2YY3PNVWWK3TUL52HS4DFVREXG43WWIMVN5 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AAI4DWUCO7ORHV5OSDCE35TQ5D25TANCNFSM4G7UEEOQ
.

@mgenev La opción 3 evita que se vuelva a renderizar el niño ( <ExpensiveTree /> , el nombre habla por sí mismo)

@Hypnosphi gracias, eso fue correcto.
https://stackblitz.com/edit/react-ycfyye

Reescribí y ahora los componentes de renderizado (visualización) reales no se vuelven a renderizar, pero todos los contenedores (conectados al contexto) se renderizan en cualquier cambio de prop en el contexto, sin importar si está en uso en ellos. Ahora, la única opción que puedo ver es comenzar a dividir los contextos, pero algunas cosas son verdaderamente globales y se ajustan al nivel más alto y un cambio en cualquier accesorio en ellos hará que todos los contenedores se activen en toda la aplicación, no lo hago ' No entiendo cómo eso puede ser bueno para el rendimiento ...

¿Hay en algún lugar un buen ejemplo de cómo hacer esto de una manera eficaz? Los documentos oficiales son realmente limitados

Puedes probar mi opción 4, que es básicamente lo que reacciona-redux hace internamente
https://github.com/facebook/react/issues/15156#issuecomment -546703046

Para implementar la función subscribe , puede usar algo como Observables o EventEmitter, o simplemente escribir una lógica de suscripción básica usted mismo:

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 aquellos que puedan tener intereses, aquí está la comparación de varias bibliotecas algo relacionadas con este tema.
https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode

Mi último esfuerzo es verificar el soporte de la ramificación estatal con useTransition que sería importante en el próximo Modo Concurrente.
Básicamente, si usamos el estado y el contexto de React de forma normal, está bien. (de lo contrario, necesitamos un truco, por ejemplo, este ).

Gracias @ dai-shi Me gustan mucho sus paquetes y estoy pensando en adoptarlos.

@ dai-shi hola, acabo de encontrar tu react-tracked lib y se ve muy bien si resuelve problemas de rendimiento con contextos como promete. ¿Sigue siendo actual o es mejor usar otra cosa? Aquí veo un buen ejemplo de su uso y también muestra cómo hacer un nivel de middleware con use-reducer-async https://github.com/dai-shi/react-tracked/blob/master/examples/12_async/src/store .ts Gracias por ello. Actualmente he hecho algo similar usando useReducer , envolviendo dispatch con el mío propio para middleware asíncrono y usando Context pero preocupándome por problemas de rendimiento de renderizado futuros debido a la envoltura de contextos.

@bobrosoft react-tracked es bastante estable, diría yo (como un producto de desarrollador seguro). Los comentarios son bienvenidos y así es como se mejoraría una biblioteca. Actualmente, utiliza internamente una característica indocumentada de React, y espero que podamos reemplazarla con una primitiva mejor en el futuro. use-reducer-async es casi como un azúcar de sintaxis simple que nunca saldría mal.

Este HoC funcionó para mí:
`` ``
importar React, {useMemo, ReactElement, FC} desde 'reaccionar';
importar reducir de 'lodash / reducir';

selector de tipo = (contexto: cualquiera) => cualquiera;

interface SelectorObject {
}

const withContext = (
Componente: FC,
Contexto: cualquiera,
selectores: SelectorObject,
): FC => {
return (props: any): ReactElement => {
Consumidor constante = ({contexto}: cualquiera): ReactElement => {
const contextProps = reducir (
selectores,
(acc: cualquiera, selector: selector, clave: cadena): cualquiera => {
valor constante = selector (contexto);
acc [clave] = valor;
retorno acc;
},
{},
);
return useMemo (
(): ReactElement => ,
[... Object.values ​​(props), ... Object.values ​​(contextProps)],
);
};
regreso (

{(contexto: cualquiera): ReactElement => }

);
};
};

exportar por defecto withContext;
`` ``

ejemplo de uso:

export default withContext(Component, Context, { value: (context): any => context.inputs.foo.value, status: (context): any => context.inputs.foo.status, });

esto podría verse como el equivalente de contexto de redux mapStateToProps

Hice un hoc casi muy similar a connect () en 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;
};

Implementación:

const mapActions = state => {
  return {};
};

const mapState = state => {
  return {
    theme: (state && state.theme) || ""
  };
};
export default connectContext(ThemeContext, mapState, mapActions)(Button);


Actualización: En última instancia, cambié a EventEmitter para obtener datos granulares que cambian rápidamente con oyentes dinámicos (al mover el mouse). Me di cuenta de que era la mejor herramienta para el trabajo. El contexto es ideal para compartir datos en general, pero no a altas frecuencias de actualización.

¿No se reduce a suscribirse declarativamente o no al contexto? Envolver condicionalmente un componente en otro que usa useContext ().

El requisito principal es que el componente interno no puede tener estado, ya que efectivamente será una instancia diferente debido a la ramificación. O tal vez pueda pre-renderizar el componente y luego usar cloneElement para actualizar los accesorios.

Algunos componentes no tienen nada que ver con el contexto en su renderizado, pero necesitan "simplemente leer un valor" de él. ¿Qué me estoy perdiendo?
context

¿ Es seguro utilizar

Estoy tratando de suscribir componentes a contextos que sean lo más baratos de representar posible. Pero luego me encontré pasando accesorios como de la manera antigua con un código sphagetti o usando memo para evitar suscribirme a actualizaciones cuando no hay nada lógico con las actualizaciones. Las soluciones de notas son buenas para cuando el renderizado de su componente depende de los contextos, pero por lo demás ...

@ vamshi9666 ¿lo has usado mucho? ¿Pudiste notar los pros / contras de tu enfoque? Me gusta mucho. Me gusta la similitud con redux, pero también cómo te libera para escribir la gestión del estado de la aplicación y la lógica libremente como contexto.

Encontré https://recoiljs.org/ una buena solución a este problema. Creo que sería genial si lo integraras en React.

@ vamshi9666 ¿lo has usado mucho? ¿Pudiste notar los pros / contras de tu enfoque? Me gusta mucho. Me gusta la similitud con redux, pero también cómo te libera para escribir la gestión del estado de la aplicación y la lógica libremente como contexto.

Lo usé solo en un lugar y mi objetivo inicial es aislar la administración del estado de jsx, pero tampoco crear un no repetitivo. así que cuando extendí la funcionalidad de mutaciones, se veía muy similar al patrón reductor y creador de acciones de redux. Que es incluso mejor. pero no veo el sentido de reinventar algo, cuando en realidad ya está ahí.

¿Fue útil esta página
0 / 5 - 0 calificaciones