I considered writing my own version of a guide like this, but instead, I’ll just post a few tips:

If you are using textures rather than a SurfaceAppearance, you can lock the water plane to the position of the camera and make it appear to move properly by scrolling the UV offset of the texture.

Use the same formula that you used to generate the water to detect surface collisions by sampling the wave at the target position rather than trying to detect collisions with the actual wave mesh.
Here is a Gerstner wave function you can use to generate your waves:
GerstnerWave(Vector3 SamplePosition, Float Wavelength, Vector2 Direction, Float Steepness, Float Gravity, Float SampleTick)
function GerstnerWave(SamplePosition,Wavelength,Direction,Steepness,Gravity,SampleTick)
local k = (2 * math.pi) / Wavelength
local a = Steepness/k
local d = Direction.Unit
local c = math.sqrt(Gravity / k)
local f = k * d:Dot(Vector2.new(SamplePosition.X,SamplePosition.Z))  c * SampleTick
local cosF = math.cos(f)
Displacement Vectors
local dX = (d.X * (a * cosF))
local dY = a * math.sin(f)
local dZ = ( d.Y * (a * cosF))
return Vector3.new(dX,dY,dZ)
end

The wavelength of your smallest wave must be larger than the distance between vertices on your water mesh, otherwise, it will look pretty strange.

You can add the displacement from multiple Gerstner waves to get a more convincing final displacement for the water.