From 8b8e8e3056a7e07c4d33982f7ba6bbba362a1b71 Mon Sep 17 00:00:00 2001 From: Gloria Loi Date: Tue, 31 Aug 2021 15:31:45 +0300 Subject: [PATCH 1/3] Replace Redux with React Context --- src/app.jsx | 49 +++++++---- src/components/toast/toast.jsx | 16 ++-- src/components/toast/toast.stories.jsx | 22 ++--- .../header/components/user-menu/user-menu.jsx | 10 +-- src/pages/forgot/forgot.jsx | 14 ++-- src/pages/profile/profile.jsx | 31 +++---- src/pages/reset/reset.jsx | 13 ++- src/pages/sign-in/sign-in.jsx | 14 ++-- src/pages/sign-up/sign-up.jsx | 13 ++- src/resources/store.js | 16 ---- src/resources/store.jsx | 45 ++++++++++ src/resources/toast/toast.actions.js | 19 +++++ src/resources/toast/toast.selectors.js | 1 - src/resources/toast/toast.slice.js | 54 ------------ src/resources/user/user.actions.js | 78 +++++++++++++++++ src/resources/user/user.handlers.js | 22 ----- src/resources/user/user.selectors.js | 1 - src/resources/user/user.slice.js | 83 ------------------- 18 files changed, 236 insertions(+), 265 deletions(-) delete mode 100644 src/resources/store.js create mode 100644 src/resources/store.jsx create mode 100644 src/resources/toast/toast.actions.js delete mode 100644 src/resources/toast/toast.selectors.js delete mode 100644 src/resources/toast/toast.slice.js create mode 100644 src/resources/user/user.actions.js delete mode 100644 src/resources/user/user.handlers.js delete mode 100644 src/resources/user/user.selectors.js delete mode 100644 src/resources/user/user.slice.js diff --git a/src/app.jsx b/src/app.jsx index 6e646e6..ebeeeb4 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -1,16 +1,14 @@ -import React from 'react'; -import { Provider, useSelector } from 'react-redux'; +import React, { useContext } from 'react'; import { Router } from 'react-router'; import { Switch, Route, Redirect } from 'react-router-dom'; import history from 'services/history.service'; import * as loaderService from 'services/loader.service'; import * as socketService from 'services/socket.service'; +import api from 'services/api.service'; -import store from 'resources/store'; - -import * as userSelectors from 'resources/user/user.selectors'; -import { userActions } from 'resources/user/user.slice'; +import { ContextStoreProvider, StoreContext } from 'resources/store'; +import { userActions } from 'resources/user/user.actions'; import Toast from 'components/toast'; import Loading from 'components/loading'; @@ -26,19 +24,33 @@ import Reset from 'pages/reset'; import Home from 'pages/home'; import NotFound from 'pages/not-found'; -import 'resources/user/user.handlers'; - import 'styles/main.pcss'; const Profile = React.lazy(() => import('./pages/profile')); function PrivateScope({ children }) { - const user = useSelector(userSelectors.selectUser); + const { state, dispatch } = useContext(StoreContext); + const { user } = state; React.useEffect(() => { socketService.connect(); + + api.on('error', (error) => { + if (error.status === 401) { + userActions.signOut(dispatch); + } + }); + + socketService.on('connect', () => { + socketService.emit('subscribe', `user-${user?._id}`); + }); + + socketService.on('user:updated', (updatedUser) => { + userActions.setUser(dispatch, { user: updatedUser }); + }); + return () => socketService.disconnect(); - }, []); + }, [user?._id, dispatch]); if (!user) { const searchParams = new URLSearchParams({ to: window.location.pathname }); @@ -86,11 +98,12 @@ Object.values(scope).forEach((s, scopeIndex) => { function App() { const [loading, setLoading] = React.useState(true); + const { dispatch } = useContext(StoreContext); React.useEffect(() => { async function init() { try { - await store.dispatch(userActions.getCurrentUser()); + await userActions.getCurrentUser(dispatch); } catch (error) { // @todo: add something like sentry } finally { @@ -100,12 +113,12 @@ function App() { } init(); - }, []); + }, [dispatch]); if (loading) return null; return ( - + <> Error!}> }> @@ -135,8 +148,14 @@ function App() { - + ); } -export default App; +const Root = () => ( + + + +); + +export default Root; diff --git a/src/components/toast/toast.jsx b/src/components/toast/toast.jsx index 8374d89..0d4df8f 100644 --- a/src/components/toast/toast.jsx +++ b/src/components/toast/toast.jsx @@ -1,10 +1,9 @@ -import React from 'react'; +import React, { useContext } from 'react'; import cn from 'classnames'; import ReactDOM from 'react-dom'; -import { useSelector, useDispatch } from 'react-redux'; -import * as toastSelectors from 'resources/toast/toast.selectors'; -import { toastActions } from 'resources/toast/toast.slice'; +import { StoreContext } from 'resources/store'; +import { toastActions } from 'resources/toast/toast.actions'; import Icon from 'components/icon'; import IconButton from 'components/icon-button'; @@ -39,9 +38,8 @@ function getIconProps(type) { } function RawToast() { - const dispatch = useDispatch(); - - const messages = useSelector(toastSelectors.selectMessages); + const { state, dispatch } = useContext(StoreContext); + const { toast: messages } = state; const element = React.useRef(document.createElement('div')); @@ -59,7 +57,7 @@ function RawToast() { <> {messages.map((m) => { const closeToast = () => { - dispatch(toastActions.remove({ id: m.id })); + toastActions.removeToast(dispatch, m.id); }; const iconProps = getIconProps(m.type); @@ -74,7 +72,7 @@ function RawToast() { color={iconProps.color} noWrapper /> - {m.text} + {m.message} ( - + - + ), ], }; export const Template = () => { - const dispatch = useDispatch(); + const { dispatch } = useContext(StoreContext); const showSuccessToast = () => { - dispatch(toastActions.success('This is success toast! This is success toast!')); + toastActions.createToast(dispatch, { type: 'success', message: 'This is success toast! This is success toast!' }); }; const showErrorToast = () => { - dispatch(toastActions.error('This is error toast! This is error toast!')); + toastActions.createToast(dispatch, { type: 'error', message: 'This is error toast! This is error toast!' }); }; const showInfoToast = () => { - dispatch(toastActions.info('This is info toast! This is info toast!')); + toastActions.createToast(dispatch, { type: 'info', message: 'This is info toast! This is info toast!' }); }; const showWarningToast = () => { - dispatch(toastActions.warning('This is warning toast! This is warning toast!')); + toastActions.createToast(dispatch, { type: 'warning', message: 'This is warning toast! This is warning toast!' }); }; return ( diff --git a/src/layouts/main/header/components/user-menu/user-menu.jsx b/src/layouts/main/header/components/user-menu/user-menu.jsx index 635362e..2649c58 100644 --- a/src/layouts/main/header/components/user-menu/user-menu.jsx +++ b/src/layouts/main/header/components/user-menu/user-menu.jsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useContext } from 'react'; import cn from 'classnames'; import { Link, useHistory } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; import { routes } from 'routes'; -import { userActions } from 'resources/user/user.slice'; +import { StoreContext } from 'resources/store'; +import { userActions } from 'resources/user/user.actions'; import styles from './user-menu.styles.pcss'; @@ -17,8 +17,8 @@ const linksList = [ ]; function UserMenu() { - const dispatch = useDispatch(); const history = useHistory(); + const { dispatch } = useContext(StoreContext); const [isOpen, setIsOpen] = React.useState(false); function toggleIsOpen() { @@ -36,7 +36,7 @@ function UserMenu() { }, []); async function logout() { - await dispatch(userActions.signOut()); + await userActions.signOut(dispatch); history.push(routes.signIn.path); } diff --git a/src/pages/forgot/forgot.jsx b/src/pages/forgot/forgot.jsx index 52a8bdb..f11eeb7 100644 --- a/src/pages/forgot/forgot.jsx +++ b/src/pages/forgot/forgot.jsx @@ -1,12 +1,11 @@ -import React from 'react'; +import React, { useContext } from 'react'; import cn from 'classnames'; import { Link, Redirect } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; import { routes } from 'routes'; -import * as userSelectors from 'resources/user/user.selectors'; -import { userActions } from 'resources/user/user.slice'; +import { StoreContext } from 'resources/store'; +import { userActions } from 'resources/user/user.actions'; import Input from 'components/input'; import Button from 'components/button'; @@ -15,9 +14,8 @@ import Form from 'components/form'; import styles from './forgot.pcss'; const Forgot = () => { - const dispatch = useDispatch(); - - const user = useSelector(userSelectors.selectUser); + const { state, dispatch } = useContext(StoreContext); + const { user } = state; const [values, setValues] = React.useState({}); const [pending, setPending] = React.useState(false); @@ -25,7 +23,7 @@ const Forgot = () => { const handleSubmit = async (submitValues) => { setPending(true); - await dispatch(userActions.forgot(submitValues)); + await userActions.forgot(dispatch, submitValues); setValues(submitValues); setSubmitted(true); setPending(false); diff --git a/src/pages/profile/profile.jsx b/src/pages/profile/profile.jsx index 20aa13d..0dff701 100644 --- a/src/pages/profile/profile.jsx +++ b/src/pages/profile/profile.jsx @@ -1,11 +1,13 @@ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { + useCallback, useState, useEffect, useContext, +} from 'react'; import * as yup from 'yup'; -import { useDispatch, useSelector } from 'react-redux'; import { useFormContext } from 'react-hook-form'; -import * as userSelectors from 'resources/user/user.selectors'; -import { userActions } from 'resources/user/user.slice'; -import { toastActions } from 'resources/toast/toast.slice'; +import { StoreContext } from 'resources/store'; +import { userActions } from 'resources/user/user.actions'; +import { toastActions } from 'resources/toast/toast.actions'; + import * as filesApi from 'resources/files/files.api'; import Input from 'components/input'; @@ -30,9 +32,8 @@ const schema = yup.object({ .email('Please enter a valid email address'), }); -const CancelButton = ({ onCancel }) => { // eslint-disable-line react/prop-types +const CancelButton = ({ onCancel, user }) => { // eslint-disable-line react/prop-types const { reset } = useFormContext(); - const user = useSelector(userSelectors.selectUser); const handleClick = () => { onCancel(); @@ -52,16 +53,15 @@ const CancelButton = ({ onCancel }) => { // eslint-disable-line react/prop-types }; const Profile = () => { - const dispatch = useDispatch(); - const user = useSelector(userSelectors.selectUser); + const { state: { user }, dispatch } = useContext(StoreContext); const { avatarFileKey: userAvatarFileKey } = user; const [avatarFileKey, setAvatarFileKey] = useState(userAvatarFileKey); const [avatarUrl, setAvatarUrl] = useState(null); const handleSubmit = useCallback(async (submitValues) => { - await dispatch(userActions.updateCurrentUser({ ...submitValues, avatarFileKey })); - dispatch(toastActions.success('User info updated!')); + await userActions.updateCurrentUser(dispatch, { ...submitValues, avatarFileKey }); + toastActions.createToast(dispatch, { type: 'success', message: 'User info updated!' }); }, [dispatch, avatarFileKey]); const getAvatarUrl = useCallback(async (key) => { @@ -70,8 +70,8 @@ const Profile = () => { if (url) { setAvatarUrl(url); } - } catch (error) { - dispatch(toastActions.error(error)); + } catch ({ data }) { + toastActions.createToast(dispatch, { type: 'error', message: data.errors.file[0] }); } }, [dispatch]); @@ -80,8 +80,8 @@ const Profile = () => { const file = files[0]; const { key } = await filesApi.upload(file); setAvatarFileKey(key); - } catch (error) { - dispatch(toastActions.error(error)); + } catch ({ data }) { + toastActions.createToast(dispatch, { type: 'error', message: data.errors.file[0] }); } }; @@ -162,6 +162,7 @@ const Profile = () => {