In the original design of views::join, it didn't just join any range of ranges, it had to be a range of reference to ranges.
This worked:
auto view2 = strings | views::join; // okay
Because strings is a range of string& (which is a reference to a range).
But this did not:
auto view3 = strings | views::transform([](std::string num) {
return std::format("{} ", num)
})
| views::join; // Compiler error
because now we're joining a range of string (specifically, prvalue strings, which is what your function returns). That violated the constraints. range-v3's views::join has the same constraint.
However, it is a fairly common use-case to be able to do this. So a recent defect against C++20 was to change the design of views::join to allow it to join any range of ranges. That's P2328. libstdc++ (gcc's standard library) already implements this, MSVC does not yet, nor does range-v3 (though I have a PR open). So the official C++20 answer is: what you're doing is correct, your library just hasn't implemented it yet.
Now, the range-v3 answer is to introduce a caching layer:
auto view3 = strings | rv::transform([](std::string num) {
return std::format("{} ", num)
})
| rv::cache1
| rv::join;
What cache1 does is cache one element at a time, which turns our range of prvalue string into a range of lvalue string such that join can successfully join it. The P2328 design is to basically incorporate this internally into join itself so that you don't have to do it manually. But until that happens, you can do the above using range-v3. There is no cache1 in C++20 and won't be in C++23 either.
Or you can use Casey's hack. But also don't.