As we know, the main idea of React Applications is that they are single page. There is just ONE HTML file, and ONE JavaScript file (although we cound split it up - more on that another day). This means that we do not have different physical files for different pages in our Application. E.g. we do not have index.html
for the homepage, about.html
for the About page, blog.html
for the blog page. This was one of the key differences we discovered when we started using React, as opposed to when we were just using HTML. With React, all the content and logic is written in JavaScript and injected into a single HTML page.
At the end of the day, when we bundle our development code, these are the files we get:
index.html
bundle.js
- Any
.css
files we have - Any other images, fonts, etc that the app requires
But although there are not multiple HTML pages, like with a "traditional" website, as we know, we can make it seem like there are multiple pages.
We've been using BrowserRouter
in our classes and examples, which works by looking at the URL in your browser address bar and making sure that it keeps the UI in sync with the URL. In other words, the BrowserRouter
is always looking for changes in the URL and when it sees a change, it will make sure that the right components are being rendered, so that the page changes with the URL.
We should be familiar with this basic example from the React Router Docs
This all works fine locally but there seems to be a problem when we want to release our application to the world using S3.
The full explanation of what's going on here touches on hosting and servers, which we haven't covered yet, so if this doesn't make 100% sense to everyone at the moment, that's fine. But if you're already thinking about hosting on S3, then perhaps this will make a little more sense.
Servers are just computers which are always connected to the internet and always ready to send a file to another computer when that file is requested. Just like on your personal computer, these files can live in folders.
In a traditional website you might have some files in this structure:
├── index.html
├── script.js
├── main.css
├── about
│ ├── index.html
├── blog
│ ├── index.html
├── contact
│ ├── index.html
Servers are generally configured so that when the root is requested, e.g. http://www.my-traditional-site.com
, the index.html
page is delivered.
They will also generally be configured so that when any path is requested, e.g. http://www.my-traditional-site.com/about
, it will look for an index page within that directory to send, e.g. about/index.html
This is called Server Side Routing because the server is sending different files depending on different routes that are visited by the user.
When hosting our app in production, you would need a server when our files will live. These files will be, as mentioned:
index.html
bundle.js
- Any
.css
files we have - Any other images, fonts, etc that the app requires
As you can see, we do not have multiple folders or multiple index.html
pages, because React is a SPA.
When a user types the URL of the application in the URL bar, e.g. http://www.my-single-page-app.com
, the browser makes a request to the server where those files live for the index.html
page. A traditionally configured server will then send a file called index.html
, as expected.
The index.html
page will be sent to the browser, and that page in turn makes a request for the bundle.js
and any other CSS/assets it needs.
With the bundle.js
delivered, the entire logic for the entire application has already been sent to the browser. All the logic related to React Router, for looking for changes in the URL and updating the UI accordingly, has been delivered. So from then on, the user can start clicking links and navigating around the site and React Router will figure out which UI to show. When you're navigating in a SPA, you're not making new requests to the server for new pages to show - React Router is doing all the magic of "changing" the page contents for you. This is called Client Side Routing because all the logic for what page/content to show is happening within the browser.
So far so good....
The problem comes when the user doesn't go directly to the root of the site first of all. What happens if they type a URL with a path e.g. http://www.my-single-page-app.com/about
in the address bar? When you hit "enter" you'd be making a request to the server for the http://www.my-single-page-app.com/about
page.
In a traditionally configured server, as we saw above, the server would see the path portion of the URL (the /about
) and would look in a folder called /about
and send back probably the index.html
page inside that folder. However, for our SPA, we don't have a folder called /about
. Our server instead needs to send the one and only index.html
page we have, and then subsequently the bundle.js
.
So this is a bit of a gotcha, but if you are configuring your own server, that's generally no problem. You can configure it to say: "No matter what path is requested, always send the index.html page". And along with the index.html
page, we get the bundle.js
, and therefore all the logic required to perform client side routing.
And when we are running locally, we are using a local server called React Dev Server which, handily, is configured exactly like this. Yay!
Amazon S3 is providing you with a place to store your files, which is also what we use a server for, but because S3 is more about file-storage than specifically hosting a web application, you are limited in what you can configure yourself. You can configure it so that when the browser requests bucket-name.s3.amazonaws.com
the index.html
page is returned, but it's not possible to say "No matter what path is requested, send the index.html page". So there we have a problem.
When the browser requests bucket-name.s3.amazonaws.com/about
, S3 will want to send whatever is in the contents of the /about
folder in S3 - which won't exist, because you've only got the files mentioned above (index.html
, bundle.js
etc) so it will send back a 404. This is the essential problem we are trying to work around.
There are actually two solutions to this.
-
Configuration changes in S3 - which I won't go into now but feel free to ask me to explain these separately
-
Use
HashRouter
instead ofBrowserRouter
BrowserRouter expects to see changes to the path in the URL, such as:
bucket-name.s3.amazonaws.com/about
bucket-name.s3.amazonaws.com/contact
bucket-name.s3.amazonaws.com/blog
So you will configure routes in your app with the paths above.
Whereas HashRouter
will look at anything in the URL which comes after a #
sign, hence the name HashRouter
. Such as:
bucket-name.s3.amazonaws.com/#about
bucket-name.s3.amazonaws.com/#contact
bucket-name.s3.amazonaws.com/#blog
The clever thing here is that anything that comes after a #
is not a path. Remember how we used hashes for navigating within the same page to different HTML elements by ID?
So whether the user types any of these URLs in the address bar:
bucket-name.s3.amazonaws.com/#about
bucket-name.s3.amazonaws.com/#contact
bucket-name.s3.amazonaws.com/#blog
The server will only see the bucket-name.s3.amazonaws.com
portion, and will send back the index.html
page, and subsequently the bundle.js
. HashRouter
, however, does understand what those bits after the #
mean, and will render the correct UI for each variation. Therefore, no matter what page the user goes to first in our app, they will have all the content and logic delivered by S3 with no problem, and React Router will be able to handle the client side routing - watching for URL changes and switch the UI accordingly.