diff --git a/.eslintrc b/.eslintrc
index 167c168efb7b04a6b7ee0581e4956c7a14a7bd25..596e1115f248c58964c343262d6dadec57910f3a 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -46,7 +46,7 @@
     "react/react-in-jsx-scope": "off",
     "react-hooks/exhaustive-deps": "error",
     "testing-library/render-result-naming-convention": "off",
-    "testing-library/no-render-in-setup": [
+    "testing-library/no-render-in-lifecycle": [
       "error",
       {
         "allowTestingFrameworkSetupHook": "beforeEach"
diff --git a/__tests__/integration/mirador/invalid-api-response.test.js b/__tests__/integration/mirador/invalid-api-response.test.js
index d0edd9de16a0702f5c1f3c14ad784f12079a09d4..a2f191d2a120f446abc4398a9506b75c6d5634ab 100644
--- a/__tests__/integration/mirador/invalid-api-response.test.js
+++ b/__tests__/integration/mirador/invalid-api-response.test.js
@@ -1,6 +1,7 @@
 describe('Mirador Invalid API Response Handler Test', () => {
   /** */
   async function fetchManifest(uri) {
+    await expect(page).toMatchElement('button');
     await page.evaluate(() => {
       document.querySelector('#addBtn').click();
     });
diff --git a/__tests__/src/actions/canvas.test.js b/__tests__/src/actions/canvas.test.js
index 4d6d92e4ab76b7d5da3bac0921f12238a0b4ffdf..fead977eedba259af0b4d8b61e945762a6e115c0 100644
--- a/__tests__/src/actions/canvas.test.js
+++ b/__tests__/src/actions/canvas.test.js
@@ -1,5 +1,5 @@
 import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
+import { thunk } from 'redux-thunk';
 
 import * as actions from '../../../src/state/actions';
 import ActionTypes from '../../../src/state/actions/action-types';
diff --git a/__tests__/src/components/App.test.js b/__tests__/src/components/App.test.js
index 868c73ad9c59d9a71a54fdfaee10aad7a43fe2be..53a7236ff7b3019b20327945b9f25f67a49894a4 100644
--- a/__tests__/src/components/App.test.js
+++ b/__tests__/src/components/App.test.js
@@ -16,8 +16,6 @@ describe('App', () => {
     createWrapper();
 
     expect(screen.queryByRole('main')).not.toBeInTheDocument();
-    await screen.findByText('welcome');
-
-    expect(screen.getByRole('main')).toBeInTheDocument();
+    expect(await screen.findByRole('main')).toBeInTheDocument();
   });
 });
diff --git a/__tests__/src/components/PrimaryWindow.test.js b/__tests__/src/components/PrimaryWindow.test.js
index ad78ebf4b5cf39c5f7433721471429ded5b96237..1eef93a4f83fc09861041ea0bd58958c977e36bb 100644
--- a/__tests__/src/components/PrimaryWindow.test.js
+++ b/__tests__/src/components/PrimaryWindow.test.js
@@ -1,4 +1,4 @@
-import { render, screen } from 'test-utils';
+import { render, screen, waitFor } from 'test-utils';
 import { PrimaryWindow } from '../../../src/components/PrimaryWindow';
 
 /** create wrapper */
@@ -31,7 +31,9 @@ describe('PrimaryWindow', () => {
   it('should render <GalleryView> if fetching is complete and view is gallery', async () => {
     createWrapper({ isFetching: false, view: 'gallery' });
     await screen.findByTestId('test-window');
-    expect(document.querySelector('#xyz-gallery')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access
+    await waitFor(() => {
+      expect(document.querySelector('#xyz-gallery')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access
+    });
   });
   it('should render <CollectionDialog> and <SelectCollection> if manifest is collection and isCollectionDialogVisible', async () => {
     render(<div id="xyz" />);
diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js
index d5e22093d19936c3e0a943eabb3e917ff2a60957..3103eea54361c22b7707aa8fd5f901688c39aca2 100644
--- a/__tests__/src/lib/MiradorViewer.test.js
+++ b/__tests__/src/lib/MiradorViewer.test.js
@@ -1,4 +1,4 @@
-import { render, screen } from 'test-utils';
+import { act, render, screen } from 'test-utils';
 import MiradorViewer from '../../../src/lib/MiradorViewer';
 
 jest.unmock('react-i18next');
@@ -23,7 +23,7 @@ describe('MiradorViewer', () => {
       expect(instance.store.dispatch).toBeDefined();
     });
     it('renders via ReactDOM', () => {
-      const instance = new MiradorViewer({ id: 'mirador' }); // eslint-disable-line no-unused-vars
+      act(() => { new MiradorViewer({ id: 'mirador' }); }); // eslint-disable-line no-new
 
       expect(screen.getByTestId('container')).not.toBeEmptyDOMElement();
     });
@@ -137,9 +137,11 @@ describe('MiradorViewer', () => {
 
   describe('unmount', () => {
     it('unmounts via ReactDOM', () => {
-      const instance = new MiradorViewer({ id: 'mirador' });
+      let instance;
+
+      act(() => { instance = new MiradorViewer({ id: 'mirador' }); });
       expect(screen.getByTestId('container')).not.toBeEmptyDOMElement();
-      instance.unmount();
+      act(() => { instance.unmount(); });
       expect(screen.getByTestId('container')).toBeEmptyDOMElement();
     });
   });
diff --git a/__tests__/utils/test-utils.js b/__tests__/utils/test-utils.js
index 7104dec6b038df0a91a8744b5378e22aeab848e9..2e30b405cb5d81590b3a50bd17911daace7811fe 100644
--- a/__tests__/utils/test-utils.js
+++ b/__tests__/utils/test-utils.js
@@ -2,7 +2,7 @@ import { Provider } from 'react-redux';
 import { render } from '@testing-library/react';
 import PropTypes from 'prop-types';
 import { createStore, applyMiddleware } from 'redux';
-import thunkMiddleware from 'redux-thunk';
+import { thunk } from 'redux-thunk';
 import { createTheme, ThemeProvider } from '@mui/material/styles';
 import createRootReducer from '../../src/state/reducers/rootReducer';
 import settings from '../../src/config/settings';
@@ -18,7 +18,7 @@ function renderWithProviders(
   {
     preloadedState = {},
     // Automatically create a store instance if no store was passed in
-    store = createStore(rootReducer, preloadedState, applyMiddleware(thunkMiddleware)),
+    store = createStore(rootReducer, preloadedState, applyMiddleware(thunk)),
     ...renderOptions
   } = {},
 ) {
diff --git a/package.json b/package.json
index 5fc129601be086a358ecb6c5b017cc88e1941fae..fabab5cbbb76b2677bb572345c4900d3b2bbd0fe 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
     "openseadragon": "^2.4.2 || ^3.0.0 || ^4.0.0",
     "prop-types": "^15.6.2",
     "rdndmb-html5-to-touch": "^8.0.0",
-    "re-reselect": "^4.0.0",
+    "re-reselect": "^5.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dnd": "^16.0.0",
     "react-dnd-html5-backend": "^16.0.0",
@@ -66,17 +66,16 @@
     "react-image": "^4.0.1",
     "react-intersection-observer": "^9.0.0",
     "react-mosaic-component": "^6.0.0",
-    "react-redux": "^7.1.0 || ^8.0.0",
+    "react-redux": "^8.0.0 || ^9.0.0",
     "react-resize-observer": "^1.1.1",
     "react-rnd": "^10.1",
     "react-sizeme": "^2.6.7 || ^3.0.0",
     "react-virtualized-auto-sizer": "^1.0.2",
     "react-window": "^1.8.5",
-    "redux": "^4.0.5",
-    "redux-devtools-extension": "^2.13.2",
+    "redux": "^5.0.0",
     "redux-saga": "^1.1.3",
-    "redux-thunk": "^2.3.0",
-    "reselect": "^4.0.0",
+    "redux-thunk": "^3.1.0",
+    "reselect": "^5.0.0",
     "stylis": "^4.3.0",
     "stylis-plugin-rtl": "^2.1.1",
     "url": "^0.11.0",
@@ -95,10 +94,10 @@
     "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
     "@testing-library/dom": "^9.2.0",
     "@testing-library/jest-dom": "^6.1.5",
-    "@testing-library/react": "^12.1.5",
+    "@testing-library/react": "^14.1.2",
     "@testing-library/user-event": "^14.4.3",
-    "@typescript-eslint/eslint-plugin": "^5.15.0",
-    "@typescript-eslint/parser": "^5.15.0",
+    "@typescript-eslint/eslint-plugin": "^6.14.0",
+    "@typescript-eslint/parser": "^6.14.0",
     "babel-jest": "^29.3.1",
     "babel-loader": "^9.1.0",
     "babel-plugin-lodash": "^3.3.4",
@@ -118,7 +117,7 @@
     "eslint-plugin-jsx-a11y": "^6.4.1",
     "eslint-plugin-react": "^7.29.4",
     "eslint-plugin-react-hooks": "^4.6.0",
-    "eslint-plugin-testing-library": "^5.10.2",
+    "eslint-plugin-testing-library": "^6.2.0",
     "glob": "^10.3.0",
     "http-server": "^14.1.0",
     "jest": "^29.3.1",
@@ -127,9 +126,9 @@
     "jest-puppeteer": "^9.0.2",
     "jsdom": "^23.0.0",
     "puppeteer": "^21.0.0",
-    "react": "^17.0.0",
+    "react": "^18.0.0",
     "react-dnd-test-backend": "^16.0.1",
-    "react-dom": "^17.0.0",
+    "react-dom": "^18.0.0",
     "react-refresh": "^0.14.0",
     "redux-mock-store": "^1.5.1",
     "redux-saga-test-plan": "^4.0.0-rc.3",
@@ -139,7 +138,7 @@
     "webpack-dev-server": "^4.7.4"
   },
   "peerDependencies": {
-    "react": "^17.0.0",
-    "react-dom": "^17.0.0"
+    "react": "^18.0.0",
+    "react-dom": "^18.0.0"
   }
 }
diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js
index c6fcf583e23f75e07bb8c9c6feac892c15973f13..8c9c23bb78c95dd7bd327121dfa5577a15c6a022 100644
--- a/src/lib/MiradorViewer.js
+++ b/src/lib/MiradorViewer.js
@@ -1,4 +1,4 @@
-import ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client';
 import { Provider } from 'react-redux';
 import HotApp from '../components/App';
 import {
@@ -20,10 +20,9 @@ class MiradorViewer {
 
     if (config.id) {
       this.container = document.getElementById(config.id);
-      config.id && ReactDOM.render(
-        this.render(),
-        this.container,
-      );
+      this.root = createRoot(this.container);
+
+      this.root.render(this.render());
     }
   }
 
@@ -42,7 +41,9 @@ class MiradorViewer {
    * Cleanup method to unmount Mirador from the dom
    */
   unmount() {
-    this.container && ReactDOM.unmountComponentAtNode(this.container);
+    if (!this.root) return;
+
+    this.root.unmount();
   }
 }
 
diff --git a/src/state/createStore.js b/src/state/createStore.js
index c115859260c6db7cd5398b22b308bf9572cf5b32..b593f54e9b5a014b7b3d38916d088a656bb43a85 100644
--- a/src/state/createStore.js
+++ b/src/state/createStore.js
@@ -3,7 +3,7 @@
 // state normalisation
 // (normalizer library)
 
-import thunkMiddleware from 'redux-thunk';
+import { thunk } from 'redux-thunk';
 import createSagaMiddleware from 'redux-saga';
 import { combineReducers, createStore, applyMiddleware } from 'redux';
 import { composeWithDevTools } from '@redux-devtools/extension';
@@ -27,7 +27,7 @@ function configureStore(pluginReducers, pluginSagas = []) {
   const store = createStore(
     rootReducer,
     composeWithDevTools(
-      applyMiddleware(thunkMiddleware, sagaMiddleware),
+      applyMiddleware(thunk, sagaMiddleware),
     ),
   );
 
diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js
index 496d8456216a94e999cce9b7940b7e2fe9d6cf59..aefe164ce328406d0dc19728b9feca7faa9bcacf 100644
--- a/src/state/selectors/manifests.js
+++ b/src/state/selectors/manifests.js
@@ -1,5 +1,5 @@
 import { createSelector } from 'reselect';
-import createCachedSelector from 're-reselect';
+import { createCachedSelector } from 're-reselect';
 import { PropertyValue, Utils, Resource } from 'manifesto.js';
 import getThumbnail from '../../lib/ThumbnailFactory';
 import asArray from '../../lib/asArray';
diff --git a/webpack.config.js b/webpack.config.js
index f31af73184ed2f9dd047042324a2a217d45e07ff..6e3c1e838f5f3634cd6320ea9da8dac72b16bc17 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -45,10 +45,6 @@ const baseConfig = mode => ({
     }),
   ],
   resolve: {
-    alias: {
-      'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js',
-      'react/jsx-runtime': 'react/jsx-runtime.js',
-    },
     extensions: ['.js'],
   },
 });