DEV Community

Marius Zaharie
Marius Zaharie

Posted on

React route refresh without page reload

Recently I ran into an interesting bug in the app I'm working on that seemed like a bug in react-router. I wrote seemed* because it's not really a bug but something left unimplemented in react-router because it can be 'fixed' in multiple ways depending on the needs of the developer. (the issue on github)

Our particular issue was that we wanted to do a page refresh after a server call that was updating an item on a list rendered by current react route. The list is loaded based on some filters stored into a higher Context state. Additionally, our app was loaded into another app meaning that routes are mounted accordingly inside a specific subset.

Initially the code that had to do the page refresh was looking something like this

handleItemUpdateSuccess() {
  history.push('app/list');
}

The problem here is that react doesn't refresh the component rendered at app/list because nothing changed in the state.

Step 1

First solution I was thinking of was to dispatch the getList action and let the List component loading itself without doing anything with history. This might have been the right solution but seemed to be very specific to my List component.

Then I found a hack that worked just fine inside our standalone app.

handleItemUpdateSuccess() {
  history.push('/');
  history.replace(redirectPath);
}

Step 2

But because the app's router was mounted into a nested route, doing history.push('/') un-mounts the entire react app loaded there. This means the whole context gets wiped.

Next step was to push back to the index route in the microfronted app.


  history.push(MicroFrontendRoute.Index);
  history.replace(redirectPath);

This one solves one problem, the react Context wrapping the routes remains untouched.

Step #3 -- solution

Now the problem is that this is not working properly all the time. Depending on how fast(or slow) the react router updates the location it manages to trigger a component un-mount or not! So if the second history update would be delayed a bit react would trigger it's un-mount hook..

After struggling to use setTimeout into a JS method and not beeing able to properly do clearTimeout in the same method, I've decided to extract everything into a custom hook:

# ../app/routes/useRefresh.ts
import { useEffect } from 'react';

import { MyAppRoute } from './MyAppRoute';

export default function useRefresh(history: any, path: string, resetRoute: string = MyAppRoute.Index) {
  let handler: any;

  const refresh = () => {
    history.push(resetRoute);

    handler = setTimeout(() => history.push(path), 10);
  };

  useEffect(() => {
    return () => handler && clearTimeout(handler);
  }, [handler]);

  return refresh;
}

Now that all the refresh logic is safely encapsulated into its own hook, let me show you how easy it is to use it:

  const history = useHistory();
  const refresh = useRefresh(history, redirectPath);

  const handleSuccess = () => {
    if (history.location.pathname === redirectPath) {
      refresh();
    } else {
      history.push(redirectPath);
    }
  };

  const handleErrors = () => {
    ...

Afterthought

As I pasted the above code snippet, I'm starting to think my useRefresh hook api can be made even more simple. I mean no parameters:

  1. How about doing useHistory inside the hook -- 1st parameter down
  2. redirectPath seems to fit more with the returned refresh function -- 2nd parameter down too.

Will let you do the refactoring as an exercise ;)

Bonus

Here are some unit tests I wrote for the useRefresh hook using jest and testing-library ..

import { renderHook, act } from '@testing-library/react-hooks';
import { createMemoryHistory } from 'history';

import useRefresh from '../../../../components/app/routes/useRefresh';
import { MyAppRoute } from '../../../../components/app/routes/MyAppRoute';

describe('useRefresh', () => {
  const history = createMemoryHistory();
  const subject = () => renderHook(() => useRefresh(history, MyAppRoute.List));

  it('returns a function', () => {
    const { result } = subject();

    expect(typeof result.current).toBe('function');
  });

  it('redirect to the Index route and then back to the given path', async () => {
    jest.useFakeTimers();
    jest.spyOn(history, 'push');

    const { result } = subject();

    result.current();

    expect(history.push).toHaveBeenCalledWith(MyAppRoute.Index);

    act(() => {
      jest.runAllTimers();
    });

    expect(history.push).toHaveBeenCalledWith(MyAppRoute.List);
    expect(history.push).toHaveBeenCalledTimes(2);
  });

  it('clears the timeout', () => {
    jest.useFakeTimers();

    const { result, unmount } = subject();

    result.current();

    act(() => {
      jest.runAllTimers();
    });

    unmount();
    expect(clearTimeout).toHaveBeenCalledTimes(1);
  });
});

If you know of a nicer way of refreshing a route with react-router than using setTimeout :| let me know in the comments

Top comments (4)

Collapse
 
adam1658 profile image
Adam Rich • Edited

What about forcing a component remount on route change

const location = useLocation();
return <MyPage key={location.key} />
Enter fullscreen mode Exit fullscreen mode

MyPage will unmount and a new one mount every time location.key changes. location.key changes every time you push a new location, even if you navigate to the same URL the .key still changes.

I just discovered this and it's working great!

Collapse
 
zbmarius profile image
Marius Zaharie • Edited

Good catch if it really changes when you push to the same location!!
Looks like it's in the location API v5.reactrouter.com/web/api/location.

I'd just call the hook inside the routed component like:

function MyPage() {
  const { key } = useLocation();
  return (jsx);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dsvgit profile image
Sergey Dedkov

thank you! great idea having empty page for refresh
for my case it was enough to use it without hook

const refresh = () => {
  history.replace("/empty");
  setTimeout(() => {
    history.replace("/same-page");
  }, 10);
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
junior774 profile image
Junior774

This works great! thanks