You'd think it's obvious – use D3 to drive the force simulation, React for rendering. That's the approach I teach in React For Dataviz.
Finding the best layout for a complex graph is hard. Even impossible in some cases. Force-directed graphs solve the problem by simulating physical forces between nodes, which leads to visually pleasing results.
The downside is that you have to wait for the simulation. You can pre-generate the final result before rendering, but you're waiting at some point somewhere.
You create a new simulation with d3.forceSimulation(), add different forces with .force('name', func), pass-in data with .nodes(), and update your visuals on each tick of the animation with .on('tick', func).
Where it gets tricky in a React context is that d3.forceSimulation() assumes it's running on DOM nodes directly. It wants to change attributes.