How to properly refactor router components? - reactjs

I'm building a to-do list app in React. With React-Router it has routes for "/all", "/today", "/week", "/inbox", and custom "/:projectId" tasks. They all render the same component <TaskList /> which accepts a couple of props:
Project ID, to add tasks to
Project Name, and
Tasks belonging to the respective project
I don't know how to properly refactor such code so that it's as efficient and DRY as possible. Here's my current attempt:
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import {
selectAllTasks,
selectTodayTasks,
selectWeekTasks,
selectProjectTasks
} from "../../redux/tasks.module";
import { selectCurrentProject } from "../../redux/projects.module";
import TaskList from "../task-list/task-list.component";
const projectId = 0; // Tasks entered will be added to Inbox
export const AllTasks = () => {
const tasks = useSelector(selectAllTasks);
return <TaskList projectName="All" projectId={projectId} tasks={tasks} />;
};
export const TodayTasks = () => {
const tasks = useSelector(selectTodayTasks);
return <TaskList projectName="Today" projectId={projectId} tasks={tasks} />;
};
export const WeekTasks = () => {
const tasks = useSelector(selectWeekTasks);
return (
<TaskList projectName="Next 7 Days" projectId={projectId} tasks={tasks} />
);
};
export const ProjectTasks = () => {
const { projectId } = useParams();
const { text } = useSelector(state => selectCurrentProject(state, projectId));
const tasks = useSelector(state => selectProjectTasks(state, projectId));
return <TaskList projectName={text} projectId={projectId} tasks={tasks} />;
};
And here's the page that calls them:
import { Switch, Route, useRouteMatch } from "react-router-dom";
import Header from "../../components/header/header.component";
import Sidebar from "../../components/sidebar/sidebar.component";
import {
AllTasks,
TodayTasks,
WeekTasks,
ProjectTasks
} from "../../components/filters/filters.component";
import useStyles from "./tasks.styles";
const TasksPage = () => {
const { path } = useRouteMatch();
const classes = useStyles();
return (
<div className={classes.container}>
<Header />
<Sidebar />
<Switch>
<Route exact path={`${path}/all`} component={AllTasks} />
<Route exact path={`${path}/today`} component={TodayTasks} />
<Route exact path={`${path}/week`} component={WeekTasks} />
<Route exact path={`${path}/:projectId`} component={ProjectTasks} />
</Switch>
</div>
);
};
export default TasksPage;
What's the most efficient way to structure this? And allow not only the hardcoded routes (i.e. all, today, week, etc.) but also the custom user project routes (/:projectId) to coexist and not have repeating code?
Thank you so much.

Related

Router Router Switch causes unmount on updating route search/query strings

In my App.js I have a number of components wrapped in a Switch component from react-router-dom
App.js
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
const Test = Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
});
const Test1 = Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
});
const Test2 = Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
});
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="blue" exact path="/" component={Test} />
<ProtectedRoute bgColour="red" exact path="/1" component={Test1} />
<ProtectedRoute bgColour="green" exact path="/2" component={Test2} />
</Switch>
);
};
export default App;
The ProtectedRoute component renders a Route component from react-router-dom passing in the specified component. It also has a HOC, which in my actual application checks the user is authenticated
ProtectedRoute.js
import React from "react";
import { Route } from "react-router-dom";
const withAuth = (Component) => {
return (props) => {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, bgColour, ...args }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route component={withAuth(component)} {...args} />
</div>
);
};
export default ProtectedRoute;
For each component, I have alerts setup to trigger on mount and unmount of the component. On a click on an element it updates the query string to a random number via history.push, however, this currently triggers an unmount, due to the Switch added in App.js, without the Switch there is no unmount. This is causing an issue in my application as an unmount is not desired behaviour and is causing issues with loading the correct data.
Test.js
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
export default function Test() {
const history = useHistory();
useEffect(() => {
alert("MOUNTED BASE");
return () => {
alert("UNMOUNTED BASE");
};
}, []);
return (
<div>
<h1>TEST COMPONENT BASE - BLUE</h1>
<div
onClick={() =>
history.push({
pathname: history.location.pathname,
search: `?query=${Math.random().toFixed(2)}`
})
}
>
UPDATE QUERY STRING
</div>
<div onClick={() => history.push("/1")}>GO TO Next ROUTE</div>
</div>
);
}
I still want the functionality of the Switch but prevent the unmount on history.push, is this possible?
I have a CodeSandbox below to recreate this issue
Issue
Based on only the code you've provided, an issue I see is how every component the ProtectedRoute renders is decorated with the withAuth Higher Order Component. This results in a new component being created every time ProtectedRoute renders.
Solution
You want to only decorate the routed components with HOCs once prior to where they are used.
Example:
const ProtectedRoute = ({ bgColour, ...props }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route {...props} />
</div>
);
};
...
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import withAuth from "..path/to/withAuth";
// Decorate components with HOCs once out here
const Test = withAuth(Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
}));
const Test1 = withAuth(Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
}));
const Test2 = withAuth(Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
}));
// Render decorated components in App
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="red" path="/1" component={Test1} />
<ProtectedRoute bgColour="green" path="/2" component={Test2} />
<ProtectedRoute bgColour="blue" path="/" component={Test} />
</Switch>
);
};

Hiding Banner at a certain page

I'm currently attempting to hide the banner at a certain page. I have successfully hid the banner in all other pages except one with a page with a id. I have a dynamic folder named [content]
import { useRouter } from "next/router";
const HIDDEN_BOARDLIST = ["/board/board_list"];
//this is successful
const HIDDEN_BOARDDETAILS = [`board/${content}`].
//this does not work
//http://localhost:3000/board/620471f057aad9002de7f04f. I have to enter the id manually but since this is a dynamic, the id will change every time
export default function Layout(props: ILayoutProps) {
const router = useRouter();
console.log(router.asPath);
const isHiddenBoardList = HIDDEN_BOARDLIST.includes(router.asPath);
return (
<Wrapper>
<Header />
{!isHiddenBoardList && <Banner />}
<BodyWrapper>
<Body>{props.children}</Body>
</BodyWrapper>
</Wrapper>
);
}
useRouter is a hook.
CSR
import React, { useState, useEffect } from 'React';
import { useRouter } from "next/router";
interface ILayoutProps {
//...
}
export default function Layout(props: ILayoutProps) {
const router = useRouter();
const [hidden, setHidden] = useState(false);
useEffect(() => {
if(router.asPath.includes('board/')) {
setHidden(true);
}
}, [router.asPath]);
return (
<Wrapper>
<Header />
{!hidden && <Banner />}
<BodyWrapper>
<Body>{props.children}</Body>
</BodyWrapper>
</Wrapper>
);
}
Since this code is CSR, flickering may occur. <Banner /> will disappear after being rendered.
If you don't want that, there is a way to pass the current url as props of the <Layout /> component via getServerSideProps.
SSR
// pages/board/[id].tsx
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
interface Props {
url: string;
}
const BoardPage: NextPage<Props> = (props: Props) => {
return (
<>
<Layout {...props} />
</>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const { resolvedUrl } = context; //ex) /board/12345?id=12345
return {
props: {
url: resolvedUrl ,
}, // will be passed to the page component as props
};
};
// components/Layout.tsx
import React, { useState, useEffect } from 'React';
import { useRouter } from "next/router";
interface ILayoutProps {
url: string;
// ...
}
export default function Layout(props: ILayoutProps) {
return (
<Wrapper>
<Header />
{props.url.includes('board/') && <Banner />}
<BodyWrapper>
<Body>{props.children}</Body>
</BodyWrapper>
</Wrapper>
);
}
I hope these two kinds of code are helpful.

How to link to a component using persistant header that's outside of BrowserRouter?

I'm attempting to link to somewhere within my application using react-router-dom within an appBar/header that is persistent throughout the app. I keep getting "TypeError: history is undefined" when I attempt to use RRD within the header component.
I've been playing around with this for a good few hours now and I'm not getting any where with it. I can't think straight thanks to the heat, and I'm clearly searching for the wrong answers in my searches. The best solution I have come-up with thus-far is having each component contain the header component at the top but this is obv not ideal. I know I must be missing something simple as this can't be an uncommon pattern.
Demo Code
Node Stuff
npx create-react-app rdr-header --template typescript
npm install react-router-dom
App.tsx
import React from "react";
import "./App.css";
import {
BrowserRouter as Router,
Switch,
Route,
useHistory,
} from "react-router-dom";
function App() {
let history = useHistory();
const handleClick = (to: string) => {
history.push(to);
};
return (
<div className='App'>
<header className='App-header'>
<button onClick={() => handleClick("/ger")}>German</button>
<button onClick={() => handleClick("/")}>English</button>
</header>
<Router>
<Switch>
<Route exact path='/' component={English} />
<Route path='/ger' component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push("/ger");
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push("/");
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
You should create separate component for header
header.js
import React from 'react';
import './style.css';
import { useHistory } from 'react-router-dom';
function Header() {
let history = useHistory();
const handleClick = to => {
history.push(to);
};
return (
<header className="App-header">
<button onClick={() => handleClick('/ger')}>German</button>
<button onClick={() => handleClick('/')}>English</button>
</header>
);
}
export default Header;
Use Header component inside Router like below:-
import React from 'react';
import './style.css';
import {
BrowserRouter as Router,
Switch,
Route,
useHistory
} from 'react-router-dom';
import Header from './header.js'; // import header component
function App() {
return (
<div className="App">
<Router>
<Header /> // use Header component inside Router
<Switch>
<Route exact path="/" component={English} />
<Route path="/ger" component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push('/ger');
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push('/');
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
Instead of changing the history object using history.push(), you can use the <Link> or <NavLink> components from react-router.
React Router - Link component
Make sure to place the header component inside the Router component.

Warning: Expected `onClick` listener to be a function, instead got a value of `boolean` type

I am working with react. When I try to play audio from a button I am receiving this error. Everything worked before when the website was only a one page website. Then I turned it into a multi-page website using react-router-dom router, switch, and route everything started to error out with the onClick. I am not sure what went wrong or how to fix it. I am still pretty new with react. Here is the code:
Player.js
import React, { useState } from "react";
import "./Button.css";
import {useTranslation} from "react-i18next";
import teaser from '../sounds/teaser-final.mp3';
const Player = ({ url }) => {
const useAudio = url => {
const [audio] = useState(new Audio(teaser));
const [playing, setPlaying] = useState(true); //nothing is playing on default
const toggle = () => {
setPlaying(!playing);
playing ? audio.play() : audio.pause()
console.log("audio is playing" + toggle);
};
return [playing, toggle];
};
const [toggle] = useAudio(url);
const {t} = useTranslation('common');
return (
<div>
<audio id="player" style={{'display': 'none'}} src={teaser}></audio>
<button
className="btns hero-button btn--outline btn--large"
onClick={toggle}
>
{t('heroSection.button')}
</button>
</div>
);
};
export default Player;
HeroSection.js (file where Player.js is used)
import React from 'react'
import './HeroSection.css'
import Player from '../Player';
function HeroSection() {
return (
<div className="hero-btns">
<Player />
</div>
)
}
export default HeroSection;
App.js
import React, { useState, useEffect } from 'react';
import './index.css';
import {BrowserRouter, Switch, Route} from 'react-router-dom';
import Axios from 'axios';
import AdminHome from './Components/auth/Admin';
import Login from './Components/auth/Login';
import Register from './Components/auth/Register';
import UserContext from './Context/UserContext';
import Navbar from './Components/Navbar/Navbar';
import HeroSection from './Components/HeroSection/HeroSection';
import ShareSection from './Components/ShareSection/ShareSection';
import Subscribe from './Components/Subscribe/Subscribe';
import Footer from './Components/Footer/Footer';
import About from './Components/About/About';
import TheApp from './Components/TheApp/TheApp';
import Contact from './Components/SocialSection/Contact';
import CookiesPopUp from './Components/Cookies/CookiesPopUp';
function Home() {
return (
<>
<CookiesPopUp />
<Navbar />
<HeroSection />
<ShareSection />
<Subscribe />
<TheApp />
<About />
<Contact />
<Footer />
</>
);
};
function App() {
const [userData, setUserData] = useState({
token: undefined,
user: undefined,
});
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
const checkedLoggedIn = async () => {
let token = localStorage.getItem("auth-token");
if(token === null) {
localStorage.setItem("auth-token", "");
token = "";
}
const tokenRes = await Axios.post(
"http://localhost:5000/users/tokenIsValid", null,
{headers: { "x-auth-token": token }}
);
if (tokenRes.data) {
const userRes = await Axios.get("http://localhost:5000/users/", {
headers: { "x-auth-token": token },
});
setUserData({
token,
user: userRes.data,
});
}
return () => setDidMount(false);
};
checkedLoggedIn();
}, [])
if(!didMount) {
return null;
}
return (
<>
<BrowserRouter>
<UserContext.Provider value={{userData, setUserData}}>
<Switch>
<Route path="/" component={Home} exact />
<Route path="/admin" component={AdminHome} exact />
<Route path="/admin/login" component={Login} exact />
<Route path="/admin/register" component={Register} exact />
</Switch>
</UserContext.Provider>
</BrowserRouter>
</>
);
}
export default App;
And this is the error I get in console when I try to play the audio.
You are overriding the definition of toggle with this code :
const [toggle] = useAudio(url);. The Player.js has multiple declarations and definitions of toggle. See:
const toggle = () => {
setPlaying(!playing);
playing ? audio.play() : audio.pause()
console.log("audio is playing" + toggle);
};
return [playing, toggle];
};
..
...
..
const [toggle] = useAudio(url);
Hence the Error Expected OnClick to be a function but provided a boolean
Thank you for your feedback! I managed to get everything to work by changing my code in Player.js to:
import React, { useState } from "react";
import "./Button.css";
import {useTranslation} from "react-i18next";
import teaser from '../sounds/teaser-final.mp3';
const Player = () => {
const [playing, setPlaying] = useState(true);
const [audio] = useState(new Audio(teaser));
const toggle = () => {
setPlaying(!playing);
playing ? audio.play() : audio.pause()
};
const {t} = useTranslation('common');
return (
<div>
<audio id="player" style={{'display': 'none'}} src={teaser}></audio>
<button
className="btns hero-button btn--outline btn--large"
onClick={toggle}
>
{t('heroSection.button')}
</button>
</div>
);
};
export default Player;

Not able to access detail information from Api using React Router not rendering on the page

I am building a small React Routing application to get a better idea as to how it work. My App.js looks like this with the basic routing:
import React from 'react';
import './App.css';
import Nav from './Nav';
import About from './About';
import Shop from './Shop';
import CountryDetail from './CountryDetail'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
function App() {
return (
<Router>
<div className="App">
<Nav />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/shop" exact component={Shop} />
<Route path="/shop/:name" component={CountryDetail} />
</Switch>
</div>
</Router>
);
}
const Home = () => (
<div>
<h1>Home Page</h1>
</div>
);
Now the Shop component a list of countries from the api which is in the code below:
import React from 'react';
import './App.css';
import { useEffect } from 'react';
import { useState } from 'react';
import {Link} from 'react-router-dom';
function Shop() {
useEffect(() => {
fetchItems();
},[])
const [countries, setCountries] = useState([])
const fetchItems = async () => {
const data = await fetch('https://restcountries.eu/rest/v2/all');
const countries = await data.json();
console.log(countries);
setCountries(countries);
}
return (
<div>
{countries.map(country => (
<div>
<Link to={`shop/${country.name}`}>
<h1 key={country.alpha2Code}>
{country.name}
</h1>
</Link>
<p>Popluation {country.population}</p>
<p> Region {country.region}</p>
<p>Capital {country.capital}</p>
</div>
)
)}
</div>
);
}
export default Shop;
Now what I want to do is render more information about the country when I click on it. So I have created another component called CountryDetail:
import React from 'react';
import './App.css';
import { useEffect } from 'react';
import { useState } from 'react';
function CountryDetail( { match } ) {
useEffect(() => {
fetchItem();
console.log(match)
},[])
const [country, setCountry] = useState([])
const fetchItem = async ()=> {
const fetchCountry = await fetch(`https://restcountries.eu/rest/v2/name/${match.params.name}`);
const country = await fetchCountry.json();
setCountry(country);
console.log(country);
}
return (
<div>
<h1>Name {country.name}</h1>
<p>Native Name{country.nativeName}</p>
<p>Region {country.region}</p>
<p>Languages {country.languages}</p>
<h1>This Country</h1>
</div>
);
}
export default CountryDetail;
The problem I am having is that it is not rendering anything on the CountryDetail page. I am sure I have hit the api correctly but not sure if I am getting the data correctly. Any help would be appreciated.
Issue: The returned JSON is an array but your JSX assumes it is an object.
Solution: You should extract the 0th element from the JSON array. Surround in a try/catch in case of error, and correctly render the response.
Note: the languages is also an array, so that needs to be mapped
function CountryDetail({ match }) {
useEffect(() => {
fetchItem();
console.log(match);
}, []);
const [country, setCountry] = useState(null);
const fetchItem = async () => {
try {
const fetchCountry = await fetch(
`https://restcountries.eu/rest/v2/name/${match.params.name}`
);
const country = await fetchCountry.json();
setCountry(country[0]);
console.log(country[0]);
} catch {
// leave state alone or set some error state, etc...
}
};
return (
country && (
<div>
<h1>Name {country.name}</h1>
<p>Native Name{country.nativeName}</p>
<p>Region {country.region}</p>
<p>Languages {country.languages.map(({ name }) => name).join(", ")}</p>
<h1>This Country</h1>
</div>
)
);
}
As you said it yourself, the response is an array (with a single country object in it), but you are using it as if it would be an object.
So, instead of:
const country = await fetchCountry.json();
setCountry(country);
It should be:
const countries = await fetchCountry.json();
setCountry(countries[0]);

Resources