The token has four properties and yours needs all of them.
Unpredictable. Generated from a cryptographically secure random source, long enough that guessing is hopeless. Not derived from the user id, the email, or a timestamp.
Single use. Invalidated the moment it is successfully used. This is the one people miss most often, a reused link means anyone who ever sees that email can reset the account again later.
Short lived. Somewhere in the region of fifteen minutes to an hour. Long enough for a real person, short enough that an old email in an inbox is not a standing key.
Stored hashed. Store a hash of the token, not the token itself, and compare hashes. Your database is then not a list of working reset links, which matters enormously if it is ever exposed.
Also:
Invalidate all outstanding tokens for that account when a new one is issued, and when the password is successfully changed.
Put the token in the path or a query parameter you handle carefully, and be aware it can end up in logs and in referrer headers: a redirect to a clean URL after consuming it is a small thing that helps.
Single use and short lived are the two that turn a working flow into a correct one.