Skip to content

Commit

Permalink
recoil to jotai (#6668)
Browse files Browse the repository at this point in the history
* initial setup

* replace recoil in components, stories and tests

* update in SSR and CSR

* change redux legacy updates method

* fixed test
  • Loading branch information
Zasa-san authored Apr 15, 2024
1 parent 8d8c9bd commit 5822b6a
Show file tree
Hide file tree
Showing 42 changed files with 247 additions and 267 deletions.
7 changes: 2 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,9 @@ module.exports = {
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/mouse-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off',
//react-hooks && recoil
//react-hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'warn',
{ additionalHooks: '(useRecoilCallback|useRecoilTransaction_UNSTABLE)' },
],
'react-hooks/exhaustive-deps': ['warn'],
},
overrides: [
{ files: ['app/**/*spec.js'], rules: { 'max-lines-per-function': 'off' } },
Expand Down
36 changes: 25 additions & 11 deletions app/react/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { MutableSnapshot, RecoilRoot } from 'recoil';
import { Provider } from 'react-redux';
import { createStore, Provider } from 'jotai';
import { Provider as ReduxProvider } from 'react-redux';
import { getRoutes } from './Routes';
import CustomProvider from './App/Provider';
import { settingsAtom, templatesAtom, translationsAtom } from './V2/atoms';
import { relationshipTypesAtom } from './V2/atoms/relationshipTypes';
import { store } from './store';

const reduxState = store?.getState();
Expand All @@ -14,20 +15,33 @@ const templates = reduxState?.templates.toJS() || [];

const router = createBrowserRouter(getRoutes(settings, reduxState?.user.get('_id')));

const recoilGlobalState = ({ set }: MutableSnapshot) => {
set(settingsAtom, settings);
set(templatesAtom, templates);
set(translationsAtom, { locale: reduxState?.locale || 'en' });
};
const atomStore = createStore();
atomStore.set(settingsAtom, settings);
atomStore.set(templatesAtom, templates);
atomStore.set(translationsAtom, { locale: reduxState?.locale || 'en' });

//sync deprecated redux store
atomStore.sub(settingsAtom, () => {
const value = atomStore.get(settingsAtom);
store?.dispatch({ type: 'settings/collection/SET', value });
});
atomStore.sub(templatesAtom, () => {
const value = atomStore.get(templatesAtom);
store?.dispatch({ type: 'templates/SET', value });
});
atomStore.sub(relationshipTypesAtom, () => {
const value = atomStore.get(relationshipTypesAtom);
store?.dispatch({ type: 'relationTypes/SET', value });
});

const App = () => (
<Provider store={store as any}>
<ReduxProvider store={store as any}>
<CustomProvider>
<RecoilRoot initializeState={recoilGlobalState}>
<Provider store={atomStore}>
<RouterProvider router={router} fallbackElement={null} />
</RecoilRoot>
</Provider>
</CustomProvider>
</Provider>
</ReduxProvider>
);

export { App };
5 changes: 2 additions & 3 deletions app/react/App/App.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Outlet, useLocation, useParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useSetAtom } from 'jotai';
import Notifications from 'app/Notifications';
import Cookiepopup from 'app/App/Cookiepopup';
import { TranslateForm, t } from 'app/I18N';
import { Icon } from 'UI';

import { socket } from 'app/socket';
import { NotificationsContainer } from 'V2/Components/UI';
import { settingsAtom } from 'app/V2/atoms/settingsAtom';
Expand All @@ -27,7 +26,7 @@ import 'flowbite';
const App = ({ customParams }) => {
const [showMenu, setShowMenu] = useState(false);
const [confirmOptions, setConfirmOptions] = useState({});
const setSettings = useSetRecoilState(settingsAtom);
const setSettings = useSetAtom(settingsAtom);

const location = useLocation();
const params = useParams();
Expand Down
4 changes: 2 additions & 2 deletions app/react/Templates/components/MetadataTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { actions as formActions, Control, Field } from 'react-redux-form';
import { useSetAtom } from 'jotai';
import { Icon } from 'UI';
import { withContext } from 'app/componentWrappers';
import { FormGroup } from 'app/Forms';
Expand All @@ -16,7 +17,6 @@ import { I18NLink, t, Translate } from 'app/I18N';
import { notificationActions } from 'app/Notifications';
import { notify } from 'app/Notifications/actions/notificationsActions';
import { templatesAtom } from 'app/V2/atoms';
import { useSetRecoilState } from 'recoil';
import api from 'app/Templates/TemplatesAPI';
import {
addProperty,
Expand Down Expand Up @@ -329,7 +329,7 @@ const target = {
const withTemplatesAtom =
<T,>(Comp: React.ComponentClass<T, any>) =>
(props: T) => {
const updateTemplatesAtom = useSetRecoilState(templatesAtom);
const updateTemplatesAtom = useSetAtom(templatesAtom);
return <Comp {...props} updateTemplatesAtom={updateTemplatesAtom} />;
};

Expand Down
3 changes: 1 addition & 2 deletions app/react/V2/Components/Forms/ConfirmNavigationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import { SetterOrUpdater } from 'recoil';
import { Translate } from 'app/I18N';
import { Button, Modal } from '../UI';

type confirmationModalType = {
setShowModal: SetterOrUpdater<boolean>;
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm?: () => void;
};

Expand Down
17 changes: 9 additions & 8 deletions app/react/V2/Components/UI/NotificationsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React, { useEffect, useState } from 'react';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { useAtomValue } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { notificationAtom } from 'V2/atoms';
import { Notification } from 'V2/Components/UI/Notification';

const NotificationsContainer = () => {
const timeout = 6000;
const [timerId, setTimerId] = useState<NodeJS.Timeout>();
const notification = useRecoilValue(notificationAtom);
const resetRecoilAtom = useResetRecoilState(notificationAtom);
const notification = useAtomValue(notificationAtom);
const resetAtom = useResetAtom(notificationAtom);
const notificationIsSet = Boolean(notification.text && notification.type);

useEffect(() => {
if (notificationIsSet && !timerId) {
const timer = setTimeout(() => {
resetRecoilAtom();
resetAtom();
}, timeout);

setTimerId(timer);
Expand All @@ -25,10 +26,10 @@ const NotificationsContainer = () => {
setTimerId(undefined);
}
};
}, [resetRecoilAtom, notificationIsSet, timerId]);
}, [resetAtom, notificationIsSet, timerId]);

const onClickHandler = () => {
resetRecoilAtom();
resetAtom();
};

const handleMouseEnter = () => timerId && clearTimeout(timerId);
Expand All @@ -37,7 +38,7 @@ const NotificationsContainer = () => {
if (timerId) clearTimeout(timerId);

const timer = setTimeout(() => {
resetRecoilAtom();
resetAtom();
}, timeout);

setTimerId(timer);
Expand All @@ -52,7 +53,7 @@ const NotificationsContainer = () => {
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className="fixed bottom-1 left-2 md:w-2/5 w-4/5 z-10"
className="fixed bottom-1 left-2 z-10 w-4/5 md:w-2/5"
>
<div className="shadow-lg" role="alert">
<Notification
Expand Down
14 changes: 7 additions & 7 deletions app/react/V2/Components/UI/specs/NotificationsContainer.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { Provider, useSetAtom } from 'jotai';
import { mount } from '@cypress/react18';
import { Provider } from 'react-redux';
import { Provider as ReduxProvider } from 'react-redux';
import { LEGACY_createStore as createStore } from 'V2/shared/testingHelpers';
import { notificationAtom, notificationAtomType } from 'V2/atoms';
import { NotificationsContainer } from '../NotificationsContainer';
Expand All @@ -14,21 +14,21 @@ describe('Notifications container', () => {
};

const Component = () => {
const setNotification = useSetRecoilState(notificationAtom);
const setNotification = useSetAtom(notificationAtom);

const onClick = () => {
setNotification(notification);
};

return (
<Provider store={createStore()}>
<ReduxProvider store={createStore()}>
<>
<NotificationsContainer />
<button type="button" id="send-notification" onClick={onClick}>
Send notification
</button>
</>
</Provider>
</ReduxProvider>
);
};

Expand All @@ -38,9 +38,9 @@ describe('Notifications container', () => {

beforeEach(() => {
mount(
<RecoilRoot>
<Provider>
<Component />
</RecoilRoot>
</Provider>
);
});

Expand Down
28 changes: 15 additions & 13 deletions app/react/V2/CustomHooks/specs/useApiCaller.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
* @jest-environment jsdom
*/
import { act, renderHook } from '@testing-library/react';
import * as recoil from 'recoil';
import { RecoilRoot, RecoilState } from 'recoil';
import { Provider } from 'jotai';
import { RequestParams } from 'app/utils/RequestParams';
import { Translate } from 'app/I18N';
import React from 'react';
import { useApiCaller } from '../useApiCaller';

const mockSetNotification = jest.fn();

jest.mock('jotai', () => ({
...jest.requireActual('jotai'),
useSetAtom: () => mockSetNotification,
}));

describe('describe useApiCaller', () => {
let apiCallerHook: {
current: {
Expand All @@ -19,14 +25,9 @@ describe('describe useApiCaller', () => {
) => any;
};
};
const setNotificationMock = jest.fn();

beforeEach(() => {
jest
.spyOn(recoil, 'useSetRecoilState')
.mockImplementation((_state: RecoilState<any>) => setNotificationMock);

({ result: apiCallerHook } = renderHook(() => useApiCaller(), { wrapper: RecoilRoot }));
({ result: apiCallerHook } = renderHook(() => useApiCaller(), { wrapper: Provider }));
});

afterEach(() => {
Expand All @@ -41,20 +42,21 @@ describe('describe useApiCaller', () => {
new RequestParams({ data: 'paramid' }),
<Translate>successful action</Translate>
);
expect(setNotificationMock).toHaveBeenCalled();

expect(mockSetNotification).toHaveBeenCalled();

if (success) {
expect(await apiResult.data).toEqual({ data: 'result' });
expect(await apiResult.error).toBeUndefined();
expect(setNotificationMock.mock.calls[0][0].type).toEqual('success');
expect(setNotificationMock.mock.calls[0][0].text.props.children).toEqual(
expect(mockSetNotification.mock.calls[0][0].type).toEqual('success');
expect(mockSetNotification.mock.calls[0][0].text.props.children).toEqual(
'successful action'
);
} else {
expect(await apiResult.data).toBeUndefined();
expect(await apiResult.error).toEqual('An error occurred');
expect(setNotificationMock.mock.calls[0][0].type).toEqual('error');
expect(setNotificationMock.mock.calls[0][0].text.props.children).toEqual(
expect(mockSetNotification.mock.calls[0][0].type).toEqual('error');
expect(mockSetNotification.mock.calls[0][0].text.props.children).toEqual(
'An error occurred'
);
}
Expand Down
4 changes: 2 additions & 2 deletions app/react/V2/CustomHooks/useApiCaller.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { useSetRecoilState } from 'recoil';
import { useSetAtom } from 'jotai';
import { Translate } from 'app/I18N';
import { notificationAtom } from 'V2/atoms';
import { RequestParams } from 'app/utils/RequestParams';
Expand All @@ -16,7 +16,7 @@ const getError = async (res: Response) => {
};

const useApiCaller = () => {
const setNotifications = useSetRecoilState(notificationAtom);
const setNotifications = useSetAtom(notificationAtom);

const handleSuccess = async (res: Response, successMessageComponent: React.ReactNode) => {
setNotifications({
Expand Down
10 changes: 5 additions & 5 deletions app/react/V2/Routes/Settings/Account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UserSchema } from 'shared/types/userType';

import { LoaderFunction, useLoaderData, useRevalidator } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { useSetRecoilState } from 'recoil';
import { useSetAtom } from 'jotai';
import { notificationAtom } from 'app/V2/atoms';

import { InputField } from 'app/V2/Components/Forms';
Expand All @@ -24,7 +24,7 @@ const accountLoader =
const Account = () => {
const userAccount = useLoaderData() as UserSchema;
const [isSidepanelOpen, setIsSidepanelOpen] = useState(false);
const setNotifications = useSetRecoilState(notificationAtom);
const setNotifications = useSetAtom(notificationAtom);
const revalidator = useRevalidator();

type AccountForm = UserSchema & { passwordConfirm?: string };
Expand Down Expand Up @@ -125,7 +125,7 @@ const Account = () => {
title={<Translate>Two-Factor Authentication</Translate>}
color="default"
>
<div className="flex items-center gap-6">
<div className="flex gap-6 items-center">
<Button color="success" disabled className="flex-none">
<Translate>Activated</Translate>
</Button>
Expand All @@ -142,7 +142,7 @@ const Account = () => {
title={<Translate>Two-Factor Authentication</Translate>}
color="yellow"
>
<div className="flex items-center gap-6">
<div className="flex gap-6 items-center">
<Button
styling="outline"
className="flex-none"
Expand All @@ -165,7 +165,7 @@ const Account = () => {
<a
href="/logout"
data-testid="account-logout"
className="px-3 py-2 text-xs font-medium bg-white border rounded-lg hover:text-white text-error-600 border-error-600 hover:bg-error-800 hover:border-error-800 focus:outline-none focus:ring-4 focus:ring-indigo-200"
className="px-3 py-2 text-xs font-medium bg-white rounded-lg border hover:text-white text-error-600 border-error-600 hover:bg-error-800 hover:border-error-800 focus:outline-none focus:ring-4 focus:ring-indigo-200"
>
<Translate>Logout</Translate>
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import api from 'app/utils/api';
import { useRevalidator } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useSetAtom } from 'jotai';

import { RequestParams } from 'app/utils/RequestParams';

Expand All @@ -28,7 +28,7 @@ const TwoFactorSetup = ({ closePanel, isOpen }: TwoFactorSetupProps) => {
const [token, setToken] = useState('');
const [_secret, setSecret] = useState('');
const [_otpauth, setOtpauth] = useState('');
const setNotifications = useSetRecoilState(notificationAtom);
const setNotifications = useSetAtom(notificationAtom);
const revalidator = useRevalidator();
const [tokenError, setTokenError] = useState(false);

Expand Down Expand Up @@ -122,7 +122,7 @@ const TwoFactorSetup = ({ closePanel, isOpen }: TwoFactorSetupProps) => {
<Card className="mb-4 sm:col-span-3" title={<Translate>Secret keys</Translate>}>
<CopyValueInput
value={_secret}
className="w-full mb-4"
className="mb-4 w-full"
label={
<>
<Translate className="block">
Expand Down Expand Up @@ -157,7 +157,7 @@ const TwoFactorSetup = ({ closePanel, isOpen }: TwoFactorSetupProps) => {
</div>
</Sidepanel.Body>
<Sidepanel.Footer className="px-4 py-3">
<div className="flex w-full gap-2">
<div className="flex gap-2 w-full">
<Button styling="light" onClick={closePanel} className="grow">
<Translate>Cancel</Translate>
</Button>
Expand Down
Loading

0 comments on commit 5822b6a

Please sign in to comment.