How to execute a function when a Reacter Router is triggered - javascript

I'm using the package Reacter Router to create and manage the routes of my application.
I am using an OAuth authentication system, which returns the following URL to my application:
http://localhost/login-meli?code=1234567890
I need that each time this route with the query "code" is triggered to execute a certain function, which as an example, can be: console.log('It works!')
I tried the code below and it worked, but I didn't find in the documentation how can I specify a specific query. I also noticed that when I add other lines of code an error is returned.
<Route exact path="/login-meli" render={() => (
console.log('works!!!')
)}/>

You can use the useLocation hook from react-router-dom.
import { Route, useLocation } from "react-router-dom";
Then in your route:
const MyComponent = () => {
const location = useLocation();
const params = new URLSearchParams(location.search);
const code = params.get('code');
if (code === '123456789') {
return (<div>It Works!!!</div>);
} else {
return (<div>404 (you can redirect to an error page</div>);
}
}
Then, for your Router:
<Route exact path="/login-meli"><MyComponent /></Route>

Related

using JWTs saved in localstorage with react context

Im having a really weird issue with My react authentication context (authProvider) and properly rendering my application page. Currently, my root app looks like this:
const App = () => {
const authCtx = useContext(AuthState);
return(
<AuthStateProvider>
<BrowserRouter>
<Switch>
{!authCtx.isLoggedIn && <Route path="/admin" component={SignInUp} />}
{authCtx.isLoggedIn && <Route path="/admin" component={Admin} />}
<Redirect from="/" to="/admin/myDashboard" />
</Switch>
</BrowserRouter>
</AuthStateProvider>
)};
Then in a seperate file that I use to manage the authentication context, I attempt to pull a JWT from local storage and verify it. If that's successful then the context is updated using state variables (including the "isLoggedIn" variable you see above").
const AuthState = React.createContext({
userName: "",
isLoggedIn: false,
authToken: null,
});
const AuthStateProvider = (props) => {
let token = null;
if(localStorage.getItem("token")) return token = localStorage.getItem("token");
const [ user, setUser ] = useState({
userName: "Anonymous",
isLoggedIn: false,
authToken: token,
});
const autoLogin = useCallback( async () => {
try {
const response = await axios({
method:'post',
url: 'http://127.0.0.1:3001/authEn'
headers: {
"Content-Type": "application/json",
"Authentication": user.authToken
}
});
if(response.status === 200){
//code to update context using setUser state handler
} else {
throw new Error("request failed");
}
} catch (e) {
console.log(e.message);
}
});
useEffect( async () => {
await autoLogin();
}, [autoLogin]);
return (
<AuthState.Provider
value={{
userName: user.userName,
isLoggedIn: user.isLoggedIn,
authToken: user.authToken
}}
>
{props.children}
</AuthState.Provider>
);
}
(I've excluded the code for my setUser handler to try and keep this short.)
So the problem is that as of right now, I'm just trying to see that the application can
A: check for stored token on initial page load / reload
B: Navigate you to either logIn or Admin page accordingly.
The app has no problem taking you to logIn page if there is a faulty/no JWT in localstorage. But when I try testing if the application can properly navigate to the admin page when there is a valid token in local storage (i have a seperate helper function to save a valid token), the page loads, but with NONE of the actual admin dashboard. Instead, all there is on the page is the token itself displayed at the top of the window as if it were just an html page with a single div containing the token as a string. I have no Idea why this happens. When I try rendering the admin component (removing the "isLoggedIn" logic and the authStateProvider) everything is fine. But each time I try adding authentication this way things start getting weird. Am I missing something obvious (usually the case)? Is this just a really stupid approach (also usually the case)? Or is this a low-level react issue (I'm not super familiar with all the intricacies of how react works under the hood.)
I think this is a bad practice to make conditions in the Switch.
Instead, you can create a separate component like ProtectedRoute or wrap your pages with a Higher-Order Component (HOC)
First way with ProtectedRoute
Pass isProtected in props if your wrapped route requires authentification
// ProtectedRoute.js
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedRoute = ({
isProtected,
children,
}) => {
const { isLoggedIn } = useContext(AuthState);
if (isProtected && !isLoggedIn) {
return <Redirect to='/login' />;
}
return children
};
export default ProtectedRoute;
Then in your switch:
<Switch>
{ /* Other routes */ }
<ProtectedRoute isProtected>
<Route path="/admin" component={Admin} />
</ProtectedRoute>
</Switch>
HOC
// withAuth
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const withAuth = (WrappedComponent: any) =>
function (props: any) {
const { isLoggedIn ) = useContext(AuthState);
if (!isLoggedIn) {
<Redirect to='/login' />
}
return <WrappedComponent {...props} />;
}
export default withAuth;
Now you can insert your route in the switch without conditions. If your component requires authentification, wrap it with withAuth.
Example:
const NeedAuth = () => (
<div>Hello I need auth</div>
);
export default withAuth(NeedAuth)
I figured out the issue, and yes it was something super small. In the line of code where I check to see if there is a token stored on localstorage, I use an if block with a return statement. I saw a while back that doing this allows for "if" statements to be written completely on a single line and without brackets {} encapsulating the code. At the time it was really just a style choice but now I see that when the if statement runs (i.e. there is a token in local storage) the return statement within overrides the return statement of the whole functional component. So rather than having a context file that returns a provider that wraps your desired children (my admin page router), It just prints the authtoken. So I returned to traditional styling for the If statement and removed the return statement and it worked fine!

How can I define the route for the following schema in react router dom

I'm trying to make the page for the following route:
/password?token=479wasc8-8ffe-47a6-fatw-624e9d2c323a&user=e238bc4c-cf79-4cc3-b4a5-8fe7ewrta54a9w8a5
My solution to that initially was like the following:
<Route exact path='/password?token=:token&user=:user' component={Password}/>
But I guess I'm missing something important here. I tried various sources, but didn't find anything close to my problem.
The Password component can make use of the useLocation hook to read the query string:
<Route path='/password' component={Password} />
import { useLocation } from 'react-router-dom'
const useQuery = () => {
return new URLSearchParams(useLocation().search)
}
const Password = () => {
const query = useQuery()
return (
<div>
<p>{query.get('token')}</p>
<p>{query.get('user')}</p>
</div>
)
}
Here's the example on the react router website

React useEffect: react on a value change to update another one who's also a dependency

I have a view that is a big paginated table.
I want to include the page index in the URL query params to be able to share a specific page of the table by sharing the URL.
My problem is that the page isn't the only query param, so what I need to do is, when a page change:
Retrieve the current query params
Create the new query params from the old ones and the new page
Update the query params.
However, updating the query params is a problem, because the query params are a dependency of the function that retrieves the old ones and I am stuck in this loop.
I first tried to do something like this:
import query from 'qs'
import { useLocation, useHistory } from 'react-router-dom'
function Comp() {
const [pageIndex, setPageIndex] = useState(0)
// Updates the query params using the provided ones.
const updateQueryParams = useCallback(
(pageIndex) => {
const oldSearch = query.parse(location.search, { ignoreQueryPrefix: true })
const newSearch = { ...oldSearch, pageIndex }
history.push({ pathname: location.pathname, search: query.stringify(newSearch) })
},
[location.search, location.pathname]
)
// Updates the query params when pageIndex changes.
useEffect(() => updateQueryParams(pageIndex), [updateQueryParams, pageIndex])
}
But updating the query params updates location.search which is a dependency of the updateQueryParams callback and it triggers an infinite loop.
I tried to make use of useRef to store the search and the pathname, updating them manually, but I've been unable to come up to a working solution.
My last try was to extract the logic in a custom hook to cut the problem in smaller pieces (and I will have to reuse this piece of logic elsewhere).
The current state of this hook is:
import { useLocation, useHistory } from 'react-router-dom'
import { useRef, useCallback } from 'react'
import query from 'qs'
export default function useQueryParams<QueryParams extends query.ParsedQs>(): [
QueryParams,
(newSearch: QueryParams) => void
] {
const location = useLocation()
const history = useHistory()
const search = useRef(query.parse(location.search, { ignoreQueryPrefix: true }) as QueryParams)
const pathname = useRef(location.pathname)
const setSearch = useCallback(
(newSearch: QueryParams) => {
history.push({ pathname: pathname.current, search: query.stringify(newSearch) })
search.current = newSearch
},
[history, pathname]
)
return [search.current, setSearch]
}
But it doesn't solve my problem.
Trying to access the route triggers an infinite loop and I get a Warning: Maximum update depth exceeded error in the console.
The problem was caused by how my routes was declared.
The solution has been to update all my routes from this pattern:
<Route path="/login" component={Login} />
to this one:
<Route path="/login">
<Login />
</Route>

testing routing capabilities of preact-router

How would I be able to test the router in the code below? When using React you are able to use MemoryRouter to pass initialEntries to mock a route change but I cannot find an alternative for preact-router. I looked at the Preact docs and the preact-router docs but I am unable to find a clear solution.
import 'preact/debug';
import { h, render } from 'preact';
import HomePage from './pages/homepage';
import Router from 'preact-router';
import AsyncRoute from 'preact-async-route';
import './styles/index.scss';
const App = () => (
<Router>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
export default App;
This is a little old, but I figured I would share what I found.
The first and quickest thing to do is to just use the route function in preact-router.
import { render, route } from 'preact-router';
import App from './App';
describe('<App/>', () => {
it('renders admin', async () => {
const { container, findByText } = render(<App/>);
// Go to admin page
route('/admin');
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
});
While this works, I don't like that it relies on the brower's real history. Luckily, the <Router> component accepts a history prop of type CustomHistory. So you can use an in-memory implementation of a History API to make this happen. I think I've seen docs that suggest using the history package - however I had to make an adjustment
import { createMemoryHistory } from 'history';
class MemoryCustomHistory {
constructor(initialEntries = undefined) {
this.wrapped = createMemoryHistory({initialEntries});
}
get location() {
return this.wrapped.location;
}
// Listen APIs not quite compatible out of the box.
listen(callback) {
return this.wrapped.listen((locState) => callback(locState.location));
}
push(path) {
this.wrapped.push(path);
}
replace(path) {
this.wrapped.replace(path);
}
}
Next, update your app to accept a history property to pass to the <Router>
const App = ({history = undefined} = {}) => (
<Router history={history}>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
Finally, just update the tests to wire your custom history to the app.
it('renders admin', async () => {
const history = new MemoryCustomHistory(['/admin]);
const { container, findByText } = render(<App history={history}/>);
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});

Server Side Rendering React Router + Redux load data when entering route

I'm using React Router with Redux with server rendering.
I have component that will fetching data from remote api, so I need wait for loading at server side, when making server side rendering
I've used this solution
https://github.com/bananaoomarang/isomorphic-redux
server.jsx
... after matching route, creating storage etc. wait for all needed promises completed ...
fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
.then(renderView)
shared/lib/fetchComponentData.jsx
export default function fetchComponentData(dispatch, components, params) {
const needs = components.reduce( (prev, current) => {
return current ? (current.needs || []).concat(prev) : prev;
}, []);
const promises = needs.map(need => dispatch(need(params)));
return Promise.all(promises);
}
and component that I wrote myself was
export default class ListComponent extends React.Component {
static needs = [
loadSomeData
]
render() {
return (
<div>
</div>
);
}
}
My routes.js was
export default (store) => {
return (<div>
<Route path='/'>
<IndexRoute component={AnotherComponent}/>
<Route path="component" component={Component}/>
</Route>
</div>);
};
So, the flow works good.
1) I go to /component by typing this in browser
2) Server.js calls fetchComponentData
3) fetchComponentData determines that component need load data before rendering, dispatch action with promise
4) wait promise for completed
5) render component and give this as html
But there is a problem. When I go to component from another by calling browserHistory.push('/component') data was not loading at all
I try to decide this by modifiying routes.js in such way
export default (store) => {
const loadData = (nextState, replace, cb) => {
store.dispatch(loadSomeData());
cb();
};
return (<div>
<Route path='/'>
<IndexRoute component={AnotherComponent}/>
<Route path="component" component={Component} onEnter={loadData}/>
</Route>
</div>);
};
But I've got another problem server give me rendered html and client call loadData again
So my question is how to avoid calling onEnter on client for the first time rendering on client?
Thank you for answers!
I propose problem here is affected by invoking routes.js on both clienside and serverside. To say it shorter:
1) there are should be 2 functions: bootstrapServer and bootstrapClient
2) bootstrapServer works well if I realized you correctly: you matches the url with a page what in your request url, dispatch data for its and render;
3) bootstrapClient should contains routes.js with onEnter - it must be invoked only on client-side
So the striking difference is that routes.js must be implemented only on clientside.

Categories