diff --git a/README.md b/README.md
index e52aaa1a..5bb8f2fa 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ The goal of this project is to provide a React with TypeScript starter applicati
- Platform: [React](https://react.dev/) with [TypeScript](https://www.typescriptlang.org/)
- Component Library: [Comet Component Library](https://github.com/MetroStar/comet)
- Data Visualization: [Victory Charts](https://formidable.com/open-source/victory/)
-- State Management: [Recoil](https://recoiljs.org/)
+- State Management: [Jotai](https://jotai.org/)
- Form Validation: [React Hook Form](https://react-hook-form.com/)
- Unit Testing: [Vitest](https://vitest.dev/) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- Code Analysis: [ESLint](https://eslint.org/)
diff --git a/package-lock.json b/package-lock.json
index 1c8d63d8..9589a70c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,13 +17,13 @@
"@uswds/uswds": "3.10.0",
"axios": "1.7.9",
"axios-mock-adapter": "2.1.0",
+ "jotai": "2.10.4",
"oidc-client-ts": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.54.1",
"react-oidc-context": "^3.2.0",
- "react-router-dom": "7.0.2",
- "recoil": "0.7.7"
+ "react-router-dom": "7.0.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -2757,13 +2757,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "dev": true
+ "devOptional": true
},
"node_modules/@types/react": {
"version": "18.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.17.tgz",
"integrity": "sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -2792,7 +2792,7 @@
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
- "dev": true
+ "devOptional": true
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
@@ -5472,7 +5472,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true
+ "devOptional": true
},
"node_modules/cypress": {
"version": "13.16.1",
@@ -7809,11 +7809,6 @@
"unenv": "^1.10.0"
}
},
- "node_modules/hamt_plus": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
- "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
- },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -8956,6 +8951,27 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jotai": {
+ "version": "2.10.4",
+ "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.10.4.tgz",
+ "integrity": "sha512-/T4ofyMSkAybEs2OvR8S4HACa+/ASUEPLz86SUjFXJqU9RdJKLvZDJrag398suvHC5CR0+Cs4P5m/gtVcryzlw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=17.0.0",
+ "react": ">=17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@@ -12131,25 +12147,6 @@
"object-assign": "^4.1.0"
}
},
- "node_modules/recoil": {
- "version": "0.7.7",
- "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
- "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
- "dependencies": {
- "hamt_plus": "1.0.2"
- },
- "peerDependencies": {
- "react": ">=16.13.1"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
- }
- },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
diff --git a/package.json b/package.json
index d1a9ab80..4b627df3 100644
--- a/package.json
+++ b/package.json
@@ -39,13 +39,13 @@
"@uswds/uswds": "3.10.0",
"axios": "1.7.9",
"axios-mock-adapter": "2.1.0",
+ "jotai": "2.10.4",
"oidc-client-ts": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.54.1",
"react-oidc-context": "^3.2.0",
- "react-router-dom": "7.0.2",
- "recoil": "0.7.7"
+ "react-router-dom": "7.0.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
diff --git a/src/components/header/header.test.tsx b/src/components/header/header.test.tsx
index a88b47d1..dd5cf968 100644
--- a/src/components/header/header.test.tsx
+++ b/src/components/header/header.test.tsx
@@ -2,8 +2,8 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { User } from '../../types/user';
import { Header } from './header';
@@ -11,11 +11,11 @@ import { Header } from './header';
describe('Header', () => {
const headerComponent = (
-
+
-
+
);
diff --git a/src/components/protected-route/protected-route.test.tsx b/src/components/protected-route/protected-route.test.tsx
index 4bf8786a..be74dd48 100644
--- a/src/components/protected-route/protected-route.test.tsx
+++ b/src/components/protected-route/protected-route.test.tsx
@@ -1,19 +1,19 @@
import { User } from '@src/types/user';
import { act, render } from '@testing-library/react';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { ProtectedRoute } from './protected-route';
describe('ProtectedRoute', () => {
const wrapperComponent = (
-
+
-
+
);
diff --git a/src/hooks/use-auth-sso.test.tsx b/src/hooks/use-auth-sso.test.tsx
index 9dfee306..b17e9ac3 100644
--- a/src/hooks/use-auth-sso.test.tsx
+++ b/src/hooks/use-auth-sso.test.tsx
@@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react';
-import { RecoilRoot } from 'recoil';
+import { Provider } from 'jotai';
import useAuth from './use-auth'; // Import your useAuth function
interface ContextWrapperProps {
@@ -24,7 +24,7 @@ describe('useAuth', () => {
});
const contextWrapper = ({ children }: ContextWrapperProps) => (
- {children}
+ {children}
);
it('should set isSignedIn to true when authenticated with sso', async () => {
diff --git a/src/hooks/use-auth.test.tsx b/src/hooks/use-auth.test.tsx
index bb499fe6..a35b0668 100644
--- a/src/hooks/use-auth.test.tsx
+++ b/src/hooks/use-auth.test.tsx
@@ -1,7 +1,7 @@
import keycloak from '@src/utils/keycloak';
import { act, renderHook } from '@testing-library/react';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
-import { RecoilRoot } from 'recoil';
import useAuth from './use-auth';
interface ContextWrapperProps {
@@ -16,7 +16,7 @@ describe('useAuth', () => {
const contextWrapper = ({ children }: ContextWrapperProps) => (
- {children}
+ {children}
);
diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts
index 261b87e4..a8d841ec 100644
--- a/src/hooks/use-auth.ts
+++ b/src/hooks/use-auth.ts
@@ -1,19 +1,19 @@
import { getSignInRedirectUrl } from '@src/utils/auth';
+import { useAtom } from 'jotai';
import { useEffect, useState } from 'react';
import { useAuth as useKeycloakAuth } from 'react-oidc-context';
-import { useRecoilState } from 'recoil';
import { userData } from '../data/user';
import { currentUserState, signedInState } from '../store';
import { User } from '../types/user';
const useAuth = () => {
const auth = useKeycloakAuth();
- const [isSignedIn, setIsSignedIn] = useRecoilState(signedInState);
+ const [isSignedIn, setIsSignedIn] = useAtom(signedInState);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState();
- const [currentUserData, setCurrentUserData] = useRecoilState<
- User | undefined
- >(currentUserState);
+ const [currentUserData, setCurrentUserData] = useAtom(
+ currentUserState,
+ );
/* TODO: Uncomment for interacting with own API, no need to send tokens to external public API */
// useEffect(() => {
diff --git a/src/main.tsx b/src/main.tsx
index c6d8f2a0..0de44337 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,8 +1,8 @@
+import { Provider } from 'jotai';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from 'react-oidc-context';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import { App } from './App.tsx';
import './styles.scss';
import keycloak from './utils/keycloak.ts';
@@ -11,9 +11,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
-
+
-
+
,
diff --git a/src/pages/dashboard/dashboard-bar-chart/dashboard-bar-chart.test.tsx b/src/pages/dashboard/dashboard-bar-chart/dashboard-bar-chart.test.tsx
index dd76c878..bd0d24b2 100644
--- a/src/pages/dashboard/dashboard-bar-chart/dashboard-bar-chart.test.tsx
+++ b/src/pages/dashboard/dashboard-bar-chart/dashboard-bar-chart.test.tsx
@@ -1,17 +1,17 @@
import { mockData } from '@src/data/spacecraft';
import { act, render } from '@testing-library/react';
+import { Provider } from 'jotai';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import { DashboardBarChart } from './dashboard-bar-chart';
describe('DashboardBarChart', () => {
test('should render successfully', async () => {
const { baseElement } = render(
-
+
- ,
+ ,
);
await act(async () => {
expect(baseElement).toBeTruthy();
diff --git a/src/pages/dashboard/dashboard-pie-chart/dashboard-pie-chart.test.tsx b/src/pages/dashboard/dashboard-pie-chart/dashboard-pie-chart.test.tsx
index 54e12ac6..f521d10f 100644
--- a/src/pages/dashboard/dashboard-pie-chart/dashboard-pie-chart.test.tsx
+++ b/src/pages/dashboard/dashboard-pie-chart/dashboard-pie-chart.test.tsx
@@ -1,17 +1,17 @@
import { mockData } from '@src/data/spacecraft';
import { act, render } from '@testing-library/react';
+import { Provider } from 'jotai';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import { DashboardPieChart } from './dashboard-pie-chart';
describe('DashboardPieChart', () => {
test('should render successfully', async () => {
const { baseElement } = render(
-
+
- ,
+ ,
);
await act(async () => {
expect(baseElement).toBeTruthy();
diff --git a/src/pages/dashboard/dashboard-table/dashboard-table.test.tsx b/src/pages/dashboard/dashboard-table/dashboard-table.test.tsx
index 9b40e20e..d277f1de 100644
--- a/src/pages/dashboard/dashboard-table/dashboard-table.test.tsx
+++ b/src/pages/dashboard/dashboard-table/dashboard-table.test.tsx
@@ -1,28 +1,28 @@
import { mockData } from '@src/data/spacecraft';
import { act, render } from '@testing-library/react';
+import { Provider } from 'jotai';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import { DashboardTable } from './dashboard-table';
describe('DashboardTable', () => {
test('should render successfully', () => {
const { baseElement } = render(
-
+
- ,
+ ,
);
expect(baseElement).toBeTruthy();
});
test('should render with mock data', async () => {
const { baseElement } = render(
-
+
- ,
+ ,
);
await act(async () => {
expect(baseElement).toBeTruthy();
diff --git a/src/pages/dashboard/dashboard.test.tsx b/src/pages/dashboard/dashboard.test.tsx
index 5c3ad9dd..d9a6109e 100644
--- a/src/pages/dashboard/dashboard.test.tsx
+++ b/src/pages/dashboard/dashboard.test.tsx
@@ -3,9 +3,9 @@ import axios from '@src/utils/axios';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, render } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { User } from '../../types/user';
import { Dashboard } from './dashboard';
@@ -20,13 +20,13 @@ describe('Dashboard', () => {
});
const componentWrapper = (
-
+
-
+
);
diff --git a/src/pages/details/details.test.tsx b/src/pages/details/details.test.tsx
index 62c6e8b2..266d446a 100644
--- a/src/pages/details/details.test.tsx
+++ b/src/pages/details/details.test.tsx
@@ -3,9 +3,9 @@ import axios from '@src/utils/axios';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { User } from '../../types/user';
import { Details } from './details';
@@ -30,13 +30,13 @@ describe('Details', () => {
});
const componentWrapper = (
-
+
-
+
);
diff --git a/src/pages/home/home.test.tsx b/src/pages/home/home.test.tsx
index 0ec19a13..1bc41da4 100644
--- a/src/pages/home/home.test.tsx
+++ b/src/pages/home/home.test.tsx
@@ -1,7 +1,7 @@
import { act, render } from '@testing-library/react';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { User } from '../../types/user';
import { Home } from './home';
@@ -9,11 +9,11 @@ import { Home } from './home';
describe('Home', () => {
const componentWrapper = (
-
+
-
+
);
diff --git a/src/pages/search-results/search-results.test.tsx b/src/pages/search-results/search-results.test.tsx
index 5282a109..9120f988 100644
--- a/src/pages/search-results/search-results.test.tsx
+++ b/src/pages/search-results/search-results.test.tsx
@@ -4,9 +4,9 @@ import axios from '@src/utils/axios';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, render } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
import { BrowserRouter } from 'react-router-dom';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { SearchResults } from './search-results';
@@ -21,13 +21,13 @@ describe('SearchResults', () => {
const componentWrapper = (
-
+
-
+
);
diff --git a/src/pages/sign-in/sign-in.test.tsx b/src/pages/sign-in/sign-in.test.tsx
index 882185f6..2f538445 100644
--- a/src/pages/sign-in/sign-in.test.tsx
+++ b/src/pages/sign-in/sign-in.test.tsx
@@ -2,8 +2,8 @@ import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
+import { Provider } from 'jotai';
import { AuthProvider } from 'react-oidc-context';
-import { RecoilRoot } from 'recoil';
import * as useAuthMock from '../../hooks/use-auth';
import { User } from '../../types/user';
import { SignIn } from './sign-in';
@@ -11,11 +11,11 @@ import { SignIn } from './sign-in';
describe('SignIn', () => {
const signInComponent = (
-
+
-
+
);
diff --git a/src/store.ts b/src/store.ts
index 7ed2ce15..f224778f 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -1,14 +1,7 @@
import { User } from '@src/types/user';
-import { atom } from 'recoil';
+import { atom } from 'jotai';
-const signedInState = atom({
- key: 'signedIn',
- default: false,
-});
-
-const currentUserState = atom({
- key: 'currentUser',
- default: undefined,
-});
+const signedInState = atom(false);
+const currentUserState = atom(undefined);
export { currentUserState, signedInState };