Hammerspace
Hammerspace

Reputation: 93

How to handle when a user enters the URL manually rather than through a link?

I have a component (ContainerMain.js) that fetches a list of containers from an API subsequently rendering each container name within a button. When one of the buttons are clicked, state will be passed to another component (ContainerContents.js) using "history.push(/containercontents/${id}, {container: container})". That works when I select a button in ContainerMain.js.

To handle when I refresh and input the url ("http://localhost:3000/containercontents/6") manually, that leads to ContainerContents.js being rendered, I use "props.match.params.id" to grab the id of the container and go from there.

This is as far as I have gotten because I don't know how to use only one source of data depending if that data comes from the url or the button. Does that make sense? Even if I did, this solution seems terrible and I feel like I am missing an obvious way of handling this scenario. I'm not very familiar with Redux/Context but I do believe I can use one of those to remedy this. I would just like to know if there is a simpler, more common way of handling this scenario before using Redux/Context, assuming I can use them to handle this.

ContainerMain.js

import React, { useState, useEffect } from 'react'
import { Link, Route, useHistory } from 'react-router-dom'
import ContainerContents from './ContainerContents';

const ContainerMain = (props) => {
  const [containers, setContainers] = useState([]);
  // const [showContainerContents, setShowContainerContents] = useState(false);
  // const [selectedContainerId, setSelectedContainerId] = useState();
  const history = useHistory();
  
  const getContainers = async () => {
    const url = "/api/containers";    

    await fetch(url)
      .then(response => response.json())
      .then(response => setContainers(response))
      .catch(() => console.log("Can’t access " + url + " response. Blocked by browser?"))
  }

  const deleteContainer = async (containerId) => {
    let result = window.confirm("Are you sure you want to delete your " + containers.find(container => container.id === containerId).name + "?");
    if (result) {
        setContainers(containers.filter(container => container.id !== containerId))

        fetch(`api/containers/${containerId}`, {
             method: 'DELETE',
        })
        .then(res => res.json()) // or res.json()
        .then(res => console.log(res))
    } 
    else {
        return
    }
  }


  // const onButtonClick = (containerId) => {
  //   setShowContainerContents(true)
  //   setSelectedContainerId(containerId)
  // }

  useEffect(() => {
    getContainers();  
  }, []);

  
  return (
    <div>   
      <strong>MY FOOD CONTAINERS</strong>
      <br></br><br></br>

      <ul>
        {containers.map(container => (
          <li key={container.id}>
            <div>
              <button onClick={() => deleteContainer(container.id)} className="button is-rounded is-danger">Delete</button>
              <button onClick={() => history.push(`/containercontents/${container.id}`, {container: container})} className="button is-primary">{container.name}</button>
              

              {/* <Route 
              path="/ContainerContents" component={HomePage}/> */}

              {/* <Link to={`/containercontents/${container.id}`}>{container.name}</Link> */}
              {/* <Link to={{
                  pathname: `/containercontents/${container.name}`,
                  state: {container: container}
                }}>{container.name}
              </Link> */}
            </div>
          </li>
        ))}
      </ul>  

   
      {/* {showContainerContents ? <ContainerContents containerId={selectedContainerId}  /> : null}   */}
      
      
    </div>
  );
}

export default ContainerMain;

ContainerContents.js

import React, { useEffect, useState} from 'react';

const ContainerContents = (props) => {
  const [container, setContainer] = useState({});



  useEffect(() => {
    const fetchContainer = async () => { 
      const { id } = props.match.params;

      console.log("This is the id: " + id)

      let url = `/api/containers/${id}`;
  
      await fetch(url)
        .then(response => response.json())
        .then(response => setContainer(response))
        .catch(() => console.log("Can’t access " + url + " response. Blocked by browser?"))
  }

  fetchContainer();   

      
  }, [props.match.params]);



  


  
  return (
    <div>

          {/* will not work when I refresh and enter url as expected*/}
         <h1>{props.location.state.container.name}</h1> 



         <h1>{container.name}</h1> 
         

    </div>
  )
}

export default ContainerContents;

Upvotes: 3

Views: 538

Answers (2)

lawrence-witt
lawrence-witt

Reputation: 9354

I don't know how to use only one source of data depending if that data comes from the url or the button.

This is indeed the whole idea behind using Redux, or some other state management system - having one universal store of truth for the whole app so you always know where your data is coming from. So yes, ultimately it would be best to implement something like that, and you will probably want to after having to deal with huge React states and prop drilling, but it is not essential by any means.

So long as your containers data lives sufficiently high up in the component tree for all components that need it to access it, you can pass it down to them via props. Containers will only ever live in one place, so you can be sure that if it's not there, it needs to be fetched. It's a little bit unclear from all your commented code whether ContainerMain is actually supposed to render ContainerContents, but lets say it doesn't. Here's a simplified version of your component structure - the idea is basically that you check whether the data prop exists before requesting it in the child.

Setting up a parent component which is responsible for holding all containers:

export default function App() {
  const [containers, setContainers] = useState([]);

  return (
    <Router>
      <Switch>
        <Route exact path="/">
          <ContainerMain
            containers={containers}
            setContainers={setContainers}
          />
        </Route>
        <Route exact path="/containercontents/:id">
          <ContainerContents
            containers={containers}
            setContainers={setContainers}
          />
        </Route>
      </Switch>
    </Router>
  );
}

Component where we fetch all containers, sending them up to the parent and recieving them back as props:

function ContainerMain({ containers, setContainers }) {
  const history = useHistory();

  const getContainers = async () => {
    const url = "/api/containers";

    await fetch(url)
      .then(response => response.json())
      .then(response => setContainers(response))
      .catch(() =>
        console.log("Can’t access " + url + " response. Blocked by browser?")
      );
  };

  useEffect(() => {
    getContainers();
  }, [getContainers]);

  return (
    <div>
      <strong>MY FOOD CONTAINERS</strong>
      <br />
      <br />

      <ul>
        {containers.map(container => (
          <li key={container.id}>
            <div>
              <button
                onClick={() =>
                  history.push(`/containercontents/${container.id}`)
                }
                className="button is-primary"
              >
                {container.name}
              </button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

Specific container component. If the container doesn't exist in the parent, fetch it - the return function will automatically display it if/once it exists in the parent state.

function ContainerContents({ containers, setContainers }) {
  const match = useRouteMatch();

  useEffect(() => {
    const fetchContainer = async id => {
      let url = `/api/containers/${id}`;

      await fetch(url)
        .then(response => response.json())
        .then(response => {
          setContainers([...containers, response]);
        })
        .catch(() =>
          console.log("Can’t access " + url + " response. Blocked by browser?")
        );
    };

    const found = containers.find(el => el.id === match.params.id);

    if (!found) {
      fetchContainer(match.params.id);
    }
  }, [match.params, containers, setContainers]);

  function selectContainer() {
    const found = containers.find(el => el.id === match.params.id);

    return found ? (
      <div>
        <h1>{found.name}</h1>
      </div>
    ) : null;
  }

  return selectContainer();
}

Upvotes: 1

zjb
zjb

Reputation: 400

As far as I understand it, you want to be able to take the id from the button click when that method is used, and take the id from the url when it is manually reached instead.

A simple way to do this might be to just retrieve the id from the url in the case that props.match.params doesn't exist? You should be able to use the useLocation hook to access it, and then I'm sure there's a simpler way but you could just parse the id you want.

like:

  const location = useLocation();

  let url_segs = location.pathname.split("/");
  let id = url_segs[url_segs.length - 1]

Also I believe react-router-dom now supports useParams for a simpler method to access the params if you are interested.

Upvotes: 1

Related Questions