From 5efa8ca6d3a41c72e3623a551f442cbf702db196 Mon Sep 17 00:00:00 2001 From: Arad Margalit Date: Sat, 2 Jan 2021 12:38:21 -0800 Subject: [PATCH] useProtected (#79) --- .../components/DraftPage/DraftPage-redux.tsx | 11 ++--- client/src/components/Drafts/Drafts-redux.tsx | 19 +++------ client/src/components/Home/Home-container.tsx | 9 ++--- client/src/components/Home/Home-redux.tsx | 19 ++------- client/src/components/Home/Home-view.tsx | 11 +---- client/src/components/Home/Home.test.tsx | 7 ---- client/src/components/Login/Login.tsx | 6 +-- client/src/hooks/useProtected/index.ts | 1 + .../hooks/useProtected/useProtected.test.tsx | 40 +++++++++++++++++++ client/src/hooks/useProtected/useProtected.ts | 35 ++++++++++++++++ client/src/reducers/reducer_auth.ts | 2 +- 11 files changed, 96 insertions(+), 64 deletions(-) create mode 100644 client/src/hooks/useProtected/index.ts create mode 100644 client/src/hooks/useProtected/useProtected.test.tsx create mode 100644 client/src/hooks/useProtected/useProtected.ts diff --git a/client/src/components/DraftPage/DraftPage-redux.tsx b/client/src/components/DraftPage/DraftPage-redux.tsx index d73f3827..d97e9183 100644 --- a/client/src/components/DraftPage/DraftPage-redux.tsx +++ b/client/src/components/DraftPage/DraftPage-redux.tsx @@ -1,15 +1,10 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { Redirect } from 'react-router-dom'; -import { RootState } from '../../reducers'; -import { User } from '../../types'; +import { useProtected } from '../../hooks/useProtected'; + import Drafts from '../Drafts'; function DraftPageRedux(): JSX.Element { - const user: User = useSelector((state: RootState) => state.auth); - - if (!user) return ; - + useProtected(); return (
diff --git a/client/src/components/Drafts/Drafts-redux.tsx b/client/src/components/Drafts/Drafts-redux.tsx index b7424346..ed1fbb9f 100644 --- a/client/src/components/Drafts/Drafts-redux.tsx +++ b/client/src/components/Drafts/Drafts-redux.tsx @@ -1,8 +1,8 @@ import axios from 'axios'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { Redirect } from 'react-router-dom'; -import { setReview, updateDraftId, updateDrafts, fetchUser } from '../../actions'; +import { useHistory } from 'react-router-dom'; +import { setReview, updateDraftId, updateDrafts } from '../../actions'; import { RootState } from '../../reducers'; import { Review } from '../../types'; import SearchableReviewDisplay from '../SearchableReviewDisplay'; @@ -11,16 +11,7 @@ export default function DraftsRedux(): JSX.Element { const dispatch = useDispatch(); const drafts: Review[] = useSelector((state: RootState) => state.drafts); const renderMath: boolean = useSelector((state: RootState) => state.user.renderMath); - const [redirectHome, setRedirectHome] = useState(false); - - useEffect(() => { - dispatch(fetchUser()); - }, [dispatch]); - - // go home if back arrow is pressed - if (redirectHome) { - return ; - } + const { back } = useHistory(); // function to delete the specified draft const deleteDraft = (draftToDelete: Review) => { @@ -38,7 +29,7 @@ export default function DraftsRedux(): JSX.Element { const pageHeaderProps = { title: 'Your Drafts', - onBack: () => setRedirectHome(true), + onBack: () => back(), }; return ( diff --git a/client/src/components/Home/Home-container.tsx b/client/src/components/Home/Home-container.tsx index 6e8c3ba1..d69857ba 100644 --- a/client/src/components/Home/Home-container.tsx +++ b/client/src/components/Home/Home-container.tsx @@ -1,9 +1,6 @@ import React from 'react'; -import HomeView, { HomeViewProps } from './Home-view'; +import HomeView from './Home-view'; -// These are the same, for now. If they ever change, replace this alias. -type HomeContainerProps = HomeViewProps; - -export default function HomeContainer(props: HomeContainerProps): JSX.Element { - return ; +export default function HomeContainer(): JSX.Element { + return ; } diff --git a/client/src/components/Home/Home-redux.tsx b/client/src/components/Home/Home-redux.tsx index 06649139..c36da6be 100644 --- a/client/src/components/Home/Home-redux.tsx +++ b/client/src/components/Home/Home-redux.tsx @@ -1,19 +1,8 @@ -import React, { useEffect } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { fetchUser } from '../../actions'; -import { RootState } from '../../reducers'; -import { User } from '../../types'; +import React from 'react'; +import { useProtected } from '../../hooks/useProtected'; import HomeContainer from './Home-container'; export default function HomeRedux(): JSX.Element { - const dispatch = useDispatch(); - const user: User = useSelector((state: RootState) => state.auth); - - // by passing [dispatch] as the second argument of useEffect, we replicate the behavior - // of componentDidMount + componentDidUnmount, but not componentDidUpdate - useEffect(() => { - dispatch(fetchUser()); - }, [dispatch]); - - return ; + useProtected(); + return ; } diff --git a/client/src/components/Home/Home-view.tsx b/client/src/components/Home/Home-view.tsx index d62f2cee..94074163 100644 --- a/client/src/components/Home/Home-view.tsx +++ b/client/src/components/Home/Home-view.tsx @@ -1,21 +1,12 @@ import React from 'react'; -import { Redirect } from 'react-router-dom'; import { Row, Col } from 'antd'; import PaperSearchBar from '../PaperSearchBar'; import ReadingList from '../ReadingList'; import ReviewReader from '../ReviewReader'; import './Home.scss'; -import { User } from '../../types'; -import { blankUser } from '../../templates'; - -export interface HomeViewProps { - user: User; -} - -export default function HomeView({ user }: HomeViewProps): JSX.Element { - if (user === blankUser) return ; +export default function HomeView(): JSX.Element { return (
diff --git a/client/src/components/Home/Home.test.tsx b/client/src/components/Home/Home.test.tsx index 78427285..734832e1 100644 --- a/client/src/components/Home/Home.test.tsx +++ b/client/src/components/Home/Home.test.tsx @@ -10,13 +10,6 @@ import { RootState } from '../../reducers'; const renderHome = (initialState?: RootState) => renderWithRouterRedux(, { initialState }); describe('', () => { - describe('when nobody is logged in', () => { - it('redirects when no user is present', () => { - renderWithRouterRedux(, { redirectTo: '/' }); - expect(screen.getByText(/Redirected to a new page./)).toBeDefined(); - }); - }); - describe('with a user logged in', () => { const initialState: RootState = { ...getBlankInitialState(), auth: { ...blankUser, displayName: 'Jim Henderson' } }; diff --git a/client/src/components/Login/Login.tsx b/client/src/components/Login/Login.tsx index c74d0c6b..a06c3642 100644 --- a/client/src/components/Login/Login.tsx +++ b/client/src/components/Login/Login.tsx @@ -1,8 +1,8 @@ /* eslint-disable react/no-unescaped-entities */ -import React, { useEffect } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; -import { Card, Col, notification, Row } from 'antd'; -import { CalendarOutlined, SmileOutlined, TeamOutlined } from '@ant-design/icons'; +import { Card, Col, Row } from 'antd'; +import { CalendarOutlined, TeamOutlined } from '@ant-design/icons'; import { Redirect } from 'react-router-dom'; import LazyHero from 'react-lazy-hero'; import { Location } from 'history'; diff --git a/client/src/hooks/useProtected/index.ts b/client/src/hooks/useProtected/index.ts new file mode 100644 index 00000000..ed86d48a --- /dev/null +++ b/client/src/hooks/useProtected/index.ts @@ -0,0 +1 @@ +export * from './useProtected'; diff --git a/client/src/hooks/useProtected/useProtected.test.tsx b/client/src/hooks/useProtected/useProtected.test.tsx new file mode 100644 index 00000000..a0dd70ff --- /dev/null +++ b/client/src/hooks/useProtected/useProtected.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useProtected } from '.'; +import { blankUser } from '../../templates'; +import { getBlankInitialState, renderWithRouterRedux } from '../../testUtils/reduxRender'; + +const mockHistoryPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(jest.requireActual('react-router-dom') as any), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +// Because you can't run a hook outside of a react component, we'll make a small test case: +function TestComponent({ redirect }: { redirect?: string }): JSX.Element { + useProtected({ redirectTo: redirect }); + return

Hi

; +} + +describe('useProtected', () => { + describe('redirect behavior', () => { + it('redirects to the default path if a user is present', () => { + renderWithRouterRedux(); + expect(mockHistoryPush).toHaveBeenCalledWith('/'); + }); + + it('redirects to a custom path if one is specific', () => { + renderWithRouterRedux(); + expect(mockHistoryPush).toHaveBeenCalledWith('customPath'); + }); + + it('does not redirect if no user is present', () => { + const initialState = { ...getBlankInitialState(), auth: { ...blankUser, displayName: 'John' } }; + renderWithRouterRedux(, { initialState }); + expect(mockHistoryPush).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/client/src/hooks/useProtected/useProtected.ts b/client/src/hooks/useProtected/useProtected.ts new file mode 100644 index 00000000..f0dfb2c8 --- /dev/null +++ b/client/src/hooks/useProtected/useProtected.ts @@ -0,0 +1,35 @@ +// This hook will check if the user is logged in. +// If yes, do nothing. +// If not, this will redirect the user to login. + +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { fetchUser } from '../../actions'; +import { RootState } from '../../reducers'; +import { initialState as initialBlankUser } from '../../reducers/reducer_auth'; + +import { User } from '../../types'; + +interface UseProtectedOptions { + redirectTo?: string; +} + +const DEFAULT_REDIRECT_PATH = '/'; + +export function useProtected(options?: UseProtectedOptions): void { + const { push } = useHistory(); + const dispatch = useDispatch(); + const auth: User = useSelector((state: RootState) => state.auth); + + // by passing [dispatch] as the second argument of useEffect, we replicate the behavior + // of componentDidMount + componentDidUnmount, but not componentDidUpdate + useEffect(() => { + dispatch(fetchUser()); + }, [dispatch]); + + // If this doesn't exist or is equivalent to an empty user, redirect + if (!auth || auth === initialBlankUser) { + push(options?.redirectTo || DEFAULT_REDIRECT_PATH); + } +} diff --git a/client/src/reducers/reducer_auth.ts b/client/src/reducers/reducer_auth.ts index e79facad..f826dd7d 100644 --- a/client/src/reducers/reducer_auth.ts +++ b/client/src/reducers/reducer_auth.ts @@ -4,7 +4,7 @@ import { FetchUserAction } from '../actions/types'; import { blankUser } from '../templates'; import { User } from '../types'; -const initialState: User = blankUser; +export const initialState: User = blankUser; const reducer: Reducer = (state = initialState, action) => { switch (action.type) {