The actual answer to your question depends entirely on how the token is generated and used. In some cases, doing it server-side is a necessity if you don't want to trade away other security. In other cases, it makes no difference.
There is only one important part of "generating the CSRF token in the JS code and sending it as cookie and header" and it's the "header" part. You can't send custom headers (or standard headers with any but a narrow set of values) in cross-site requests, unless CORS is specifically configured on the server to allow it. Thus, a completely fixed header name/value pair of IsCsrf: no
is sufficient to prevent CSRF, so long as no third-party sites are allowed (per CORS preflight responses) to send requests with that header set. No randomness required.
Anyhow, that approach certainly doesn't require the server to pick a string, except in the sense of "the server should probably have a known header name it looks for". Nothing else matters, and it's fine for every user to use the same string (so long as there's no way for one user to force another to make a forged request from the target site directly... but of course that's not Cross-Site Request Forgery anymore).
Of course, that approach means all state-changing requests must be sent using script, which is undesirable for some sites (and already inherent for some others). Doing the more traditional version of double-submit cookie - where the value is both in a cookie and in the request body - is much more broadly compatible, but suffers from the thing where the security model of cookies is kind of a mess (what happens if the same cookie name is set with different values in Secure and non-Secure contexts? Why is it possible at all for a value set in a secure context to ever be visible in an insecure one, or vice versa?). There are various approaches to solving this - the most portable and widely-supported being HTTP Strict Transport Security (HSTS) as this effectively makes all cookies "Secure" whether they are actually flagged that way or not and totally obviates the issue of non-secure contexts - but if you're going for modern solutions you can also just use the SameSite
flag and just break the assumption that cookie-based sessions mean CSRF risk.
If you go with traditional double-submit cookies, I don't see any reason they need to be generated server-side. The important part is that something a cross-site request controls (the anti-CSRF token in the body) and something the cross-site request (hopefully) doesn't control (the anti-CSRF token in the cookie) match, or at least have some tightly coupled relationship (it's fine for one to be a cryptographic hash digest of the other, for example). On the other hand, double-submit cookies are... not a great approach, because there are too many ways to plant a cookie on a victim that suddenly you need to be careful about.
Or you can go with your initial idea, of hashing the session token and using that value as the anti-CSRF token! This is actually a great idea, one I wish I saw more often. It's as stateless as your session token. It's as secure as your session token (which had better be "very"). It inherently solves a problem I've seen in home-rolled anti-CSRF code, where the CSRF tokens are generated and checked securely but nobody remembered to tie them to a session, so I can log in, see my CSRF token, and use it to CSRF you even though you're supposed to have a different one. The meaningful requirement of anti-CSRF tokens isn't actually that they be "random", it's that they be "unpredictable", which is less strict. Since the exact same constraint applies to session tokens (e.g. a JWT is not at all random, but it does contain a very unpredictable signature), it's perfectly reasonable to use the session token as a source for the anti-CSRF token. Leaving the session token as HttpOnly and hashing it to get the anti-CSRF token means no risk of somebody stealing the session token through XSS (though of course, remote-controlling the site using script injected via XSS will still work; stealing the session token itself is less relevant than some people think).
If you're still concerned about randomness, you can easily solve this by using a (random) salt and concatenating the (unhashed, plain-text) salt with the hash digest rather than using the digest alone... but I'd love to hear any argument for any attack this actually protects against.
Obviously, if you use the "hash of the session token" approach and your session token is still HttpOnly, you do need to generate the anti-CSRF token server-side. Fortunately, that's really simple to do. The server won't even need to re-compute it except when actually checking the value; the value could be computed once and stored client-side (in local / session storage), then get dynamically injected into any HTML form or similar request where the protection is needed.