They are fine for sorting! You just need to think about things differently.
SELECT user, text FROM (
SELECT u.user, text, row_number() OVER (PARTITION BY user ORDER BY rank) as rank
FROM users u
LEFT JOIN items using(user)
) ranked_items
WHERE rank < 4
This option works for unlimited top-n without creating a bunch of unnecessary columns in the response.