I have been using React for close to a year know and understand a majority of the basics, however I have some questions regarding best practices (General or industry) when it comes to passing functions/ref/hooks and how it affects things like state flow and tests. I have been instantiating hooks such as useDispatch or useNavigation(React-router) in the App.tsx(js) file and then passing it down to all of my components who need to use it. I have been using this same concept for things like Axios and then within my components, I've been trying out passing MUI components(Grid, Card, etc) to my created component (i.e. LoginForm.tsx/js) where the initial rendering of the main component brings in those hooks instead of repeated instantiation throughout my project (Below for example). Is this breaking in standards or practices, such as SOLID OOP, and would this make testing harder down the line?
App.tsx
import { Dispatch, FC, Suspense, lazy } from "react";
import {
Navigate,
NavigateFunction,
Route,
Routes,
useNavigate,
useSearchParams,
} from "react-router-dom";
import {
HOMEPAGE,
LOGIN,
REDIRECT,
ROOM,
SEARCH,
SETUPROOM,
SIGNUP,
} from "./component/UI/Constatns";
import Layout from "./component/UI/Layout/Layout";
import { User } from "./types/types";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "./store/store";
import { Theme, useMediaQuery, useTheme } from "#mui/material";
import axios from "axios";
import LoadingSpinner from "./component/UI/LoadingSpinner";
const Homepage = lazy(() => import("./pages/Homepage"));
const Login = lazy(() => import("./pages/Login"));
const Signup = lazy(() => import("./pages/Signup"));
const Room = lazy(() => import("./pages/Room"));
const Search = lazy(() => import("./pages/subpages/Search"));
const CreateRoom = lazy(() => import("./pages/subpages/CreateRoom"));
const App: FC = () => {
const USER: User = useSelector((state: RootState) => state.user);
const theme: Theme = useTheme();
const isMobile: boolean = useMediaQuery(theme.breakpoints.down("md"));
const [params] = useSearchParams();
const dispatch: Dispatch<any> = useDispatch();
const navigation: NavigateFunction = useNavigate();
return (
<Suspense fallback={<LoadingSpinner />}>
<Layout>
<Routes>
<Route
path={HOMEPAGE}
element={
<Homepage
user={USER}
isMobile={isMobile}
axios={axios}
dispatch={dispatch}
param={params}
/>
}
/>
<Route
path={SEARCH}
element={
<Search
axios={axios}
dispatch={dispatch}
params={params}
nav={navigation}
isMobile={isMobile}
/>
}
/>
<Route
path={ROOM}
element={
<Room
isMobile={isMobile}
nav={navigation}
dispatch={dispatch}
param={params}
/>
}
/>
<Route
path={SETUPROOM}
element={
<CreateRoom
params={params}
axios={axios}
nav={navigation}
isMobile={isMobile}
user={USER}
/>
}
/>
<Route
path={LOGIN}
element={
<Login
nav={navigation}
isMobile={isMobile}
params={params}
axios={axios}
dispatch={dispatch}
/>
}
/>
<Route
path={SIGNUP}
element={
<Signup nav={navigation} isMobile={isMobile} axios={axios} />
}
/>
<Route path={REDIRECT} element={<Navigate replace to={HOMEPAGE} />} />
</Routes>
</Layout>
</Suspense>
);
};
export default App;
Example of MUI hooks
import { Button, Card, CardContent, Grid, TextField } from "#mui/material";
import { AxiosStatic } from "axios";
import { Dispatch, FC, FormEvent, useEffect, useRef, useState } from "react";
import { NavigateFunction, NavLink } from "react-router-dom";
import { FETCHLOGIN, HOMEPAGE, LOGGEDIN } from "../component/UI/Constatns";
import { userActions } from "../store/user/user-slice";
import LoginForm from "../component/forms/login/LoginForm";
import classes from "../styles/LoginStyles.module.css";
const Login: FC<{
dispatch: Dispatch<any>;
isMobile: boolean;
params: URLSearchParams;
axios: AxiosStatic;
nav: NavigateFunction;
}> = ({ axios, dispatch, isMobile, params, nav }) => {
const [userPassword, setUserPassword] = useState<string>("");
const username = useRef<HTMLInputElement | undefined>();
const password = useRef<HTMLInputElement | undefined>();
const userSearchParam: string | null = params.get("username");
useEffect(() => {
if (userSearchParam) {
const fetchUser: (
axios: AxiosStatic,
username: string,
password: string
) => void = async (axios, username, password) => {
await axios
.post(FETCHLOGIN, { username: username, password: password })
.then((response) => {
dispatch(userActions.login({ username: response.data.username }));
nav(LOGGEDIN, { replace: true });
})
.catch(() => {
nav(HOMEPAGE, { replace: true });
});
};
fetchUser(axios, userSearchParam, userPassword);
}
}, [nav, axios, userPassword, userSearchParam, dispatch]);
const submitHandler: (e: FormEvent<HTMLFormElement>) => void = (e) => {
e.preventDefault();
setUserPassword(password.current?.value as string);
nav(`?username=${username.current?.value}`, { replace: true });
};
return (
<Grid className={classes.loginContainer} container>
<Card className={!isMobile ? classes.card : classes.mobCard}>
<div className={classes.cardHeader}>
<p>Please login</p>
</div>
<CardContent>
<LoginForm
Link={NavLink}
Submit={submitHandler}
TextField={TextField}
Button={Button}
Grid={Grid}
username={username}
password={password}
/>
</CardContent>
</Card>
</Grid>
);
};
export default Login;
Related
I am making a site whereby after the user signs in, the user is meant to be redirected to the home page. The homepage and all the other pages of the site are only accessible by signed in users but even after a user signs in(firebase auth), the rest of the site(protected routes) is still not accessible and the only page accessible is the login page. The technologies I am using are react, react router dom and firebase and this is how my code looks like, starting with the App.js
import Home from "./pages/home/Home";
import Login from "./pages/login/Login";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import List from "./pages/list/List";
import User from "./pages/user/User";
import AddNew from "./pages/addnew/AddNew";
import { useContext } from "react";
import { AuthContext } from "./context/AuthContext";
function App() {
const {currentUser} = useContext(AuthContext);
const RequireAuth = ({ children }) => {
return currentUser ? children : <Navigate to="/login" />;
};
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="/login" exact element={<Login />} />
<Route path="/" exact element={ <RequireAuth> <Home /> </RequireAuth> } />
<Route path="/users" exact element={<RequireAuth><List /></RequireAuth>} />
<Route path="/users/:id" exact element={<RequireAuth><User /></RequireAuth>} />
<Route path="/add" exact element={<RequireAuth><AddNew /></RequireAuth>} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
And then followed by the login.js page
import React,{useState} from 'react'
import { useContext } from 'react';
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from '../../firebase';
import "./login.css";
import {useNavigate} from "react-router-dom";
import { AuthContext } from '../../context/AuthContext';
export default function Login() {
const [error, seterror] = useState(false);
const [email, setemail] = useState("");
const [password, setpassword] = useState("");
const navigate = useNavigate();
const {dispatch} = useContext(AuthContext)
const handleLogin = (e) => {
e.preventDefault();
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
dispatch({type: "LOGIN", payload: user});
navigate("/");
})
.catch((error) => {
seterror(true);
console.log(error.message);
});
}
return (
<div className='login'>
<form onSubmit={handleLogin}>
<input className='ok' type="email" placeholder='email' onChange={e => setemail(e.target.value)} />
<input className='ok' type="password" placeholder='password' onChange={e => setpassword(e.target.value)} />
<button className='sb'>Submit</button>
{error && <span className='ks'>Wrong email or password</span>}
</form>
</div>
)
}
And then I have the authreducer.js file that deals with the state
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN": {
return {
currentUser: action.payload,
}
}
case "LOGOUT": {
return {
currentUser: null
}
}
default:
return state;
}
}
export default AuthReducer
And finally the authcontext.js file
import { createContext, useEffect, useReducer } from "react";
import AuthReducer from "./AuthReducer";
const INITIAL_STATE = {
currentUser: JSON.parse(localStorage.getItem("user")) || null,
}
export const AuthContext = createContext(INITIAL_STATE);
export const AuthContextProvider = ({children}) => {
const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE);
useEffect(() => {
localStorage.setItem("user", JSON.stringify(state.currentUser))
}, [state.currentUser])
return (
<AuthContext.Provider value={{current: state.current, dispatch}}>
{children}
</AuthContext.Provider>
)
}
I do not know what could be causing this problem but I have an idea that it has something to do with the state because it was redirecting well before I started combining it with the state. What could be the problem
Issue
From that I can see, the App isn't destructuring the correct context value to handle the conditional route protection.
The AuthContextProvider provides a context value with current and dispatch properties
<AuthContext.Provider value={{ current: state.current, dispatch }}>
{children}
</AuthContext.Provider>
but App is accessing a currentUser property, which is going to be undefined because state.current is undefined.
const { currentUser } = useContext(AuthContext);
const RequireAuth = ({ children }) => {
return currentUser ? children : <Navigate to="/login" />;
};
The Navigate component will always be rendered.
Solution
Assuming the handleLogin handler correctly updates the state then the solution is to be consistent with state properties.
<AuthContext.Provider value={{ currentUser: state.currentUser, dispatch }}>
{children}
</AuthContext.Provider>
...
const { currentUser } = useContext(AuthContext);
const RequireAuth = ({ children }) => {
return currentUser ? children : <Navigate to="/login" />;
};
// I am working on Login/Register from in React and I am using firebase auth for
authentication. When the user logged in I want to redirect the user on the root path or
on App component. But I got into an infinite loop which gives me this error (#Throttling
navigation to prevent the browser from hanging.#)//
**App Js**
import { useEffect } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "./App.css";
import Header from "./components/Header";
import Home from "./components/Home";
import Login from "./components/Login";
import { getUserAuth } from "./actions";
import { connect } from "react-redux";
function App(props) {
useEffect(() => {
props.getUserAuth();
}, []);
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Login />} />
<Route
path="/home"
element={
<>
<Header />
<Home />
</>
}
/>
</Routes>
</BrowserRouter>
</div>
);
}
const mapStateToProps = (state) => {
return {};
};
const mapDispatchToProps = (dispatch) => ({
getUserAuth: () => dispatch(getUserAuth()),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
//App Section End//
**LogIn**
import styled from "styled-components";
import { connect } from "react-redux";
import { signInAPI } from "../actions";
import { Navigate } from "react-router-dom";
const Login = (props) => {
return (
<Container>
{props.user && <Navigate to="/home" />}
<Nav>
<a href="/">
<img src="/images/login-logo.svg" alt="" />
</a>
<div>
<Join>Join now</Join>
<SignIn>Sign in</SignIn>
</div>
</Nav>
<Section>
<Hero>
<h1>Welcome to your professional community</h1>
<img src="/images/login-hero.svg" alt="" />
</Hero>
<Form>
<Google onClick={() => props.signIn()}>
<img src="/images/google.svg" alt="" />
Sign in with Google
</Google>
</Form>
</Section>
</Container>
);
};
const mapStateToProps = (state) => {
return {
user: state.userState.user,
};
};
const mapDispatchToProps = (dispatch) => ({
signIn: () => dispatch(signInAPI()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Login);
//LogIn Section End to identify the error //
**SignIn**
import { auth, provider } from "../firebase";
import { signInWithPopup, onAuthStateChanged, signOut } from "firebase/auth";
import { SET_USER } from "./actionType";
export const setUser = (payload) => ({
type: SET_USER,
user: payload,
});
export function signInAPI() {
return (dispatch) => {
signInWithPopup(auth, provider)
.then((payload) => {
// console.log(payload.user);
dispatch(setUser(payload.user));
})
.catch((error) => alert(error.message));
};
}
export function getUserAuth() {
return (dispatch) => {
onAuthStateChanged(auth, (user) => {
console.log(user);
if (user) {
dispatch(setUser(user));
}
});
};
}
export function signOutAPI() {
return (dispatch) => {
signOut(auth)
.then(() => {
dispatch(setUser(null));
})
.catch((error) => {
console.log(error.message);
});
};
}
**End SignIn**
//**Throttling navigation** to prevent the browser from hanging.//
//Warning: Maximum update depth exceeded. This can happen when a component calls
setState inside useEffect, but useEffect either doesn't have a dependency array, or
one of the dependencies changes on every render. Unable to identify the error //
import React from 'react';
import PropTypes from 'prop-types';
import { Route } from 'react-router-dom';
import { SelectModal } from 'ux-components';
const ItemSelectRoute = (props) => {
console.log('1111111', props);
return (
<Route
path="/item-select/:label"
render={(routeProps) => (
<SelectModal
isOpen
label={routeProps.match.params.label}
onCloseClick={() => (routeProps.history.push(props.background.pathname))}
/>
)}
/>
);
}
export default ItemSelectRoute;
SelectModal.js
import React from 'react';
import PropTypes from 'prop-types';
import { Dialog } from 'styleguide-react-components';
import ModalHeader from 'ux-components/src/ModalHeader';
import ModalBody from '../../ModalBody/ModalBody';
const SelectModal = ({
onCloseClick, isOpen, itemSummaries,
}) => {
const itemList = itemSummaries;
return (
<Dialog
appearance="lite"
open={isOpen}
title={<ModalHeader header="Please select" />}
type="modal"
hasCloseButton
clickOffToClose
width={750}
onClose={onCloseClick}
>
<ModalBody items={itemList} />
</Dialog>
);
};
export default SelectModal;
I am writing the test case as for ItemSelectRoute
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const state = {
settings: {
configuration: {},
featureToggle: {},
properties: {},
},
};
const store = mockStore(state);
const newProps = {
appData: {
background: {
pathname: '/',
},
};
const wrapper = mount(<ReduxProvider store={store}>
<MemoryRouter initialEntries={['/item-select/test']}>
<Switch>
<ItemSelectRoute
store={store}
dispatch={jest.fn()}
{...newProps}
render={() => (<SelectModal
isOpen
label="track-my-item"
onCloseClick={() => jest.fn()}
/>)}
/>
</Switch>
</MemoryRouter>
</ReduxProvider>);
console.log(wrapper.debug());
When I run the test, I am getting the following error
Cannot read property 'addEventListener' of undefined
I want to write the test case, where if the route is correct, then SelectModal should be present in the elements tree. I tried few options, but I am unable to resolve the error.
I am making a simple SPA where you need to login before you can access other pages. I can successfully login and store the login data (firstname, lastname, etc.) cause I plan to use the data again later in the other pages. The problem is whenever I refresh the page, it always empty the state in the context which cause me to return to the login page. I am referring link for my SPA.
Do I need to do this? I would be thankful if someone can point out what I should change / improve. Thank you.
Here is my code.
App.js
import React, { useState } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
import { AuthContext } from "./context/auth";
import PrivateRoute from "./PrivateRoute";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import Home from "./pages/Home";
import Admin from "./pages/Admin";
function App() {
const [authTokens, setAuthTokens] = useState();
const setTokens = (data) => {
// console.log("DATA ",data);
localStorage.setItem("tokens", JSON.stringify(data));
setAuthTokens(data);
}
// console.log(authTokens);
return (
<AuthContext.Provider value={{ authTokens, setAuthTokens: setTokens }}>
<Router>
<div className="app">
<ul>
<li><Link to="/">Home Page</Link></li>
<li><Link to="/admin">Admin Page</Link></li>
</ul>
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/" component={Home} />
<PrivateRoute exact path="/admin" component={Admin} />
</div>
</Router>
</AuthContext.Provider>
);
}
export default App;
Login.js
import React, { useState } from "react";
import axios from "axios";
import { Link, Redirect } from "react-router-dom";
import { useAuth } from "../context/auth";
import { Card, Form, Input, Button, Error } from "../components/AuthForm";
const Login = () => {
const [isLoggedIn, setLoggedIn] = useState(false);
const [isError, setIsError] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { setAuthTokens } = useAuth();
const handleLogin = () => {
axios
.post("LOGINLINK", {
email,
password,
})
.then((result) => {
if (result.status === 200) {
setAuthTokens(result.data);
setLoggedIn(true);
} else {
setIsError(true);
}
})
.catch((error) => {
setIsError(true);
});
};
if (isLoggedIn) {
return <Redirect to="/" />;
}
return (
<Card>
<Form>
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
/>
<Input
type="password"
placeholder="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<Button onClick={handleLogin}>Login</Button>
</Form>
<Link to="/signup">Don't have an account?</Link>
{isError && (
<Error>The username or password provided were incorrect!</Error>
)}
</Card>
);
};
export default Login;
Auth.js
import { createContext, useContext } from "react";
export const AuthContext = createContext();
export function useAuth() {
console.log("CONTEXT", useContext(AuthContext));
return useContext(AuthContext);
}
In your App component you need to fetch the data from localStorage when initializing your state so it has some data to start with.
const localToken = JSON.parse(localStorage.getItem("tokens"));
const [authTokens, setAuthTokens] = useState(localToken);
If user has already authenticated it will be available in localStorage else it's going to be null.
I also had same problem but I solved liked this Don't use localStorage directly use your state and if it is undefined then only use localStorage. cause directly manipulating state with localStorage is in contrast with react internal state and effects re-render .
const getToken = () => {
JSON.parse(localStorage.getItem('yourtoken') || '')
}
const setToken = (token) => {
localStorage.setItem('key' , token)
}
const [authTokens, setAuthTokens] = useState(getToken());
const setTokens = (data) => {
// console.log("DATA ",data);
setToken(token);
setAuthTokens(data);
}
I am doing a typescript assignment, which is an app for a doctor to add patients, diagnoses and so on. I am using react-router. The router is changing the URL, but not rendering the patient view for some reason. I have been trying to figure this out for a while now. Can someone push me into the right direction? Thank you.
App.tsx
import React, { useState } from "react";
import axios from "axios";
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
import { Button, Divider, Header, Container } from "semantic-ui-react";
import { apiBaseUrl } from "./constants";
import { useStateValue } from "./state";
import { Patient } from "./types";
import PatientListPage from "./PatientListPage";
import PatientPage from "./components/PatientPage";
const App: React.FC = () => {
//const [, dispatch] = useStateValue();
const [{ patient }, dispatch] = useStateValue();
const [page, setPage] = useState('');
React.useEffect(() => {
axios.get<void>(`${apiBaseUrl}/ping`);
const fetchPatientList = async () => {
try {
const { data: patientListFromApi } = await axios.get<Patient[]>(
`${apiBaseUrl}/api/patients`
);
dispatch({ type: "SET_PATIENT_LIST", payload: patientListFromApi });
} catch (e) {
console.error(e);
}
};
fetchPatientList();
}, [dispatch]);
const showPatient = async (id: string) => {
try {
const { data: patientFromApi } = await axios.get<Patient>(`${apiBaseUrl}/api/patients/${id}`);
dispatch({ type: "GET_PATIENT", payload: patientFromApi });
setPage(patientFromApi.id);
console.log('patient', patient);
} catch (error) {
console.log(error.message);
}
}
return (
<div className="App">
<Router>
<Container>
<Header as="h1">Patientor</Header>
<Button as={Link} to="/" primary>
Home
</Button>
<Divider hidden />
<Switch>
<Route path="/">
<PatientListPage showPatient={showPatient} />
</Route>
<Route path={`/${page}`} >
<PatientPage />
</Route>
</Switch>
</Container>
</Router>
</div>
);
};
export default App;
PatientListPage.tsx
import React from "react";
import axios from "axios";
import { Container, Table, Button } from "semantic-ui-react";
import { PatientFormValues } from "../AddPatientModal/AddPatientForm";
import AddPatientModal from "../AddPatientModal";
import { Patient } from "../types";
import { apiBaseUrl } from "../constants";
import HealthRatingBar from "../components/HealthRatingBar";
import { useStateValue } from "../state";
import { Link } from "react-router-dom";
const PatientListPage: React.FC<{ showPatient: any }> = ({ showPatient }) => {
const [{ patients, patient }, dispatch] = useStateValue();
const [modalOpen, setModalOpen] = React.useState<boolean>(false);
const [error, setError] = React.useState<string | undefined>();
const openModal = (): void => setModalOpen(true);
const closeModal = (): void => {
setModalOpen(false);
setError(undefined);
};
const submitNewPatient = async (values: PatientFormValues) => {
try {
const { data: newPatient } = await axios.post<Patient>(
`${apiBaseUrl}/api/patients`,
values
);
dispatch({ type: "ADD_PATIENT", payload: newPatient });
closeModal();
} catch (e) {
console.error(e.response.data);
setError(e.response.data.error);
}
};
return (
<div className="App">
<Container textAlign="center">
<h3>Patient list</h3>
</Container>
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Gender</Table.HeaderCell>
<Table.HeaderCell>Occupation</Table.HeaderCell>
<Table.HeaderCell>Health Rating</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{Object.values(patients).map((patient: Patient) => (
<Table.Row key={patient.id} onClick={() => showPatient(patient.id)}>
<Link to={`/${patient.id}`}>
<Table.Cell>{patient.name}</Table.Cell>
</Link>
<Table.Cell>{patient.gender}</Table.Cell>
<Table.Cell>{patient.occupation}</Table.Cell>
<Table.Cell>
<HealthRatingBar showText={false} rating={1} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<AddPatientModal
modalOpen={modalOpen}
onSubmit={submitNewPatient}
error={error}
onClose={closeModal}
/>
<Button onClick={() => openModal()}>Add New Patient</Button>
</div>
);
};
export default PatientListPage;
PatientPage.tsx
import React, { useState } from "react";
import { Patient } from '../types';
import { useStateValue } from "../state";
const PatientPage: React.FC = () => {
const [{ patient }, dispatch] = useStateValue();
return (
<>
name: {patient.name}
ssn: {patient.ssn}
occupation: {patient.occupation}
</>
)
}
export default PatientPage
It might be because you have not used the exact keyword, so its rendering the PatientListPage component instead.
<Switch>
<Route exact path="/">
<PatientListPage showPatient={showPatient} />
</Route>
<Route path={`/${page}`}>
<PatientPage />
</Route>
</Switch>
More information here
First Solution and Best Solution:
If you use are using React Router 5.3.x, check whether it is 5.3.3 in your package.json file.
If it is not 5.3.3 uninstall the last version then install the bug-free version which has been resolved by John and updated in version 5.3.3.
npm uninstall -S react-router-dom
npm install -S react-router-dom#5.3.3
Second Solution:
React has launched its StrictMode in its latest update.
you can see it in an index.js file
index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
So here your React Router is in the child component. And we have to make it a parent component.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<React.StrictMode>
<App />
</React.StrictMode>
</BrowserRouter>
);
Third Solution:
Remove the Strict mode from the index.js file
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);