How to pass props to route components when using useRoutes? - javascript

I'm trying out the new useRoutes hook from react-router-dom and it seems to be pretty interesting. The only problem, is that I can't figure out how I would pass props down to the components.
Before, I'd have a Route component, and I would select parts of local or global state there and pass it on, but how would I do it with useRoutes?
In the below sandbox, I'm trying to change the background based on the boolean value of isLoading. How would I pass isLoading on?
Edit
Here's the code:
import React from "react";
import { BrowserRouter as Router, Outlet, useRoutes } from "react-router-dom";
const Main = ({ isLoading }) => (
<div
style={{
height: "40vh",
width: "50vw",
backgroundColor: isLoading ? "red" : "pink"
}}
>
<Outlet />
</div>
);
const routes = [
{
path: "/",
element: <Main />
}
];
const App = ({ isLoading }) => {
const routing = useRoutes(routes);
return (
<>
{routing}
{JSON.stringify(isLoading)}
</>
);
};
export default function Entry() {
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setInterval(() => {
setIsLoading(!isLoading);
}, 3000);
}, [isLoading]);
return (
<Router>
<App isLoading={isLoading} />
</Router>
);
}
Edit
I've considered passing in an isLoading argument to routes, but I feel like that won't be an efficient approach, because the whole tree will rerender at any route, regardless of whether or not it depends on isLoading or doesn't. Would a better approach be to use Switch and a custom Route component for routes that depend on isLoading and just use useSelector in that custom Route component?

I think this would work:
const routes = (props) => [
{
path: "/",
element: <Main {...props} />
}
];
const App = ({ isLoading }) => {
const routing = useRoutes(routes({isLoading}));
return (
<>
{routing}
{JSON.stringify(isLoading)}
</>
);
};

Instead of passing the array, you can create a function that returns a constructed array to the useRoutes hook:
const routes = (isLoading) => [
{
path: "/",
element: <Main isLoading={isLoading} />
}
];
code sandbox:
https://codesandbox.io/s/dark-dream-hx1y9

more pretty would be:
const routes = (props) => [
{
"/": () => <Main {...props} />
}
];
const App = ({ isLoading }) => {
const routing = useRoutes(routes(isLoading));
return (
<>
{routing}
{JSON.stringify(isLoading)}
</>
)
};

Related

load routes after getting roles from an api

I created a react app, I added role based mechanism, the idea is that after the athentication, directly I send a request to an api to get the roles of the user, because the token contains only the username and I can not add any information on the payload.
so while getting thr roles from the api, I added a loder component that will block the user from using the app until the roles are loaded, at this point everything is working well, but when I reloaded the page, the app redirect me to the default route everytime, because the routes are not created yet, I would like to know how to block the app until the routes also are loaded? :
App.tsx :
const App: React.FC = () => {
const useRoles = useRoleBased(); // hook to get roles
return (
<>
{useRoles?.loading(
<Loader open backgroundColor="#ffffff" circleColor="primary" />
)}
<Box className={`mainSideBar ${openSideBar && 'openSideBar'}`}>
<Router />
</Box>
</>
);
};
export default App;
Router.tsx :
const routes = [
{ path: '/logout', element: <ConfirmLogout /> },
{
path: '/dashboard-page',
element: <DashboardPage />,
allowedRoles: [Roles.Director, Roles.RequestFullAccess],
},
{
path: '/profil',
element: <RequestPage />,
allowedRoles: [Roles.Manager],
},
];
const Router: React.FC = () => {
return <RolesAuthRoute routes={routes}></RolesAuthRoute>;
};
export default Router;
RolesAuthRoute.tsx :
export function RolesAuthRoute({ routes }: { routes: IRoutes[] }) {
const userRoles = useSelector((state: any) => state?.roles?.roles);
const isAllowed = (
allowedRoles: Roles[] | undefined,
userRoles: string[]) =>
process.env.REACT_APP_ACTIVATE_ROLE_BASED_AUTH === 'false' ||
!allowedRoles ||
allowedRoles?.some((allowedRole) => userRoles?.includes(allowedRole)
);
return (
<Routes>
{routes.map((route) => {
if (isAllowed(route?.allowedRoles, userRoles))
return (
<Route
path={route?.path}
element={route?.element}
key={route?.path}
/>
);
else return null;
})}
<Route path="*" element={<Navigate to="/" replace />} /> //this route is created in all cases
</Routes>
);
}
You could return early (conditional rendering) to stop the router from rendering prematurely. You'll need to modify the hook to return the loading state as boolean instead of rendering the component as it seems to be currently implemented.
const App: React.FC = () => {
const useRoles = useRoleBased(); // hook to get roles
if(useRoles.isLoading){
return <Loader open backgroundColor="#ffffff" circleColor="primary" />
};
return (
<>
<Box className={`mainSideBar ${openSideBar && 'openSideBar'}`}>
<Router />
</Box>
</>
);
};
export default App;

React UseContext with useReducer re-renders the whole app even after splitting

I am struggling to prevent my whole app getting re-rendered.I have even tried to split the context and memoize components but nothing helps me.
I have already read Use context Github issue this thread and a lot of other articles describing the right implementation but with no result.
I am using websockets to retrieve bitcoin value every second and it causes the whole app re-render on every second.
This is the App.tsx
<AppProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<BrowserRouter>
<div className="App">
<AppRoot isBusy={!ready || !isConnected} />
</div>
</BrowserRouter>
</ThemeProvider>
</AppProvider>
This is the main context
import React, { createContext, useReducer, Dispatch } from 'react';
import { BTCReducerActions, BTCData, BTCPayload, btcReducer } from '../reducers/btc';
import { CommonReducerActions, CommonData, CommonPayload, commonReducer } from '../reducers/common';
import { UserReducerActions, UserData, UserPayload, userReducer } from '../reducers/user';
interface InitialStateType {
user: UserData;
btc: BTCData;
common: CommonData;
}
const initialState: InitialStateType = {
btc: {} as BTCData,
common: {},
user: {} as UserData,
};
export type ActionType = (
BTCReducerActions |
CommonReducerActions |
UserReducerActions |
);
type MainReducer = (state: InitialStateType, action: ActionType) => InitialStateType;
const AppContext = createContext<{
state: InitialStateType;
dispatch: Dispatch<ActionType>;
}>({
state: initialState,
dispatch: () => null,
});
const mainReducer: MainReducer = ({ btc, common, user}, action) => ({
btc: btcReducer<BTCPayload>(btc, action as BTCReducerActions),
user: userReducer<UserPayload>(user, action as UserReducerActions),
common: commonReducer<CommonPayload>(common, action as CommonReducerActions),
});
const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
export { AppProvider, AppContext };
And this is the root.tsx
export const AppRoot = memo(({ isBusy }: Record<'isBusy', boolean>) => {
const {
dispatch,
state: {
common: { isMobile, soundSettings: { state, volume } },
user: { token },
},
} = useContext(AppContext);
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (!isBusy) {
SocketManager.socketEmitter('query', {
header: { action: 'main::getInitialData' },
}, ({ token, user: { id } }) => dispatch({ type: UserActionTypes.UpdateUserData, payload: { token, id } }));
}
}, [isBusy]);
return (
<AppRootStyled className="fb horizontal">
<Content>
<AppHeader />
<div className="fb main-content">
<Switch>
<Route exact strict path={GAME_SCREEN} component={Roulette} />
<Route exact strict path={STATISTICS_SCREEN} component={Activity} />
<Route exact strict path={BET_HISTORY_SCREEN} render={props => <BetHistory roundHistory={roundHistory} {...props} />} />
<Route exact strict path={USER_BETS_SCREEN} component={MyBets} />
</Switch>
</div>
</Content>
{location.pathname !== GAME_SCREEN && (
<div className="go-home">
<div className="fb horizontal aCenter jCenter">
<Icon name="round-back-circled" onClick={() => history.push(GAME_SCREEN)} />
</div>
</div>
)}
</AppRootStyled>
);
});
Somewhere in the app I am using socket to get the btc data
useEffect(() => {
SocketManager.connectChannel('main::BTCData', (data) => {
dispatch({ type: BTCActionTypes.SetBTCData, payload: data });
});
}, []);
I have already tried to create a separate context by splitting the main state so the btc data has its own slice of the state, but again the Route components re-render every second.Also other components that use the context are re-rendering every second.
This is the context splitting implementation
const AppContext = {/ ** \}
const BtcContext = createContext<{btc: BTCData}>({ btc: {} as BTCData });
const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
<BtcContext.Provider value={{btc}}>
{children}
</BtcContext.Provider>
</AppContext.Provider>
);
};
I know I am doing something wrong.I am using context as a state management service for the first time.Before I was using only redux and haven't faced this kind of problem.
I am already on it a few days and I need to solve the problem until tomorrow morning.
I have a lot of tables and other heavy components that shouldn't re-render so often.
UPDATED
I have tried this 2 options.
OPTION 1
<AppProvider>
<BtcProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<BrowserRouter>
<div className="App">
<AppRoot isBusy={!ready || !isConnected} />
</div>
</BrowserRouter>
</ThemeProvider>
</BtcProvider>
</AppProvider>
const AppProvider: React.FC = ({ children }) => {
const [{ btc, ...mainStateData }, dispatch] = useReducer(mainReducer, initialState);
const mainState = useMemo(() => ({
state: mainStateData,
dispatch,
}), [mainStateData]);
return (
<AppContext.Provider value={mainState}>
{children}
</AppContext.Provider>
);
};
const BtcProvider: React.FC = ({ children }) => {
const [{ btc }, dispatch] = useReducer(mainReducer, initialState);
const btcData = useMemo(() => ({
btc,
dispatch,
}), [btc]);
return (
<BtcContext.Provider value={btcData}>
{children}
</BtcContext.Provider>
);
};
export { AppProvider, BtcProvider, AppContext, BtcContext };
And OPTION 2
const AppProvider: React.FC = ({ children }) => {
const [{ btc, ...mainStateData }, dispatch] = useReducer(mainReducer, initialState);
const mainState = useMemo(() => ({
state: mainStateData,
dispatch,
}), [mainStateData]);
const btcData = useMemo(() => ({
btc,
dispatch,
}), [btc]);
return (
<AppContext.Provider value={mainState}>
<BtcContext.Provider value={btcData}>
{children}
</BtcContext.Provider>
</AppContext.Provider>
);
};
<AppProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<BrowserRouter>
<div className="App">
<AppRoot isBusy={!ready || !isConnected} />
</div>
</BrowserRouter>
</ThemeProvider>
The result is the same.The case is that the btc data is being changed every second and I need it to provide a lot of components, but to re-render only the components where I consume the data.
<AppContext.Provider value={{ state, dispatch }}>
Every time AppProvider renders, this code creates a new object { state, dispatch }. It may have the same contents as the previous object, but it's a different object, so react is forced to rerender any component that consumes this context. Similarly, you're making a brand new object for the BtcContext.Provider.
When providing an object, you need to make sure to memoize that object so it only changes when necessary:
const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);
const appValue = useMemo(() => {
return { state, dispatch };
}, [state]);
const btcValue = useMemo(() => {
return { btc };
}, [btc]);
return (
<AppContext.Provider value={appValue}>
<BtcContext.Provider value={btcValue}>
{children}
</BtcContext.Provider>
</AppContext.Provider>
);
};

Dynamic Routes for my list of projects in React

I am new to React, I have JSON data with projects I want to dynamically link. I have created a component called "ProjectSingle" to display each one, however I can't seem to get the data to display.
const App = () => {
const [projects, setProjects] = useState([]);
const getProjects = () => {
fetch('http://localhost:8000/projects')
.then((res) =>{
return res.json();
})
.then((data) => {
setProjects(data);
})
}
useEffect(() => {
getProjects();
AOS.init({disable: 'mobile'});
},[]);
return (
<Router>
<div className="App">
<Switch>
{projects.map( project =>(
<Route exact path='/project/:slug' render={props => (
<ProjectSingle {...props} project={project}/>
)}/>
))}
</Switch>
<Footer/>
</div>
</Router>
);
}
export default App;
The requested JSON is below, I'm using "slug" to generate the slug.
{
"projects": [
{
"title": "Project One",
"image": "/img/wp-logo.png",
"slug": "project-one",
"key": 1
},
{
"title": "Project Two",
"image": "/img/wp-logo.png",
"slug": "project-two",
"key": 2
},
}
Although I'm not sure what data to put in my single component to get the data of the project iteration
const ProjectSingle = ({project}) => {
const { slug } = useParams();
return (
<>
<h1>{project.title}</h1>
</>
)
}
export default ProjectSingle;
Firstly, you only need one <Route path="/project/:slug" /> ... </Route> assigned. The :slug param can be used to dynamically determine what the content of this route will be.
<Route
exact
path="/project/:slug"
render={(props) => <ProjectSingle {...props} projects={projects} />}
/>
const ProjectSingle = ({projects}) => {
const { slug } = useParams();
const project = projects.find(p => p.slug === slug)
return (
<>
<h1>{project.title}</h1>
</>
)
}
export default ProjectSingle;
The route's child (ProjectSingle) will then useParams() to retrieve the value associated with :slug and use that to find the correct project from projects to render.
There may be an even more clever way of using the render() of the route and determining the desired slug & project at that stage - making the ProjectSingle a pure component without any knowledge of routing.
There are several important steps to achive this.
Include libraries like react-router, react-router-dom
Create a history
Render the Route with history
4.Define Navigation to the routes
Here is a sample as per your requirement:
Note: As per your design the routes will render in the same page. I have updated it to be in single file here. complete working example in codesandbox
import React, { useState } from "react";
import { Router, Switch, Route, useParams } from "react-router";
import { Link } from "react-router-dom";
import createHistory from "history/createBrowserHistory";
import "./styles.css";
const history = createHistory();
const Footer = () => {
return <div>Footer here.</div>;
};
const ProjectSingle = ({ project }) => {
const { slug } = useParams();
//TODO useEffect to get project details
return (
<>
<h1>Details of {slug} </h1>
<div>{JSON.stringify(project)}</div>
</>
);
};
export default function App() {
const [projects, setProjects] = useState([
{
title: "Project One",
image: "/img/wp-logo.png",
slug: "project-one",
key: 1
},
{
title: "Project Two",
image: "/img/wp-logo.png",
slug: "project-two",
key: 2
}
]);
/*const getProjects = () => {
fetch('http://localhost:8000/projects')
.then((res) =>{
return res.json();
})
.then((data) => {
setProjects(data);
})
}
useEffect(() => {
getProjects();
AOS.init({disable: 'mobile'});
},[]);
*/
return (
<Router history={history}>
<div className="App">
{projects.map((project) => {
return (
<div key={project.key}>
<Link to={`/project/${project.slug}`}> {project.title} </Link>
</div>
);
})}
<Switch>
{projects.map((project) => (
<Route
key={project.key}
exact
path="/project/:slug"
render={(props) => <ProjectSingle {...props} project={project} />}
/>
))}
</Switch>
<Footer />
</div>
</Router>
);
}

How to use dynamic routing to pass information / react

The ButtonList returns a list of filtered Buttons that should each link to a new page. I would like to do it by adding dynamic routes, but I don't know how to write the code... How do I use history.push correctly? Any help would be highly appreciated.
export default function Home() {
const [searchValue, setSearchValue] = useState("");
const history = useHistory();
const filteredData = data.filter(item =>
item.name.toLowerCase().includes(searchValue.toLowerCase())
);
function handleSearch(value) {
setSearchValue(value);
}
const selectedItem = filteredData.find(item => item.name);
function handleSelect(item) {
history.push(`/home/${item.name}`);
}
return (
<WrapperDiv>
<StyledHeader />
<Switch>
      
<Route exact path="/home">
      
<Searchbar onSearch={handleSearch} />
<ButtonList data={filteredData} onClick={handleSelect} /> 
</Route>
<Route exact path="/home/{...}">
<StyledDiv>
<Button item={selectedItem} light />
<AccordionList />
</StyledDiv>
</Route>
</Switch>
</WrapperDiv>
);
}
export default function Button({ children, handleSelect }) {
return (
<button
to={{
pathname: "/home/" + data.name,
data: data
}}
onClick={handleSelect}
>
{children}
</button>
);
}
export const data = [
{
name: "Apple",
id: 1
},
{
name: "Banana",
id: 2
},
{
name: "Blueberry",
id: 3
}
];
one good way to achieve it would be to use React Router which makes creating dynamic routes really easy.
Writing the whole code is a little difficult here, therefore I found a good example of an easy way to implement react-router with your code.
Example
import { useHistory } from "react-router-dom";
function MyButton() {
let history = useHistory();
function triggerClick() {
history.push("/somewhere");
}
return (
<button type="button" onClick={triggerClick}>
Go somewhere
</button>
);
}

React hooks - state in useState() is not reset when route is changed

const Create = () => {
console.log('rerender !!')
const [parcelType, setParcelType] = useState('paper')
console.log('parcelType =', parcelType)
return (
<Container onClick={() => setParcelType('plastic')}>
<BookingList />
<Card title="Business">
<p>Header</p>
</Card>
</Container>
)
}
export default Create
I want to change parcelType state to 'plastic' when click on Container in Create component. and I want to reset parcelType state to 'paper' when route is change ( Create component re-render ). But when component re-render state is not set to paper
For more details: CreateComponent is re-render when route is change in BookingList component
const BookingList = props => {
const { id } = props.match.params
const containerStyle = useTranslateSpring('-100px', '0')
const itemList = items.map((item, idx) => {
const itemStyle = useTranslateSpring('-100px', '0', '0', 200 + 200 * idx)
const url = `/booking/${item.id}/create`
return (
<ItemContainer
onClick={() => props.history.push(url)}
style={itemStyle}
key={item.id}
isactive={id === item.id}
>
{item.id}
</ItemContainer>
)
})
return <Container style={containerStyle}>{itemList}</Container>
}
export default withRouter(BookingList)
Create Component is render in route by routeTemplate
const Routes = () => (
<Router basename={process.env.REACT_APP_BASE_URL}>
<>
<RouteTemplate
exact
path="/booking/:id/create"
component={Booking.create}
title="Booking"
/>
</>
</Router>
)
and RouteTemplate is render Component wrapped by PageTemplate component
const RouteTemplate = props => {
const {
component: Component,
title,
query,
isAuthenticated,
isLanding,
...rest
} = props
return (
<Route
{...rest}
render={matchProps =>
isAuthenticated ? (
<PageTemplate title={title} isLanding={isLanding}>
<Component {...matchProps} query={query} />
</PageTemplate>
) : (
<Redirect
to={{
pathname: '/',
state: { from: props.location },
}}
/>
)
}
/>
)
}
So I assume you want to reset component's state once route is changed.
This should happen wherever you use functional component + hooks or class-based component with explicit this.state. It's how React works under the hood.
You already have <Create> rendered at the page
Once route is changed <Route> tries to render <Create> element
React sees there is already existing <Create> element and tries to update that instead of re-creating(typically update is much more efficient than re-creating). That's why state is not reset - since it should not reset for updates.
There are different way to handle that.
If such a case happen outside react-router's <Route> I'd suggest use key prop to reset state. But for <Route> it would mean replacing more clear/straightforward <Route path="..." component={Create} /> with more verboose <Route path="..." render={({match}) => <Create match={match} key={match.params.id} />}
So instead let's apply useEffect hook to reset state once props.match.params.id is changed:
const Create = ({ match: {params: {id} } }) => {
useEffect(() => {
setParcelType('paper');
}, [id]);
That should be equal to class-based
state = {
typeOfWhatEver: 'paper'
};
componentDidUpdate(prevProps) {
if(prevProps.match.params.id !== this.props.match.params.id) {
this.setState({
typeOfWhatEver: 'paper'
});
}
}

Categories

Resources