It’s mostly because of the React lifecycle. It runs once in the initial render without making any side effects, and then again in useEffect where it can set up side effects (observers). It has to be this way to support suspense, concurrent mode, error boundaries, etc.
Focusing too much on limiting rerenders can lead to some anti-patterns, and premature optimizations. That can lead to a lot of inappropriate use of useMemo for example, which I often see from junior devs in my project teams. The “React way” is to just make your renders cheap, and stop worrying about it.
If you’ve measured, and you are sure you have performance issues, you can pass a skipUpdate comparator which can check for specific changes to your data. It works kind of like how shouldComponentUpdate used to work.
Just be super careful. Checking something like an updated timestamp is fast and easy, but doing something like a deep-equal in your comparator is a big anti-pattern, since it’s performance is so indeterminate.
If you are looking for maximum performance with list views, you may also be interested in the upcoming useFind. Check it out!