Using React, findDOMNode is deprecated in StrictMode is thrown as a warning when using react-transition-group
Asked Answered
I

3

9

I'm using the package react-transition-group, I have tried using the nodeRef props on the CSSTransition component, and added a wrapper on my component but I still get the warning regarding findDOMNode.

Here's the code:

 <CSSTransition
        key={entry.id}
        timeout={500}
        classNames="timesheet-entry"
      >
          <TimesheetEntry
            taskOptions={taskOptions || []}
            deleteHandler={(event) => {
              deleteHandler(event, entry.id.toString());
            }}
            data={entry}
            dateChangeHandler={(date: Date) =>
              dateChangeHandler(date, entry.id)
            }
            hoursChangeHandler={(event) => hoursChangeHandler(event, entry.id)}
            taskCodeChangeHandler={(event, value) =>
              taskCodeChangeHandler(event, value, entry.id)
            }
          />
      </CSSTransition>

Code for the TimesheetEntry component:

function TimesheetEntry(props: TimesheetEntryProps) {
  return (
    <div>
      <MuiPickersUtilsProvider utils={DateFnsUtils}>
        <KeyboardDatePicker
          label="Date"
          style={{ marginRight: '15px', height: '20px', marginTop: '-2px' }}
          disableToolbar
          variant="inline"
          format="MM/dd/yyyy"
          margin="normal"
          value={props.data.date}
          onChange={props.dateChangeHandler}
          size="small"
          KeyboardButtonProps={{
            'aria-label': 'change date',
          }}
        />
      </MuiPickersUtilsProvider>

      <Autocomplete
        size="small"
        style={{
          width: 300,
          display: 'inline-block',
          marginRight: '15px',
        }}
        options={props.taskOptions}
        getOptionLabel={(option) => option.name}
        getOptionSelected={(option, value) => {
          return option.id === value.id && option.name === value.name;
        }}
        onChange={props.taskCodeChangeHandler}
        renderInput={(params) => (
          <TextField {...params} label="Task" variant="outlined" />
        )}
      />

      <TextField
        size="small"
        style={{ marginRight: '15px', height: '20px' }}
        label="Hours"
        type="number"
        inputProps={{ min: 0.5, step: 0.5 }}
        onChange={props.hoursChangeHandler}
        InputLabelProps={{
          shrink: true,
        }}
      />

      <Button
        style={{ marginRight: '15px' }}
        variant="contained"
        color="secondary"
        size="small"
        startIcon={<DeleteIcon />}
        onClick={props.deleteHandler}
      >
        Delete
      </Button>
    </div>
  );
}

export default TimesheetEntry;

I've also made a somewhat similar code setup in codesandbox here

I've tried adding nodeRef and a ref reference through a div wrapper on my TimesheetEntry component but that seems to make the animation behave improperly(adding new entries works properly but when I try to delete the entry, the animation doesn't seem to work anymore). I'm also looking for a way without creating a div wrapper on the TimesheetEntry component.

Irrelievable answered 4/6, 2020 at 5:31 Comment(2)
your timesheet component returns multiple elements, so I'm not sure you will be able to achieve that without wrapping with a div. in this changelog it mentions you should use the noderef github.com/reactjs/react-transition-group/blob/…Lorient
@Lorient I've already tried that prior to asking actually as I've mentioned in my post, the thing is when I tried it the warning no longer shows but the animation when a TimesheetEntry is deleted no longer works properly, also what happen is that the last item always get deleted.Irrelievable
C
14

There are actually two distinct findDOMNode warnings in your CodeSandbox demo:

  1. When you first add or remove an entry, which originates from the direct usage of react-transition-group for TimesheetEntry.

  2. When you save your timesheet, which originates from the indirect usage of react-transition-group through Material UI's Snackbar component.

Unfortunately, you have no control over the latter, so let's fix the former; you're managing a list of transitioning TimesheetEntry components, but to correctly implement nodeRef, each element needs a distinct ref object, and because you cannot call React hooks within a loop (see rules of hooks), you have to create a separate component:

const EntryContainer = ({ children, ...props }) => {
  const nodeRef = React.useRef(null);
  return (
    <CSSTransition
      nodeRef={nodeRef}
      timeout={500}
      classNames="timesheet-entry"
      {...props}
    >
      <div ref={nodeRef}>
        {children}
      </div>
    </CSSTransition>
  );
};

which you will wrap around TimesheetEntry:

const controls: JSX.Element[] = entries.map((entry: entry, index: number) => {
  return (
    <EntryContainer key={entry.id}>
      <TimesheetEntry
        deleteHandler={event => {
          deleteHandler(event, entry.id.toString());
        }}
        data={entry}
        dateChangeHandler={(date: Date) => dateChangeHandler(date, entry.id)}
        hoursChangeHandler={event => hoursChangeHandler(event, entry.id)}
        taskCodeChangeHandler={(event, value) =>
          taskCodeChangeHandler(event, value, entry.id)
        }
      />
    </EntryContainer>
  );
});

You said that you tried something like this already, but my suspicions are that you forgot to forward EntryContainer's props to CSSTransition, which is the crucial step because those are being passed down by TransitionGroup.

Congressman answered 4/6, 2020 at 10:12 Comment(6)
that's where I got stuck because I can't use hooks(useRef) on a loop, I just wasn't able to think of creating an EntryContainer to circumvent that. Thanks!Irrelievable
I feel like I'm having a similar issue here...but I checked that I was passing in my props and all is well. Exit transitions work, but not enter. Can someone spot the bug in this wrapper component? (TS) gist.github.com/stevecastaneda/899e83d47fa27397c447e2e6ae3d7771Amphi
I don't know whether it's a similar issue, but I recommend posting a new question explaining what is the purpose of this wrapper and why you're using Transition instead of CSSTransition, which contains a crucial reflow hack needed for CSS transitions to work as expected, as explained in the docs.Congressman
@Congressman I have one unrelated question. What would be the solution for this if I want to have only <CSSTransition ...>{children}</CSSTransition> without the wrapping <div>. I got stuck at this because it won't work without that ref of the wrapping <div> which I want to get rid off as it affects my layout a lotPrecede
@Precede nodeRef has to refer to a DOM node that is wrapping your transitioning content. I would need to know more about your situation in order to find a solution for your specific case. Maybe best to ask another Stack Overflow question and ping me on Twitter. (@silvenon)Congressman
@Congressman #63150340Precede
S
1

Caleb Taylor's answer solves the problem by forwarding the ref, and you can avoid using // @ts-ignore if you test the child element's validity before cloning it. For instance,

import { Children, cloneElement, isValidElement, useRef } from 'react';
import { CSSTransition as _CSSTransition } from 'react-transition-group';
import { CSSTransitionProps } from 'react-transition-group/CSSTransition';

export const CSSTransition = ({ children, ...props }: CSSTransitionProps) => {
  const nodeRef = useRef(null);

  return (
    <_CSSTransition {...props} nodeRef={nodeRef}>
      <>
        {Children.map(children, child =>
          isValidElement(child)
            ? cloneElement(child, { ref: nodeRef })
            : child
        )}
      </>
    </_CSSTransition>
  );
};

The idea came from this answer to an unrelated question.

Stinson answered 26/3, 2022 at 0:21 Comment(0)
B
0

My answer uses the same strategy as @sivenon's answer, but it's annoying to write "useless" refs that takes up space in the Component where your Transition is used. So here's a wrapper that adds the refs for you without using hidden divs because extra markup will ruin styling.

import React, { useRef } from 'react';
import { CSSTransition as _CSSTransition } from 'react-transition-group';
import { CSSTransitionProps } from 'react-transition-group/CSSTransition';

const CSSTransition = (props: CSSTransitionProps) => {
  const nodeRef = useRef(null);

  return (
    <_CSSTransition {...props} nodeRef={nodeRef}>
      <>
        {React.Children.map(props.children, (child) => {
          // @ts-ignore
          return React.cloneElement(child, { ref: nodeRef });
        })}
      </>
    </_CSSTransition>
  );
};

export default CSSTransition;
Bugbee answered 7/1, 2022 at 0:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.