Очень похоже на баг реакта. А может и не баг (источник):
Stricter Strict Mode: In the future, React will provide a feature that lets components preserve state between unmounts. To prepare for it, React 18 introduces a new development-only check to Strict Mode. React will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount. If this breaks your app, consider removing Strict Mode until you can fix the components to be resilient to remounting with existing state.
Если вместо нового рендеринга через createRoot использовать старый через render, то эффенкт будет вызываться 1 раз как и должен. Это касается как 17го, таки 18го реакта, но 18й выдаёт предупреждение о том, что старый рендер устарел.
const { StrictMode, useState, useEffect } = React
const { createRoot } = ReactDOM
const { log } = console
const rootElement = document.querySelector("main");
const root = createRoot(rootElement);
function App() {
log("render")
useState(() => log("useState"))
useEffect(() => log("useEffect"), [])
return <h1>Hi!</h1>
}
root.render(
<StrictMode>
<App />
</StrictMode>
);
<script crossorigin src="//unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="//unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<main></main>
const { StrictMode, useState, useEffect } = React
const { render } = ReactDOM
const { log } = console
function App() {
log("render")
useState(() => log("useState"))
useEffect(() => log("useEffect"), [])
return <h1>Hi!</h1>
}
render(
<StrictMode>
<App />
</StrictMode>,
document.querySelector("main")
);
<script crossorigin src="//unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="//unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<main></main>
В production-сборке в любом случае будет только один вызов (как и один рендеринг):
const { StrictMode, useState, useEffect } = React
const { createRoot } = ReactDOM
const { log } = console
const rootElement = document.querySelector("main");
const root = createRoot(rootElement);
function App() {
log("render")
useState(() => log("useState"))
useEffect(() => log("useEffect"), [])
return <h1>Hi!</h1>
}
root.render(
<StrictMode>
<App />
</StrictMode>
);
<script crossorigin src="//unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="//unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<main></main>
const { StrictMode, useState, useEffect } = React
const { render } = ReactDOM
const { log } = console
function App() {
log("render")
useState(() => log("useState"))
useEffect(() => log("useEffect"), [])
return <h1>Hi!</h1>
}
render(
<StrictMode>
<App />
</StrictMode>,
document.querySelector("main")
);
<script crossorigin src="//unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="//unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<main></main>
React.StrictMode? – Maksim Bogdanov Apr 30 '22 at 19:59