Password storage is a solved problem with a small number of correct answers, and getting it wrong is the difference between a database breach that is embarrassing and one that hands over every user's credentials - including the ones they reused on their bank. The decision is worth ten minutes even though almost every framework makes it for you.
The essential property is that the hash must be slow and memory-hard. General-purpose hashes such as SHA-256 are designed to be fast, which is exactly wrong here: fast means an attacker with a database dump and consumer graphics hardware can attempt billions of guesses per second. A purpose-built password hash is deliberately expensive, so the same hardware manages thousands rather than billions. Argon2id is the current recommendation, with bcrypt and scrypt remaining acceptable; anything from the SHA or MD5 families, however many times iterated by hand, is not.
Salting is not optional and is handled for you by any of those algorithms. A unique random salt per password means two users with the same password have different hashes, which defeats precomputed rainbow tables and stops an attacker cracking one hash and getting every account that shared it. Modern password hashing functions generate and store the salt inside the hash string, so the common implementation mistake - a single application-wide salt - simply cannot happen if you use them correctly.
Tune the cost parameters to your hardware rather than accepting defaults. The right setting is the highest cost your login endpoint can bear, which for most applications means a hash taking somewhere in the region of a few hundred milliseconds. Too low and you have given up the protection you chose the algorithm for; too high and your login becomes a denial-of-service surface, since each attempt costs you real CPU. Revisit it every couple of years, because hardware improves in the attacker's favour.
Plan for upgrading. Store the algorithm and parameters alongside the hash - the standard formats do this for you - and rehash a user's password with current settings when they next log in successfully, since that is the only moment you have the plaintext. Without that, an application deployed today is still running today's parameters in 2032, and the migration will require forcing a password reset on everyone.
Two policy points that matter more than the hashing. Check new passwords against a list of known breached passwords rather than enforcing composition rules - a mandatory symbol produces predictable substitutions, while rejecting passwords already in circulation prevents the attack that actually happens. And never limit password length to something short, or truncate silently, which is a surprisingly common bug that quietly weakens every credential in the system.