it's the primary way to shift left
it's the same thing you would do on your local machine, serve the app on localhost:whateverport, and run the tests against that.
The deployment happens after a feature branch gets merged to master/main (usually called dev deployment), and or tags get pushed (usually called stage). Those are when you want to build and deploy the app. You can only e2e test a deployment after you deploy it. That's the only reason you need intermediate deployments.
branch | dev | stage | prod
You can block PRs immediately at branch level if there is something wrong with the new code
To be able to test a dev deployment, the feature branch has to pass
To be able to test a stage deployment, the master branch has to pass
To be able to test a prod deployment, tags have to pass
___________
There are some teams that use S3 buckets, on feature branches they build and deploy the app to an ephemeral instance, then remove that instance upon merge to master/main. That flow is rare, you are probably are not doing it. The only thing I don't love about it is, it makes it a bit more difficult to deal with a PWA, and you don't get code coverage from Cypress e2e tests so that you can combine it with unit tests.
I would just do something like react-hooks-in-action repo
build -> unit
--------- lint
--------- typecheck
--------- parallelized e2e (n machines)
I think that's pretty much meta. You might want to move e2e to the next stage if you're using your own runners, which is a horrible thing. My old company, Siemens had this as a cost cutting measure and it hurt engineering a ton more than it helped costs.
You could have a 3rd stage later for something like combined unit and e2e coverage, and block merges if coverage is below 90 something %. We had this easily above 96% in a previous job, and the only failures were Angular/TS errors just choking out.