0

I am trying to change this class based react component to a functional component but i am gettig an infinite loop issue on setting the reference, i think its because of on each render the ref is a new object.

How could i convert the class based component to a functional component

index-class.js - Ref

class Collapse extends React.Component {
constructor(props) {
    super(props);
    this.state = {
        showContent: false,
        height: "0px",
        myRef: null,
    };
}

componentDidUpdate = (prevProps, prevState) => {
    if (prevState.height === "auto" && this.state.height !== "auto") {
        setTimeout(() => this.setState({ height: "0px" }), 1);
    }
}

setInnerRef = (ref) => this.setState({ myRef: ref });

toggleOpenClose = () => this.setState({
    showContent: !this.state.showContent,
    height: this.state.myRef.scrollHeight,
});

updateAfterTransition = () => {
    if (this.state.showContent) {
        this.setState({ height: "auto" });
    }
};

render() {
    const { title, children } = this.props;
    return (
        <div>
            <h2 onClick={() => this.toggleOpenClose()}>
                Example
            </h2>
            <div
                ref={this.setInnerRef}
                onTransitionEnd={() => this.updateAfterTransition()}
                style={{
                    height: this.state.height,
                    overflow: "hidden",
                    transition: "height 250ms linear 0s",
                }}
            >
                {children}
            </div>
        </div>
    );
}
}

what i have tried so far.

index-functional.js

import React, { useEffect, useState } from "react";
import { usePrevious } from "./usePrevious";

const Collapse = (props) => {
  const { title, children } = props || {};

  const [state, setState] = useState({
    showContent: false,
    height: "0px",
    myRef: null
  });

  const previousHeight = usePrevious(state.height);

  useEffect(() => {
    if (previousHeight === "auto" && state.height !== "auto") {
      setTimeout(
        () => setState((prevState) => ({ ...prevState, height: "0px" })),
        1
      );
    }
  }, [previousHeight, state.height]);

  const setInnerRef = (ref) =>
    setState((prevState) => ({ ...prevState, myRef: ref }));

  const toggleOpenClose = () =>
    setState((prevState) => ({
      ...prevState,
      showContent: !state.showContent,
      height: state.myRef.scrollHeight
    }));

  const updateAfterTransition = () => {
    if (state.showContent) {
      this.setState((prevState) => ({ ...prevState, height: "auto" }));
    }
  };

  return (
    <div>
      <h2 onClick={toggleOpenClose}>{title}</h2>
      <div
        ref={setInnerRef}
        onTransitionEnd={updateAfterTransition}
        style={{
          height: state.height,
          overflow: "hidden",
          transition: "height 250ms linear 0s"
        }}
      >
        {children}
      </div>
    </div>
  );
};

usePrevious.js - Link

import { useRef, useEffect } from "react";

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

export { usePrevious };
dev
  • 814
  • 10
  • 27

1 Answers1

1

The problem here is you set your reference to update through setState and useEffect (which is what causes you the infinite loop).

The way you would go by setting references on functional components would be as followed:

const Component = () => {
   const ref = useRef(null)

   return (
      <div ref={ref} />
   )
}

More info can be found here: https://reactjs.org/docs/refs-and-the-dom.html

ale917k
  • 1,494
  • 7
  • 18
  • 37
  • So how should i rewrite it in my functional component to make it working as like class component – dev Jun 06 '21 at 06:01
  • The way I showed above - Convert the component as you did, copy the code I shared and remove the ref from the `setState`, plus you can delete the `setInnerRef` func as that won't be used anymore – ale917k Jun 06 '21 at 06:44
  • And if you want to get the previous value for your height state, instead of using that `usePrevious` function, you can just access it whenever you need to set a new state: `setState((prevState) => console.log("prevState"))` – ale917k Jun 06 '21 at 06:48
  • https://codesandbox.io/s/cool-field-5lc9p, Thanks @ ale i am not getting the error but i am not getting the smooth transition effect, its not toggling back on click, what am i missing here ? But in class based component its working see https://codesandbox.io/s/nice-dewdney-64qh4 – dev Jun 06 '21 at 07:25
  • No worries! It was not working as when you need to access the element of a reference, you need to access it with `ref.current` instead of just `ref`. Also, you had another error where you forgot to update the state from class (where you use this.state) to functional component (where you just call the setState function). Working sandbox: https://codesandbox.io/s/mystifying-snow-x9mfh – ale917k Jun 06 '21 at 09:25
  • Okay thanks this helped me, just one doubt, if i wanted to have a recursive nested children how could i do that smooth transition effect on this https://codesandbox.io/s/ecstatic-monad-1m068 – dev Jun 06 '21 at 09:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233376/discussion-between-dev-and-ale917k). – dev Jun 06 '21 at 10:10