When I'm ready to submit the PR, I rewrite the commit history so that each commit is an independent and easily consumable chunk of code - many of them might be isolated additions of helper methods, etc. One commit may be a database migration, another the REST API to talk to the database, each one having full tests for that discrete chunk of code.
The result of this is a PR possibly with 30+ commits for a large feature where each commit could be independently merged to master that, when stepping through one-at-a-time, tells a logical story of how every commit combines to deliver the full feature. Each commit message also provides a detailed explanation of what I've intended to do and why.
This means that when I'm near delivery for a feature I have a lot of administration work to do, but I think that the small, discrete, and clearly explained commits provide great value when trying to reason about the codebase down the line.