Estoy tratando de escribir ganchos básicos para obtener currentScroll, lastScroll, scrollSpeed mientras se desplaza.

function useDocScroll() {
  const isClient = typeof window === "object"

  function getScroll() {
    return isClient
      ? window.pageYOffset || document.documentElement.scrollTop
      : undefined
  }

  const [docScroll, setDocScroll] = useState(getScroll)
  const [lastScroll, setLastScroll] = useState(null)
  const [scrollSpeed, setScrollSpeed] = useState(Math.abs(docScroll - lastScroll))

  useEffect(() => {
    if (!isClient) {
      return false
    }

    function handleScroll() {
      setDocScroll(getScroll())
      setLastScroll(getScroll())  // <-- why is this working?
      // setLastScroll(docScroll) // <-- why is this not working?

      setScrollSpeed(Math.abs(docScroll - lastScroll)) // <-- why is this not working?
    }

    window.addEventListener("scroll", handleScroll)
  }, [])

  return [docScroll, lastScroll, scrollSpeed]
}

Parece que cuando hago setLastScroll(getScroll()), guarda bien el último valor de desplazamiento.

Pero no entiendo porque cuando handleScroll() se está disparando, ¿no debería el valor getScroll() permanecer igual? No entiendo por qué setDocScroll(getScroll()) y setLastScroll(getScroll()) tienen un valor diferente.

Además, pensé que podría hacer setLastScroll(docScroll), que significa 'establecer el valor lastScroll con el valor docScroll actual', pero solo imprime '0' mientras cambia el valor docScroll.

¿Por qué es esto? Quiero entender mejor

+) Y no puedo obtener scrollSpeed que se calcula con docScroll y lastScroll, pero no sé cómo obtener esos valores.

Edit distracted-golick-c89w3

0
Jaye Park 30 dic. 2019 a las 12:35

2 respuestas

La mejor respuesta

Creo que su código no funciona por las siguientes dos razones:

  1. usar docScroll directamente después de setDocScroll no funcionará porque setState es una tarea asincrónica. No hay garantía de que docScroll se actualice antes de ejecutar la siguiente declaración
  2. obtienes 0 debido al desplazamiento dentro de algún elemento en particular (probablemente). Dado que document.documentElement apunta al elemento html y no hay desplazamiento dentro de él. Entonces recibes 0

Solución:

No necesita múltiples useState s. Dado que los eventos de desplazamiento se emiten con demasiada frecuencia, creo que es una buena idea usar useReducer para reducir el número de representaciones. Es importante comprender dónde se produce el desplazamiento, ya sea a nivel raíz o dentro de algún elemento.

Para la solución a continuación, propuse:

Si el desplazamiento ocurre en el nivel raíz (elemento html) no es necesario pasar el elemento a useDocScroll. Si el desplazamiento ocurre dentro de un elemento en particular, debe pasar la referencia del elemento.

const initState = {
  current: 0,
  last: 0,
  speed: 0,
};

function reducer(state, action) {
  switch (action.type) {
    case "update":
      return {
        last: state.current,
        current: action.payload,
        speed: Math.abs(action.payload - state.current) || 0,
      };
    default:
      return state;
  }
}

const isClient = () => typeof window === "object";
function useDocScroll(element = document.documentElement) {
  const [{ current, last, speed }, dispatch] = useReducer(reducer, initState);

  function getScroll() {
    return isClient() ? element.scrollTop : 0;
  }

  function handleScroll() {
    dispatch({ type: "update", payload: getScroll() });
  }

  useEffect(() => {
    if (!isClient()) {
      return false;
    }

    element.addEventListener("scroll", handleScroll);

    return () => element.removeEventListener("scroll", handleScroll);
  }, []);

  return [current, last, speed];
}

Ejemplo:

Si el desplazamiento ocurre dentro de la ventana

const {current, last, speed} = useDocScroll()

Si el desplazamiento ocurre en un elemento particular

const {current, last, speed} = useDocScroll(document.getElementById("main"))
1
REDDY PRASAD 30 dic. 2019 a las 10:40

No funciona debido a cierres:

useEffect(() => {
  if (!isClient) {
    return false;
  }

  function handleScroll() {
    setDocScroll(getScroll());

    // On calling handleScroll the values docScroll & lastScroll
    // are always will be of the first mount,
    // the listener "remembers" (closures) such values and never gets updated
    setLastScroll(docScroll);
    setScrollSpeed(Math.abs(docScroll - lastScroll));

    // v Gets updated on every call
    setLastScroll(getScroll());
  }

  // v Here you assigning a callback which closes upon the lexical scope of
  // docScroll and lastScroll
  window.addEventListener('scroll', handleScroll);
}, []);

Para solucionarlo, una posible solución puede ser una combinación de una referencia (useRef) y funcional setState.

Por ejemplo:

setScrollSpeed(lastScroll => Math.abs(getScroll() - lastScroll))

No estoy seguro de por qué lastScrolled es necesario:

function useDocScroll() {
  const [docScroll, setDocScroll] = useState(getScroll());
  const lastScrollRef = useRef(null);
  const [scrollSpeed, setScrollSpeed] = useState();

  useEffect(() => {
    if (!isClient) {
      return false;
    }

    function handleScroll() {
      const lastScroll = lastScrollRef.current;
      const curr = getScroll();
      setScrollSpeed(Math.abs(getScroll() - (lastScroll || 0)));
      setDocScroll(curr);

      // Update last
      lastScrollRef.current = curr;
    }

    window.addEventListener('scroll', handleScroll);
  }, []);

  return [docScroll, lastScrollRef.current, scrollSpeed];
}

Edit hidden-architecture-p8xbi

0
Dennis Vash 30 dic. 2019 a las 10:14