Using Location State in Functional Component

Viewed 2613

I'm working on a React project that requires the app URL to be fully-declared, static and immutable, so just:

https://exampleurl.com/path/to/app/index.html

..but none of the following:

https://exampleurl.com/path/to/app/

https://exampleurl.com/path/to/app/index.html?query=value

https://exampleurl.com/path/to/app/subdir/index.html

So, the project uses state and withRouter to provide basic app navigation. The responsible component is as follows:

import React from 'react';
import { withRouter} from "react-router-dom";
import RenderNav from './RenderNav';
import Cohort from '../view/Cohort';
import Holdings from '../view/Holdings';
import Policy from '../view/Policy';
import Search from '../view/Search';
import Start from '../view/Start';
import Training from '../view/Training';
import WatchList from '../view/WatchList';

class RenderDisplay extends React.Component {
  constructor(props) {
    super(props);
    this.state = this.props.location.state || { activeDisplay: 'start' };
    this.props.history.replace(this.props.location.pathname, this.state);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.props.location !== prevProps.location) {
      this.setState(this.props.location.state);
    }
  }

  setDisplay() {
    let displayMode = [];
    let d = this.state.activeDisplay;

    switch(d) {
      case 'start': displayMode = [
        <Start key={`display-${d}`} />
      ]; break;

      // ...and so on - activeDisplay determines displayMode (and so screen contents)

      default: displayMode = [<div>Error</div>]; break;
    }; return displayMode;
  }

  render() {
    return (
      <div className="container">
        {this.setDisplay()}
      </div>
    );
  }
}

export default withRouter(RenderDisplay);

The location.state object is set by the <Link /> component:

<Link to={{ state: {activeDisplay: 'start'} }}>
  <button type="button">Home</button>
</Link>

The result is normal browser navigation throughout the app while the URL remains unchanged.

My question is: how can the class-based RenderDisplay component above be converted to a functional component (using hooks)?

I've tried several permutations of useEffect, useState, and useLocation, but I clearly don't understand hooks well enough to reproduce the behavior of the class-based component above.

2 Answers

So,some steps that we always need to do:

  1. No need of HOCs, specifically withRouter. We can use the useHistory hook instead.
  2. All initialization either from the constructor or from componentDidMount can be done either directly in state, or in an useEffect that only runs once (empty dependency array).
  3. All updates to state, specifically from componentDidUpdate can be done in an useEffect that depends on your state variable. In your specific case, you need the previous prop as well. In these situations, we can create a custom hook and use it. In my case, it will be usePrev.
  4. All cleanup tasks, specifically from componentWillUnmount can be covered in an useEffect that returns the cleanup action.

Keeping these things in mind, these are probably the changes that need to be done:

Note: During this, I realized you are trying to render an array. I modified it to render a single element, you can update it otherwise.

import {useRef} from "react";

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

import React, {useState, useEffect} from 'react';
import {useHistory, useLocation} from 'react-router-dom';
import RenderNav from './RenderNav';
import Cohort from '../view/Cohort';
import Holdings from '../view/Holdings';
import Policy from '../view/Policy';
import Search from '../view/Search';
import Start from '../view/Start';
import Training from '../view/Training';
import WatchList from '../view/WatchList';
import usePrev from './usePrev';

const RenderDisplay = (props) => {

  const history = useHistory();
  const location = useLocation();
  const prevLocation = usePrev(location);  

  const [componentState, setComponentState] = useState(location.state || { activeDisplay: 'start' });

  useEffect(() => {
    history.replace(location.pathname, componentState);
  }, [location.pathname, history]);

  useEffect(() => {
    if (location.state.activeDisplay !== prevLocation.state.activeDisplay) {
        setComponentState(location.state);
    }
  }, [location]);

  setDisplay() {
    let displayMode;
    let d = componentState.activeDisplay;

    switch(d) {
      case 'start': displayMode = <Start key={`display-${d}`} />; 
      break;

      // ...and so on - activeDisplay determines displayMode (and so screen contents)

      default: displayMode = <div>Error</div>; break;
    }; 
    return displayMode;
  }

  
  return (
    <div className="container">
      {setDisplay()}
    </div>
  );
  
}

export default RenderDisplay;

Can you try this? You can convert your class component to function this way. Let me know if this helps.

import { useEffect, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";

function RenderDisplay() {
    const location = useLocation();
    const history = useHistory();
    const [state, setState] = useState(
        location.state || { activeDisplay: "start" }
    );

    useEffect(() => {
        history.replace(location.pathname, state);
    }, [history, location, state]);

    const setDisplay = () => {
        let displayMode = [];
        const d = state.activeDisplay;

        switch (d) {
            case "start":
                displayMode = [<Start key={`display-${d}`} />];
                break;

            // ...and so on - activeDisplay determines displayMode (and so screen contents)

            default:
                displayMode = [<div>Error</div>];
                break;
        }
        return displayMode;
    };

    return <div className="container">{setDisplay()}</div>;
}

export default RenderDisplay;

Also for your this block of code,

componentDidUpdate(prevProps, prevState) {
    if (this.props.location !== prevProps.location) {
      this.setState(this.props.location.state);
    }
  }

We need to handle in another useEffect hook.

Related