'React Transition Group: How to pass ref to mapped component to avoid Warning: findDOMNode is deprecated in StrictMode (still not working)

I am using "React Transition Group" ver 4.4.2 to animate menu buttons (CSSTransitions mapped inside TransitionGroup), but when I click them ones or a couple of times I get the warning:

"Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-find-node"

I know, I know, I have to use refs to get rid of that warning, so I did as it was described in these tickets:

Stackoverflow ticket 66587359

Stackoverflow ticket 63677852

But I still have the warning! I just have to click the button onece or click different menu buttons a couple of times. What is intresting is that I get that warning only once, than I have to reload page to get it again. I have inserted my part of code to the codesandbox:

CodeSandbox

Basically the code we need to look at is in NavigationItems.js and NavigationItem.js (src/components-satateLess/Navigation/NavigationItems... etc.)

As you can see I use useRef hook inside NavigationItem.js component that is mapped in NavigationItems.js:

import React, { useState } from "react";
import { TransitionGroup } from "react-transition-group";
import classes from "./NavigationItems.module.scss";
import routerButtons from "../../../router/mainMenu";
import NavigationItem from "./NavigationItem/NavigationItem"

const NavigationItems = () => {

    const [allButtons, setAllButtons] = useState(routerButtons);
    const [prevButton, setPrevButton] = useState({
        in:false, id:-1, desc:"",href:"",element:null
    });

    const allButtonsDeepUpdate = (idx, obj) => {
        const allButtonsCpy = [];
        for(let i=0;i<allButtons.length;i++) {
            if(i===idx) {
                allButtonsCpy.push(Object.assign({},obj));
            } else if (i===prevButton.id) {
                allButtonsCpy.push(Object.assign({},prevButton));
            } else {
                allButtonsCpy.push(Object.assign({},allButtons[i]));
            };
        };
        setAllButtons(allButtonsCpy);
     };

    const enterAnimation = (idx) => {
        //this contition checks if button wasn't already animated.
        if(allButtons[idx].id !== prevButton.id) {
            const newButton = {...allButtons[idx], ...{in:true}};
            if (prevButton.id !== -1)
                setPrevButton({...prevButton,...{in:false}});
            allButtonsDeepUpdate(idx, newButton);
            setPrevButton(Object.assign({},allButtons[idx]));
        }
    };

    return ( 
            <div>   
            <TransitionGroup component="ul" className={classes.NavigationItems}>
                {allButtons.map((button) => (
                    <NavigationItem
                        key={button.id}
                        starter={button.in}
                        timeout={1000}
                        click={enterAnimation.bind(this,button.id)}
                        link={button.href}
                    >
                        {button.desc}
                    </NavigationItem>
                ))}
            </TransitionGroup>
            </div>
    );
};
 
export default NavigationItems;

Then NavigationItem.js:

import React, { useEffect, useRef } from 'react';
import { NavLink } from 'react-router-dom';
import { CSSTransition } from 'react-transition-group';
import classes from './NavigationItem.module.scss';
 
const NavigationItem = props => {
    const ref = useRef(null);

    useEffect(() => {
        console.log("Ref has changed: ",ref.current);
    },[ref.current])

    return (
        <CSSTransition
            nodeRef={ref}
            in={props.starter}
            classNames={{
                enter: classes.NavigationItemEnter,
                enterActive: classes.NavigationItemEnterActive,
                enterDone: classes.NavigationItemEnterDone,
                exit: classes.NavigationItemExit,
                exitActive: classes.NavigationItemExitActive,
                exitDone: classes.NavigationItemExitDone,
            }}
            timeout={props.timeout}
        >
            <li ref={ref} className={classes.NavigationItem} onClick={props.click}>
                <NavLink
                    //activeClassName={classes.active}
                    //className={({isActive}) => isActive ? classes.active : ''}
                    to={props.link}
                    exact={props.exact}
                >
                    {props.children}
                </NavLink>
            </li>
        </CSSTransition>
    );
}
 
export default NavigationItem;

I've also used React.createRef() and forwardRef() with similar results. Am I missing something, or is it caused by "React Transition Group" liblary? If so, were can I report that?

As always your answers and suggestions are more than welcome!

Thanks in advance!!!



Solution 1:[1]

I had a similar problem, and I finally solved it by wrapping the inner component in the map in a div and passing the ref to the div instead of the component. I couldn't make it work by giving the ref via React.forwardRef.

Also, the naming of the elements inside TransitionGroup is suspicious. Seems like enterDone means enter-active but it's not similar with exit. Take note of what my CSS is and what the effect is.

// Inside List.js
const getItemsToDo = () => {
    let checkedCopy = [...checked];
    return (
      <TransitionGroup className="todo-items">
        {checkedCopy.reverse().map((check, index) => {
          if (!check) {
            let reverse_index = checked.length - 1 - index;
            const textHandler = onTextChangeHandler(reverse_index);
            const checkboxHandler = onCheckboxChangeHandler(reverse_index);

            const itemRef = createRef();
            return (
              <CSSTransition
                key={reverse_index}
                timeout={500}
                classNames={{
                  enterDone: styles["todo-item-enter-active"],
                  enterActive: styles["todo-item-enter"],
                  exitActive: styles["todo-item-exit-active"],
                  exitDone: styles["todo-item-exit"],
                }}
                nodeRef={itemRef}
              >
                <div ref={itemRef}>
                  <ListElement
                    onTextChange={textHandler}
                    onCheckboxChange={checkboxHandler}
                    checked={check}
                    text={texts[reverse_index]}
                    key={reverse_index}
                  />
                </div>
              </CSSTransition>
            );
          } else return null;
        })}
      </TransitionGroup>
    );
  };
// ListElement.js
import React from "react";

const ListElement = (props) => {
  const { checked, text, onCheckboxChange, onTextChange, readOnly } = props;
  return (
    <form onSubmit={(e) => e.preventDefault()}>
      <input type="checkbox" checked={checked} onChange={onCheckboxChange} />
      <input
        type="text"
        value={text}
        onChange={onTextChange}
        readOnly={readOnly}
      />
    </form>
  );
};

export default ListElement;
/* List.module.css */
.todo-item-enter {
  opacity: 0;
}
.todo-item-enter-active {
  opacity: 1;
  transition: opacity 500ms ease-in;
}
.todo-item-exit {
  opacity: 1;
}
.todo-item-exit-active {
  opacity: 0;
  transform: translateY(2rem);
  transition: all 500ms ease-in-out;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Konrad Pagacz