Skip to content

Smooth animation when turning off a conditionally rendered component in React

My App.js is as below

export const App = () => {
  const [toggled, setToggled] = useState(false);
  const [ordering, setOrdering] = useState(false);

  const handleColorModeClick = () => {
    setToggled((s) => !s);
  };

  const handleOrdering = () => {
    setOrdering((s) => !s);
  };

  return (
    <Ordering.Provider value={{ ordering: ordering }}>
      <div className={`app ${toggled ? "theme-dark" : "theme-light"}`}>
        <Switch>
          <Route path="/" exact>
            <HeaderComponent toggled={toggled} onClick={handleColorModeClick} />
            <div>components2</div>
            <EateryInfo toggled={toggled} />
            {/* <CategoryItems toggled={toggled} /> */}
            <MenuButton toggled={toggled} />
          </Route>
          <Route path="/menu">
            <HeaderComponent toggled={toggled} onClick={handleColorModeClick} />
            <CategoryItems toggled={toggled} />
            <CheckBox
              text="Start Ordering"
              standAlone={true}
              handleOrdering={handleOrdering}
            />
            <MenuButton toggled={toggled} />
          </Route>
        </Switch>
      </div>
    </Ordering.Provider>
  );
};

I set the state of ordering variable using a checkbox

Then I use this to conditionally render the QuantityChange component like so

export const MenuEntry = ({ mealData, toggled }: MenuEntryProps) => {
  const orderingEnabled = useContext(Ordering);

  return (
    <div className="menu-entry">
      <MenuItem oneMenuItem={mealData} toggled={toggled} />
      {orderingEnabled.ordering ? <QuantityChange toggled={toggled} /> : ""}
    </div>
  );
};

All this works fine & the component is render as desired.

I want to have a smooth transition of entry & exit of this component. The animation on entry works just fine but I am not able to figure out how to get the exit animation working.

The video is what is happening now can be found in the video here https://youtu.be/5kl1wCBwR_U (the checkbox is at the right bottom hand corner)

I looked at several online forums to find an answer to this but I am unable to figure it out.

I tried usiing react-transition-group as well but no luck

export const QuantityChange = ({ toggled }: QuantityChangeProps) => {
  const orderingEnabled = useContext(Ordering);
  const duration = 500;
  return (
    <Transition in={orderingEnabled.ordering} timeout={duration} appear>
      {(status) => (
        <div
          className={`quantity-change flex ${
            toggled ? "theme-dark" : "theme-light"
          } fade-${status}`}
        >
          <span className="add-quantity">+</span>
          <span className="quantity">0</span>
          <span className="subtract-quantity">-</span>
        </div>
      )}
    </Transition>
  );
};

I looked at onAnimationEnd but was unable to figure it out.

Answer

Looks like you need a simple Accordion thingy. You could try something like that (snippet below).

One of the main moments here is setting the height to the auto value. It allows the content to change, and it won’t strict its dimensions.

AccordionItem conditionally renders its children. If it should be closed and the animation is over, then no children will be rendered.

const AccordionItem = (props) => {
  const { className, headline, open, children } = props

  const [height, setHeight] = React.useState(0)
  const [isOver, setOver] = React.useState(false)
  const bodyRef = React.useRef(null)

  const getDivHeight = React.useCallback(() => {
    const { height } = bodyRef.current ? bodyRef.current.getBoundingClientRect() : {}

    return height || 0
  }, [])

  // set `auto` to allow an inner content to change
  const handleTransitionEnd = React.useCallback(
    (e) => {
      if (e.propertyName === 'height') {
        setHeight(open ? 'auto' : 0)
        if (!open) {
          setOver(true)
        }
      }
    },
    [open]
  )

  React.useEffect(() => {
    setHeight(getDivHeight())
    setOver(false)
    
    if (!open) {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => setHeight(0))
      })
    }

  }, [getDivHeight, open])
  
  const shouldHide = !open && isOver

  return (
    <div style={{overflow: 'hidden'}}>
      <div
        style={{ height, transition: "all 2s" }}
        onTransitionEnd={handleTransitionEnd}
      >
        <div ref={bodyRef}>
          {shouldHide ? null : children}
        </div>
      </div>
    </div>
  )
}



const App = () => {
  const [open, setOpen] = React.useState(false)

  return (
    <div>          
      <button onClick={() => setOpen(isOpen => !isOpen)}>toggle</button>
      
      <table style={{width: '100%'}}>
        <tr>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
        </tr>
         <tr>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
        </tr>
         <tr>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
        </tr>
      </table>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>


<div id="root"></div>