George Mauer
George Mauer

Reputation: 122112

How does react-router persist location state over page refreshes?

I am just so very confused.

I have a workflow where someone can start to fill out a form for a product. It is a long form and I want to save progress to the server as they type (but not until they fill it out for a bit). So we start at a url for creating the form, after they've typed for a bit, we POST to create a resource on the server, and after the request finishes, we update the url to the edit route with the new id.

In other words, you start filling out the form at url /product then after you've filled it out for a bit the url shifts to /product/123. After that, loading that URL gives you your form.

So basically I have

<Route path={`/product`} exact component={CreateProduct} />

and

<Route exact={true} path="/product/:productId" render={({
      match: {params: {productId}},
      location: {state: {data}={}}
  }) => (
    <EditProduct productId={productId} initialData={data} 
  )} />

See that state? That's because the way I do the switch over from create to edit mode is something like this

const id = await apiFetch(`/api/product`, data, {method: `POST`})
this.props.history.push({pathname: `/product/${id}`, state: {data} })

in the constructor of my <EditProduct> component I have

constructor({productId, initialData}) {
   this.super()
   this.state = {}
   if(initialData)
     this.setState({data: initialData})
   else
     getProduct(productId).then(({data}) => this.setState({data}))
}

By doing that, the initial data into the <EditProduct> is seeded from the <CreateProduct> component and I don't need to reload it from the server or anything.

This works, the transition is smooth, the url updates, and everything is hunky dory.

I can now continue editing the <EditProduct> component and it saves properly. I can open a new tab to the same url and it loads everything up and I can continue. This happens because in that situation initialData is undefined so it's loaded from the server. yay!

BUT

If I instead refresh the original tab things get weird. Any changes that have accumulated since the save are lost. Drilling down in the debugger I see the issue is that initialData passed from the location.state.data object is not empty - it is the initial object from when the product was first created.

So where on earth does it come from? I just did a full page refresh (even a "hard" refresh with no cache and devtools open). That data isn't in the URL (and in fact copy pasting the url into another tab in the same window doesn't have this issue).

The only mechanism I'm aware of that can persist data across refreshes but not to new tabs like this is sessionStorage, yet when I check it in the console, I am told

> sessionStorage
< Storage {length: 0}

I've even thought that maybe react-router is manipulating session storage just before the page unloads and just after it loads, but breaking on the first line of my javascript bundle shows the exact same thing.

So how on earth is this persistence happening!?

Upvotes: 23

Views: 13166

Answers (2)

Guichi
Guichi

Reputation: 2343

I believe the asker already resolve this problem, the answer is buried in the comment though.

The question is actually down to this:

Where the state come from when the user reloads the page? And state refers to props.location.state provided by react-router

TLDR; the state is not a plain javascript implementation, it is bound to the browser environment.

The BroswerRouter of react-router use the underlying native broswer history API directly so the history API is bound to the platform, you can not predict its behavior based on the normal rule.

Here is the special part:

The state object can be anything that can be serialized. Because Firefox saves state objects to the user's disk so they can be restored after the user restarts the browser

Most of the users treat the state as a plain javascript, so there is a problem

Upvotes: 6

pederk
pederk

Reputation: 154

I had a very similar problem, and the same confusion.

Solved it with window.history.replaceState()

I had a simple search form which redirected to a second page, and used the location state from the router to repopulate the search input on the second page.

In myse case, this happened:

  1. Search for "foo" on the first page -> Get redirected to the second page, and see search+results for "foo".
  2. Search for "bar" on the second page. -> See results for "bar".
  3. Hit refresh. Expectation? Either an empty search bar, or search+results for "bar". -> Instead, see search+results for "foo" (??)

I solved this by making it so that every time the user does a search on the second page, I replace the state using window.history.replaceState with the correct search term. This way a refresh gives the user the expected search. Replacing the state with an empty object on each search from the second page worked fine as well, giving the user an empty search on each refresh.

Upvotes: 1

Related Questions