Не знаю как nextjs - они дохрена намудрили и всё запутали(мне лень разбираться, я вообще реакт не использую), но нормальный SSR работает просто:
При первом* обращении к странице извне он рендерит страницу также как на клиенте со стейтом по умолчанию.
Т.е. если у тебя что-то происходит в useEffect(т.е. после первой отрисовки) - оно произойдёт только уже на клиенте. Если просто в useState сразу кладётся то, что уже есть - оно отработает нормально.
Если нужно наполнить всё данными заранее, то разные SSR-фреймворки предлагают разные способы "подождать данных" перед отдачей, всякие asyncData, fecth, поддержка async-await...
Но если брать всё это дело в чистом виде и делать руками, то серверный код абстрактно будет выглядеть так:
server.get('/', async (req, res) => {
try {
const baseData = await fetch('/data').then(data => data.json());
const html = await renderAppToString(<App data={data}></App>);
res.send(html);
} catch (error) {
res.status(500).send(error.message);
}
});
Т.е. при запросе /
ты руками получаешь стартовые данные, и руками их кладёшь в создаваемое приложение, после рендера которого сервер просто отдаёт результат.
Фреймворки всё это абстрагируют и упрощают, плюс есть возможность хранения стейта под клиента с сессиями и всем таким прочим, но суть всё та же - ты должен использовать предоставляемые фреймворком нестандартные для чистого React методы, чтоб он наполнил компоненты данными перед отдачей.
* Всегда стоит помнить, что для обычного пользователя SSR начинается и заканчивается при первом заходе на страницу, дальше приложение работает как обычное SPA. Только боты, которые получают каждую страницу отдельно будут получать SSR версии каждой страницы.