React route refresh without page reload
Tags: react, reacthooks, javascript, testing
Published: 2020-10-13 15:44:28

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](https://github.com/ReactTraining/react-router/issues/7416))

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

```javascript
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.

```javascript
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.

```javascript
  
  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:

```javascript
# ../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:
```javascript
  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** ..

```javascript
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