Search code examples
javascriptreactjsterraformreact-router-dom

BrowserRouter - React Router Dom: Stops navigating when deployed via CI


Set Up
Host: S3 bucket server through CloudFront
CI: Github Actions / Terraform
Cloudfront Custom error page 404 set to index.html (replaced with 200)
S3 Error Page set to index.js
Node 16.10.0
React 18.2.0
Edit: React Router Dom 6.12.1 -> BrowserRouter Component

Very strange issue that has me stumped. When My React App is built and deployed via the CI. React Router will change the URL but not navigate to the correct screen. A hard refresh will go to the correct screen.

If I build Manually and Manually upload to S3 through the AWS console, navigation works as expected. Additionally navigation works all the time on localhost.

I can not figure out what the difference is, its stumped me for a few days. If anyone has any ideas it would really make my day.

CI is simply running npm install && npm run build

Here is the terraform resource thats depoying the files:

resource "aws_s3_bucket_object" "app_files" {
  for_each      = fileset("${path.module}/app/build", "**/*.*")
  bucket        = aws_s3_bucket.app_bucket.id
  key           = each.value
  source        = "${path.module}/app/build/${each.value}"
  etag          = filemd5("${path.module}/app/build/${each.value}")
  content_type  = lookup(var.mime_types, split(".", each.value)[length(split(".", each.value)) - 1], "application/octet-stream")
}

I have confirmed that mimetypes are being set properly.

here is my Cloudfront Distribution

resource "aws_cloudfront_distribution" "app_distribution" {
  origin {
    domain_name = aws_s3_bucket.app_bucket.website_endpoint
    origin_id   = aws_s3_bucket.app_bucket.id

    custom_origin_config {
      http_port = 80
      https_port = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols = ["TLSv1.2"]
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  aliases = [var.service_domain]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = aws_s3_bucket.app_bucket.id

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  viewer_certificate {
    acm_certificate_arn = var.main_certificate_arn
    ssl_support_method = "sni-only"
  }

  custom_error_response {
    error_code            = 404
    response_code         = 200
    response_page_path    = "/index.html"
    error_caching_min_ttl = 0
  }
}

Here is the JSX for my Router

<Router>
        <Routes>
          <Route // remove trailing slash
            path="*(/+)"
            loader={({ params }) => redirect(params['*'] || '/')}
          />
          {appStore?.auth?.accessToken ? (
            <Route path="" element={<Template />}>
              <Route
                exact
                path="/"
                element={<Navigate to={ROUTES.MY_MIRRORS} />}
              />
              ... Additional App Routes
              <Route
                path="/forbidden"
                element={<Pages.ErrorBoundaryForbidden />}
              />
              <Route path="/404" element={<Pages.ErrorBoundary404 />} />
              <Route
                path="*"
                element={<Navigate replace to={ROUTES.MY_MIRRORS} />}
              />
            </Route>
          ) : (
            <Route path="" element={<UnauthenticatedTemplate />}>
              <Route path="/login" element={<Pages.Login />} />
              <Route path="/register" element={<Pages.NewUser />} />
              <Route path="/contact" element={<Pages.Contact />} />
              <Route path="/privacy-policy" element={<Pages.PrivacyPolicy />} />
              <Route path="/" element={<Pages.Home />} />
              <Route path="*" element={<Navigate replace to="/login" />} />
            </Route>
          )}
        </Routes>
      </Router>

Finally, all navigation is used by the useNavigate hook:

import { useNavigate } from 'react-router-dom';
...

const navigate = useNavigate();
...
<Button color="gray" onClick={() => navigate('/contact')}>
  Contact Us
</Button>

EDIT: What makes this even stranger, is that If I remove the build step from my CI, and instead build manually but commit the build folder to my repo and have the CI just deploy build folder. Everything works fine. So ive narrowed down the issue to that it doesnt work if the build step is done in the CI


Solution

  • I was running into the same issue today after updating react-router and react-router-dom in my SPA from version 6.10.0 to 6.12.1.

    I just wanted to share some details of my constellation.

    Running in a development environment (npm start) works just fine.

    Building manually and running the build either locally (npx serve -s build) or on an external hoster yields the problem you describe: navigation just does not work.

    I use NavLink and Navigate elements and the useNavigate function from react-router-dom. All types of navigation don't work.

    I use BrowserRouter.

    I tried with the following node/npm-versions with the same effect:

    • 18.16.0/9.5.1
    • 20.3.0/9.6.7

    Edit:
    I just tested with which version the problem occurs:
    It works with 6.11.2 and 6.12.0.
    So it seems this problem was introduced with version 6.12.1.

    Which version are you using?

    Edit 2:
    It seems the problem has been reported on GitHub already.