1

I have the following urls that route to the same component. I want to use the same component for two scenarios, either a new blank version or an existing one which is then pre-populated via API fetch using the url parameter.

Once I change my url from e.g. http://localhost:3000/dashboard/planner/1 to http://localhost:3000/dashboard/planner/ the component remains its state and doesn't reset. The console.log in useEffect also doesn't re-trigger.

My approach was to define a boolean logic is_existing_offer in the useEffect hook.

So I guess I need to somehow re-render the component once changing the url as I can't call useEffect again or use another method to update its state when jumping to http://localhost:3000/dashboard/planner/?

This didn't help

<Routes>
    <Route key="planner_id" path="planner/:id" element={<PlannerWrapper is_existing_offer={true}/>}/>
    <Route key="planner_default" path="planner/" element={<PlannerWrapper is_existing_offer={false}/>}/>
</Routes>
// PlannerWrapper

const PlannerWrapper = (props) => {

    // Get the offer's uuid from route to make API call and fetch the offer
    const params = useParams()

..

    // Fetch the associated offer via API and pre-populate components, only runs once at mount
    // Only runs if it is not the blank planner instance
    useEffect(() => {
        if (props.is_existing_offer) {
            fetch(`http://127.0.0.1:8000/api/offer/${params.id}`, {
                method: 'GET',
                headers: {'Content-Type': 'application/json'},
            })
            .then(res => res.json())
            .then(result => {
                console.log('Success:', result);
                setValues(result)
            })
        }
        else {
            setValues(initial_state)
        }
    }, []);
JSRB
  • 2,492
  • 1
  • 17
  • 48

2 Answers2

1

Just because the route changes doesn't mean the component is remounted.

Add params.id to the dependency array

Adding params.id to the useEffect hook will trigger its callback when the route changes and the id value changes. Don't rely on the is_existing_offer as this doesn't trigger the useEffect and the id alone is the actual dependency since it is referenced.

const PlannerWrapper = (props) => {
  // Get the offer's uuid from route to make API call and fetch the offer
  const { id } = useParams()

  ...

  // Fetch the associated offer via API and pre-populate components, runs when `id` updates
  // Only runs if `id` is truthy, i.e. defined
  useEffect(() => {
    if (id) {
      fetch(`http://127.0.0.1:8000/api/offer/${id}`, {
        method: 'GET',
        headers: {'Content-Type': 'application/json'},
      })
        .then(res => res.json())
        .then(result => {
          console.log('Success:', result);
          setValues(result);
        })
    } else {
      setValues(initial_state);
    }
  }, [id]); // <-- id is referenced so it's a dependency

  ...

Add a React key to the routed component

Add the React key to the component you want it to actually differentiate, in this case it's the routed component, not the route. You can also simplify boolean props to just the prop name. If a is_existing_offer prop is added to a React component it will be treated as truthy, and if it is omitted completely it will be treated as falsey. Since the component is now remounted though, this is_existing_offer prop is unnecessary, the id route path param is what is still necessary as a dependency.

<Routes>
  <Route
    path="planner/:id"
    element={<PlannerWrapper key="planner_id" />}
  />
  <Route
    path="planner/"
    element={<PlannerWrapper key="planner_default" />}
  />
</Routes>

...

const PlannerWrapper = (props) => {
  // Get the offer's uuid from route to make API call and fetch the offer
  const { id } = useParams()

  ...

  // Fetch the associated offer via API and pre-populate components, runs when `id` updates
  // Only runs if `id` is truthy, i.e. defined
  useEffect(() => {
    if (id) {
      fetch(`http://127.0.0.1:8000/api/offer/${id}`, {
        method: 'GET',
        headers: {'Content-Type': 'application/json'},
      })
        .then(res => res.json())
        .then(result => {
          console.log('Success:', result);
          setValues(result);
        })
    } else {
      setValues(initial_state);
    }
  }, [id]); // <-- id is referenced so it's a dependency

  ...

Note that even when using React keys on the routed component that it still needs to use the route path params as a proper dependency. This means that the React keys are also unnecessary since the PlannerWrapper effectively only triggers the useEffect callback and GET request if the id param is defined/truthy.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
0

Try nesting child routes:

<Routes>
  <Route path="offers/" element={<DashboardOffers/>}/>
  <Route path="planner" element={<PlannerWrapper is_existing_offer={false}/>}>
    <Route path=":id" element={<PlannerWrapper is_existing_offer={true}/>}/>
  </Route>
</Routes>
Clarity
  • 10,730
  • 2
  • 25
  • 35
  • Visiting `http://localhost:3000/dashboard/planner/1` returns `false` for `is_existing_offer` hmm – JSRB Apr 11 '22 at 10:49
  • Not sure how the latest react-router works with `element`, but you can try to add a unique key to each route to force their re-render: `}>` – Clarity Apr 11 '22 at 10:50
  • that didn't help either - thanks though! – JSRB Apr 11 '22 at 11:41