简体   繁体   中英

Trying to use useRef to run a function on a generated item in React/Remix/Prisma

I've gone through multiple useRef/useEffect instructions but I just can't seem to make it work here.

The code workflow here is: Remix/React, get data from database, display data, turn data into a ticker that can be updated

If anyone could point out any glaring errors they see in this code as to why the useEffect hook isn't firing, or why the useRef hook can never find the {listRef} within the <ul> , I would love to know.

import { Links, redirect, useLoaderData, Outlet } from 'remix'
import { db } from '~/utils/db.server'
import { getUser } from '~/utils/session.server'
import { ReactSortable } from "react-sortablejs"
import { useState, useRef, useEffect } from 'react'
import tickerStylesUrl from '~/styles/tickerDisplay.css'

export const links = () => [{ rel: 'stylesheet', href: tickerStylesUrl }]

export const loader = async ({ request, params }) => {
  
  const user = await getUser(request)
  const ticker = await db.ticker.findUnique({
    where: { id: params.tickerId },
    include: {
      headlines: true,
    },
  })
  if (!ticker) throw new Error('Ticker not found')

  const data = { ticker, user }
  return data
}

export const action = async ({ request, params }) => {

}
// The ticker function displays the items without styling, so it finds the database perfectly and can get the data
function displayTicker() {
  const { ticker, user } = useLoaderData()

  const headlines = ticker.headlines
  const tickerParentStyle = {
    width: "1920px",
    height: "1080px",
    position: "relative",
    backgroundColor: "black"
  }
  const tickerStyle = {
    position: "absolute",
    padding: "0",
    bottom: "0",
    color: `${ticker.fontColor}`,
    backgroundColor: `${ticker.backgroundColor}`,
    fontFamily: `${ticker.font}`,
    fontSize: "2em",
  }
  const tickerHeadlineStyle = {
    margin: "auto",
    height: "50%",
  }
  console.log("Headlines: " + headlines)
  // So begins the found ticker code I had hoped to integrate
  // Source: https://www.w3docs.com/tools/code-editor/2123
  function scrollTicker() {

    const marquee = listRef.current.querySelectorAll('.tickerHeadlines');
    let speed = 4;
    let lastScrollPos = 0;
    let timer;
    marquee.forEach(function (el) {
      const container = el.querySelector('.headlineItem');
      const content = el.querySelector('.headlineItem > *');
      //Get total width
      const elWidth = content.offsetWidth;
      //Duplicate content
      let clone = content.cloneNode(true);
      container.appendChild(clone);
      let progress = 1;
      function loop() {
        progress = progress - speed;
        if (progress <= elWidth * -1) {
          progress = 0;
        }
        container.style.transform = 'translateX(' + progress + 'px)';
        container.style.transform += 'skewX(' + speed * 0.4 + 'deg)';
        window.requestAnimationFrame(loop);
      }
      loop();
    });
    window.addEventListener('scroll', function () {
      const maxScrollValue = 12;
      const newScrollPos = window.scrollY;
      let scrollValue = newScrollPos - lastScrollPos;
      if (scrollValue > maxScrollValue) scrollValue = maxScrollValue;
      else if (scrollValue < -maxScrollValue) scrollValue = -maxScrollValue;
      speed = scrollValue;
      clearTimeout(timer);
      timer = setTimeout(handleSpeedClear, 10);
    });
    function handleSpeedClear() {
      speed = 4;
    }
  }

  const listRef = useRef()
  console.log("listRef: " + JSON.stringify(listRef))
  // This console appears everytime, but is always empty, presumably because DOM has just rendered

  useEffect(() => {
    console.log("useEffect fired")
    // This console NEVER fires, sadly. I thought this would happen ONCE rendered
  }, [listRef]);

  return (
    <>
      <Links />
      <div style={tickerParentStyle}>
        <div style={tickerStyle}>
          <div key={ticker.id} style={tickerHeadlineStyle} class="tickerWrapper">
// HERE IS THE TARGET UL
            <ul className="tickerHeadlines" ref={listRef} style={{ margin: "10px 0 10px 0" }} >
              {/* Hoping to map through the ticker items here, and have them displayed in a list, which would then be manipulated by the useRef/useEffect hook */}
              {headlines.map((headline) => (
                <>
                  <li class="headlineItem" key={headline.id}>
                    <span>
                      {headline.content} {ticker.seperator}
                    </span>
                  </li>
                </>
              ))}
              {scrollTicker()}
            </ul>
          </div>
        </div>
      </div>
    </>
  )
}

export default displayTicker

As always, any help is appreciated.

useRef is a hook that is used to access DOM elements, manipulating the DOM directly in a React application breaks the whole point of declarative programming. It is not at all advised to manipulate DOM directly using any dom objects and methods such as document . Coming to the useEffect hook, the useEffect hook runs conditionally depending on what's supplied in the dependency array, if none, the hook runs only once after the component finishes mounting. So you should be careful regarding what needs to be passed to the useEffect dependency array. Considering your case, when you pass listRef , the useEffect runs only when there is a change in the object and not it's properties, because objects are non-primitive, any changes in the property is not treated as a change in the object, and its merely an object property mutation that doesn't cause re-render. To steer clear, you should be sure of, when exactly you want it to be invoked, as you mentioned, you'd want it to run right after the data has rendered, you could instead use headlines in your dependency array. Change the dependency array to include headlines .

 useEffect(() => {
    console.log("useEffect fired")
    // This console NEVER fires, sadly. I thought this would happen ONCE rendered
  }, [headlines]);

Alternatively, you could also leave it empty, making it run only once after the component has mounted.

useEffect(() => {
    console.log("useEffect fired")
    // This console NEVER fires, sadly. I thought this would happen ONCE rendered
  }, []);

A caveat, the former snippet would run every time there's a change in headlines , and the latter would run only once no matter what changes. So, depending on your use case, you might want to choose the one that best suits your needs.

Your need to capitalize you displayTicker otherwise it won't be considered as a React Component by react and your hooks might not work.

Note: Always start component names with a capital letter. React treats components starting with lowercase letters as DOM tags. For example, represents an HTML div tag, but represents a component and requires Welcome to be in scope.

https://reactjs.org/docs/components-and-props.html#rendering-a-component

https://reactjs.org/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized

Don't call Hooks from regular JavaScript functions. Instead, you can: Call Hooks from React function components.

https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions

There are a couple of things to code make code better:

  1. initiate ref with 'null' value

  2. call your 'scrollTicker' function inside useEffect Hook.

  3. always remove listeners when component demount. Follow https://reactjs.org/docs/hooks-reference.html#useeffect for more details

  4. you can use useEffect hook like this:

    useEffect(() => { // use your ref here. return () => { // Remove linteners

    }; });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM