React Class component to Hooks

Viewed 55

I am converting this Class component

class Pricing extends React.Component {
  state = {
    loading: true,
    pricelist: {
      pricelistEntries: [],
      name: '',
      monthlySubscriptionFee: '',
      currency: '',
      validFrom: '',
      smsInPackage: '',
    },
    nextPackage: undefined,
    showNextPackage: false,
  };

  componentDidMount() {
    const { getData } = this.props;
    getData(
      'price-lists/profile',
      { defaultSort, resourceArrayName: 'pricelist' },
      true
    ).then((res) => {
      this.setState({
        pricelist: res.data._embedded.pricelist[0],
        nextPackage: res.data._embedded.pricelist[1],
        loading: false,
      });
    });
  }

  handleShowNextPackageDetails = () => {
    this.setState((state) => ({ showNextPackage: !state.showNextPackage }));
  };

  render() {
    const { classes } = this.props;
    const { pricelist, loading, showNextPackage, nextPackage } = this.state;
    return (
      <React.Fragment>
        <Paper className={classes.root}>
          <Loading loading={loading} spinner>
            <React.Fragment>
              <Typography variant="body1" style={{ marginBottom: 10 }}>
                <FormattedMessage id="administration.packages.form.section.package.info" />
              </Typography>
              <PackageView pricelist={pricelist} />
            </React.Fragment>
          </Loading>
        </Paper>
        {nextPackage && (
          <Paper className={classes.next}>
            <div style={{ display: 'flex' }}>
              <Typography variant="body1" style={{ flexGrow: 1 }}>
                <FormattedMessage id="account.packages.form.section.next.package" />
              </Typography>
              <Button
                color="secondary"
                onClick={this.handleShowNextPackageDetails}
                size="small"
                style={{ marginBottom: '5px' }}
              >
                <FormattedMessage id="account.balance.details.label" />
                <ExpandMoreIcon
                  className={classnames(classes.expand, {
                    [classes.expandOpen]: showNextPackage,
                  })}
                />
              </Button>
            </div>
            <Collapse in={showNextPackage} timeout="auto" unmountOnExit>
              <PackageView pricelist={nextPackage} />
            </Collapse>
          </Paper>
        )}
      </React.Fragment>
    );
  }
}

to this Functional component

const Pricing = props => {
  const [initialState, setInitialState] = useState(
    {
      loading: true,
      pricelist: {
        pricelistEntries: [],
        name: '',
        monthlySubscriptionFee: '',
        currency: '',
        validFrom: '',
        smsInPackage: '',
      },
      nextPackage: undefined,
      showNextPackage: false,
    },
  );

  useEffect(() => {
    const { getData } = props;
    getData(
      'price-lists/profile',
      { defaultSort, resourceArrayName: 'pricelist' },
      true
    ).then((res) => {
      setInitialState({
        pricelist: res.data._embedded.pricelist[0],
        nextPackage: res.data._embedded.pricelist[1],
        loading: false,
      });
    });
  }, []);

  const handleShowNextPackageDetails = () => {
    setInitialState(!showNextPackage);
  };

    const { classes } = props;
    const { pricelist, loading, showNextPackage, nextPackage } = initialState;
    return (
      <React.Fragment>
        <Paper className={classes.root}>
          <Loading loading={loading} spinner>
            <React.Fragment>
              <Typography variant="body1" style={{ marginBottom: 10 }}>
                <FormattedMessage id="administration.packages.form.section.package.info" />
              </Typography>
              <PackageView pricelist={pricelist} />
            </React.Fragment>
          </Loading>
        </Paper>
        {nextPackage && (
          <Paper className={classes.next}>
            <div style={{ display: 'flex' }}>
              <Typography variant="body1" style={{ flexGrow: 1 }}>
                <FormattedMessage id="account.packages.form.section.next.package" />
              </Typography>
              <Button
                color="secondary"
                onClick={handleShowNextPackageDetails}
                size="small"
                style={{ marginBottom: '5px' }}
              >
                <FormattedMessage id="account.balance.details.label" />
                <ExpandMoreIcon
                  className={classnames(classes.expand, {
                    [classes.expandOpen]: showNextPackage,
                  })}
                />
              </Button>
            </div>
            <Collapse in={showNextPackage} timeout="auto" unmountOnExit>
              <PackageView pricelist={nextPackage} />
            </Collapse>
          </Paper>
        )}
      </React.Fragment>
    );

}

However, the button which should change visibility of nextPackage isn't working. On class component it works fine, and functional doesn't want to expand content on click. Might it be problem with state of object? Is there better way to convert this component and how to make handleShowNextPackageDetails work?

1 Answers

When you set your state like :setInitialState(!showNextPackage); you are overriding other state variables as well. So the object becomes: false:

Instead use the spread operator in all setInitialState since it is an object:

setInitialState(is => ({...is, nextPackage: !is.nextPackage})

Also, an example for updating pricelist:

const addEntry = priceListEntry => {
  setInitialState(is => ({
     ...is, pricelistEntries: [...is.pricelistEntries, priceListEntry]
  }))
}

Explanation:

Spread operator allows you to change the field you want, leaving others the same. And is in setInitialState(is is just the current state. So, you preserve current state, other than the field you want to change:

let obj = {name: 'Test', value: 10}
console.log(obj)

obj = {...obj, value: 15}
console.log(obj)

obj = {...obj, newValue: 20}
console.log(obj)

Related