How to target specific components through Links in react-router? - reactjs

I want to insert links throughout the content of a React app, where links trigger 'navigation' only within specific Components in the tree. Ideally I would use a react library to do this, rather than invent my own framework from scratch.
Existing approaches that I can find, such as react-router, seem to assume that every routed component should only be visible when a route path matches it, rather than routes being able to selectively send 'control' signals to matching components, while unmatching components should not be affected at all.
My intended application needs independent navigation within different panes, similar to the behaviour of a HTML Frameset ( see e.g. this JavaDoc single-page navigation - https://docs.oracle.com/javase/7/docs/api/ ) where specific links have a href (a route in React) but also a 'target' which indicates which pane needs to be affected by a given navigation.
I am aware I could write a bespoke React eventing pattern. For example I could pass hooks to make changes through tree state, with my own bespoke hash or history eventing in place to monitor clicks. Before I consider writing my own framework for this, I want to understand what other approaches there are and I think I must be overlooking something obvious.
I have put together a repository which simplifies the problem from a react-router point of view.
https://github.com/cefn/graphql-gist/tree/fde58e9cf5d321d1edf3b916da4bdd95b79751a1/react-router-frames
This app has 'Frames' with embedded links. However, every Frame's component in the React tree has to be switched out for another matching component (or none) when a Link is clicked. Ideally I should be able to add a 'target' attribute or otherwise specialise a Link or target so that only a targeted part of the tree is affected by a matching link.
It should be possible for example, to cause the color of the name='left' or name='right' frames to change independently in https://github.com/cefn/graphql-gist/blob/fde58e9cf5d321d1edf3b916da4bdd95b79751a1/react-router-frames/src/FrameSet.js
I am hoping for something from the mainstream react ecosystem which supports routing (e.g. well-documented components with hash listening, history support) but not where every Link affects every Route in the page.

Here is a solution which exploits react-router and isn't totally horrible.
function FrameSet(props) {
return <Router>
<FilterPath pathPrefix="/left/">
<View />
</FilterPath>
<FilterPath pathPrefix="/right/">
<View />
</FilterPath>
</Router>
}
A FilterPath always passes on its pathPrefix value to its childrens' props, and optionally, (when the react-router location matches the pathPrefix) passes on a pathSuffix too.
In this way, each View above only receives a pathSuffix update when a route path begins with their pathPrefix and hence 'targets' them.
A draft working implementation of FilterPath is...
function FilterPath(props) {
return <Route render={({ location: { pathname } }) => {
const { pathPrefix } = props
let pathSuffix
if (pathname.startsWith(pathPrefix)) {
pathSuffix = pathname.slice(pathPrefix.length)
}
return React.Children.map(props.children, child => React.cloneElement(child, { pathPrefix, pathSuffix }))
}} />
}
A working example using FilterPath can be seen at https://github.com/cefn/graphql-gist/blob/2de588be6c2d30b92d452f71749377c9dc6c19c7/react-router-frames/src/FrameSet.js#L22

Related

React Router v6 - Handle both external hrefs and interal paths

In my react app with react-router v6 I am retrieving links dynamically that can be either a regular external url (like https://stackoverflow.com/) or an internal path (like "/my-page") that corresponds to a Route with react-router.
So far, when using react-router-dom's <Link> component, I could only get it to work with an internal path, but NOT with an external url. As far as I understood the docs I couldn't find a way to achieve this, at least not with v6.
So the best approach I've come up with so far is to write an own MyLink component that either render an <a> or a <Link> depending on whether the href is external or not:
function MyLink(props) {
const isHrefExternal = props.href.match(/^http|^https|^www/);
if (isHrefExternal) {
return <a href={props.href}>{props.children}</a>;
}
return <Link to={props.href}>{props.children}</Link>;
}
This of course doesn't look like a very good solution:
The check for isHrefExternal is very naive
It's unclear what props MyLink should accept and how they should be managed since <a> and <Link> have different props.
For a full example see this codesandbox
Can you tell me how to do it better? Ideally there would be an option to pass an external url to react-router-dom's <Link> component but I couldn't find one.
Thanks a lot!
The Link component handles only internal links within your app, and likely won't ever handle external links natively.
I don't see any overt issues with your code, and I don't think it's a bad solution. The "isExternal" check may be naive, but it only needs to cover your specific use cases. As new use cases are required you can tweak the logic for this check. In my projects I've worked with we've just typically included an isExternal type property with fetched data so the app doesn't even need to think about what is or isn't an external link, it just checks the flag when rendering.

Code design for the multidocument tabbed interface for React SPA with react-router/react-router-dom?

I have React SPA application with react-router/react-router-dom navigation, that means that there is react-router-dom (additional layer for react-router) Switch statement with selects the according to the path:
<Switch>
<Route path='/login' component={LoginPage}/>
<Route path='/about' component={AboutPage}/>
<Route exact path='/' component={HomePage}/>
<Route path='/sales/orderlist' component={OrderListPage}/>
<Route path='/sales/order' component={() => <OrderPage/>}/>
<Route path='/sales/order/:id' component={() => <OrderPage/>}/>
<Route path='/sales/invoicelist' component={InvoiceListPage}/>
<Route path='/sales/invoice' component={InvoicePage}/>
<Route path='/sales/invoice/:id' component={InvoicePage}/>
</Switch>
My application also has react-router Links in navigation bar, so - that is the way how simple navigation works. I understand that execution of the link '/sales/invoice/58' does the following 3 things:
It instantiates the InvoicePage component;
It moves the id value 58 to the props of InvoicePage;
It makes the instantiated component (effectively element with large content) visible and all the remaining instantiated components (if any, e.g. InvoiceListPage) (which are just elements/DOM trees with large content themselves) visible=false. I am not sure whether such out-navigation releases components, my guess is, that they stays in the memory (in the DOM tree). I am also not sure what happens when 2 consecutive calls 'sales/invoice/59' and 'sales/invoice/60' are made - is only one InvoicePage component instantiated and then its props attribute is changed from 59 to 60 (which may or may not trigger the full or partial data reload according to the business logic coded in that component)? Or maybe react-router assures that two different calls with two different id-values creates two different components? (these mini-questions are not questions in the sense of SO question that should be answered - I am just babbling here and I am listing the points for consideration in the question format, these are not real SO questions)
My question is - how can I make this design into multi-tab multi-document design? My application should sit into one page/tab of browser and my application shoud have application-level tabs. E.g. when user presses the link to the InvoiceList, then new application tab with InvoiceList report opens. When user selects one definite invoice and this report and click to linke then another application tab with Invoice (with specific Id) opens. And user can open many tabs with different invoices.
What is the design, ideas for such multi-tabbed multi-document interface? It should be pretty hard and confusing to create one. E.g. standard react-router displays #-link to the active component in the browser url field. But if my SPA application has multiple open/active application level tabs with multiple-open React components/documents - then what link the browser should display?
And what about Redux support - usually application has one application wide Redux store, but now - each component (InvoiceListPage, InvoicePage, OrderPage) should have their own Redux stores - i.e. Redux store should be implemented as the array and whole SPA application should keep some records - which compoent to which element of the Redux array belongs...
Just wanted to know about the general design of multi-tab multi-document interface with react-router for React SPA application for the web.
Such multi-tab GUI is quite common - e.g. Metasfresh https://github.com/metasfresh/metasfresh have one, Lotus Notes has one, multi-tab interface is the preferred organization of multi-document interface for the Microsoft WPF (at least it was when I read about it some time ago).
In reactJS you have local state (this.state or useState) you have state passed from parent components (called props), you have state passed by ancestor components (called context) you may have state from other parts of the app (redux for instance). But all of these, are just "state", based on which you render what do you want to see on the screen.
So what is MDI effectively? It is a list of open documents (so you need to have somewhere in your state such a list of open documents, each element of which sould represent the present state of that particular document), and a currently open document which should be somewhere in your state. In your example, the state that tells you which one is the currently open document is... react-router's location!
So you could use redux (or create your own store) and hold somewhere the open documents
//store's instance
{
...,
openDocuments:[
{ path: '/sales/invoice/1', title: 'Invoice #1', ... },
{ path: '/sales/invoice/2', title: 'Invoice #2', ... },
{ path: '/sales/order/13', title: 'Order #13', ... }
],
...
}
// The 'path' property of each openDocument serves as the id of that document
// and should be unique in the array
Then in your layout you should have a tabbed navigation bar that would render each item as a tab and probably highlight the active tab (by using the useMatch hook to check if page location matches the id of that particular item).
In the middle of your layout you should have your regular <Switch /> that would render the actual page based on react router's location.
You could also create some hooks like useCurrentDocumentState to read the state of the currently open document. (In that hook for example you would read from your store, find the item in the array that corresponds to the actual open document and return it)
Actually, if you think of it, the usual Single Page Application with the nav bar on the left (menu items) is just a kind of Multi Document Interface where all menu items are considered to be "opened" but only focused when you click on them.
You shouldn't actually care what happens in the DOM and wether your tabs stay open but hidden or removed from DOM. It's react's business to take care of the DOM, and actually this is one of the reasons it was built for. So that you don't bother about the DOM. You should only bother of what kind of information you want to hold open for each tab (the tabs' state) in the lifetime of the application.

How do you pass data from one view to the other via react-router-dom without using url parameters?

I use react-router-dom v 4.3.1 for client-side routing. I'm pretty new to React and can't figure out how to pass data from one view to the other without using url parameters. In Angular, the Angular router has a data property where you can pass data associated with a route. An example would be:
const appRoutes: Routes = [
{
path: 'hero/:id',
component: HeroDetailComponent,
data: { title: 'Hero Detail' }
},
];
Can you do the same in react-router-dom? If not, how would you recommend I pass data in React?
Thanks in advance for the help!
<Route path="hero/:id" render={() => <HeroDetailComponent title= "Hero Detail" />} />
Read this: Pass props to a component rendered by React Router
Or if you are using <Link> you can use pass through location object
<Link to={{ pathname: 'hero/:id', state: { title: 'Hero Detail'} }}>My route</Link>
Well you Could use the context API to create a sort of global AppState that you could update in your first component and use in your second component.
You could also abuse the localStorage API by setting a key with the data in the first component and getting it in the other.
However both of these are workarounds that Shouldn't have to be used. Why do you want to redirect to a page but not pass data to it using URL parameters.
There'a several solutions. React being a library, not a framework, doesn’t force you into a single one.
One way is to use the context api. It’s like a link to an object shared between different components.
Another one is redux, which uses context underneath, and gives you a single store for the whole app. You changes values dispatching actions to the store, so it’s a bit tricky to learn the first time.
Using a stream library would open up a lot of different options, but it’s harder to get into. Check refract if you want to go this way.
A poor man’s stream approach that may serve you is using document as a bus to pass data arround, using addEventListeners to receive data and dispatch new customEvent to send it.
Next is the simplest one of all, share a simple object. Using imports form your components, you can import the same object on both and that will be a single instance where data can be shared. Simple JavaScript. It’s not the react way though, because changes won’t trigger a repaint on the component.

Does React have keep-alive like Vue js?

I made a Todo list with React js. This web has List and Detail pages.
There is a list and 1 list has 10 items. When user scroll bottom, next page data will be loaded.
user click 40th item -> watch detail page (react-router) -> click back button
The main page scroll top of the page and get 1st page data again.
How to restore scroll position and datas without Ajax call?
When I used Vue js, i’ve used 'keep-alive' element.
Help me. Thank you :)
If you are working with react-router
Component can not be cached while going forward or back which lead to losing data and interaction while using Route
Component would be unmounted when Route was unmatched
After reading source code of Route we found that using children prop as a function could help to control rendering behavior.
Hiding instead of Removing would fix this issue.
I am already fixed it with my tools react-router-cache-route
Usage
Replace <Route> with <CacheRoute>
Replace <Switch> with <CacheSwitch>
If you want real <KeepAlive /> for React
I have my implementation react-activation
Online Demo
Usage
import KeepAlive, { AliveScope } from 'react-activation'
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<button onClick={() => setShow(show => !show)}>Toggle</button>
{show && (
<KeepAlive>
<Test />
</KeepAlive>
)}
</AliveScope>
)
}
The implementation principle is easy to say.
Because React will unload components that are in the intrinsic component hierarchy, we need to extract the components in <KeepAlive>, that is, their children props, and render them into a component that will not be unloaded.
Until now the awnser is no unfortunately. But there's a issue about it in React repository: https://github.com/facebook/react/issues/12039
keep-alive is really nice. Generally, if you want to preserve state, you look at using a Flux (Redux lib) design pattern to store your data in a global store. You can even add this to a single component use case and not use it anywhere else if you wish.
If you need to keep the component around you can look at hoisting the component up and adding a "display: none" style to the component there. This will preserve the Node and thus the component state along with it.
Worth noting also is the "key" field helps the React engine figure out what tree should be unmounted and what should be kept. If you have the same component and want to preserve its state across multiple usages, maintain the key value. Conversely, if you want to ensure an unmount, just change the key value.
While searching for the same, I found this library, which is said to be doing the same. Have not used though - https://www.npmjs.com/package/react-keep-alive

Automatic Deep Sub-Route Redirecting in React-Router

I have recently been transitioning a project from AngularJS + UI-Router+ UI-Router-Extras to React + React-Router.
One of the better features in UI-Router-Extras that I'd like to bring to React-Router is called
Deep State Redirect. In this feature, when navigating to a route which has subroutes, the application knows to redirect the user to the last subroute of it that was visited, or if none of its subroutes have yet been visited then it redirects to its first subroute to have been registered.
So for example if the loaded routing tree looks like this:
/main
|_/main_sub_1
|_/main_sub_2
/secondary
and the user starts at route /main/main_sub_2, then goes to /secondary, then goes to /main, they will be automatically redirected to /main/main_sub_2 since /main/main_sub_2 is the last subroute of /main to have been visited.
I know that I could implement this in react router by using
<IndexRedirect to={getLastSubRoute(parentRoute)}> where parentRoute is the full path of the parent <Route> tag, and getLastSubRoute is self-explanitory, but the problem with this is that I would need to add such an <IndexRedirect> tag to every single route I create, which is not optimal since the routes are loaded dynamically, there may be up to 100 subroutes, and much of the application's routing will be written by other people who I shouldn't be relying on to remember to add that tag under every <Route> tag they write.
Ideally, I should be able to apply some function or mixin to the base <Router> tag in the React routing definition to add this functionality to all routing underneath it, but I'm not sure where to start. How might I solve this problem?
Your best bet and possibly the simplest solution would be to set an onChange hook on one of the top level routes. The hook would get called with the next parameter, which would be the next route that the user would be going to.
You would also have the hierarchical structure of routes there (navigating through to parent and children of the parent), so you could dynamically redirect using the replace function, that gets passed in as a parameter also.
I implemented something similar for permission and role management. What I also did was to .bind my store to the function that I pass into the route hook. You could possibly store the route you'd like to redirect to on the user in the state tree. Basically what you refer to as getLastSubRoute.
...
<Route onChange={myRedirectFunctionThatHasStoreBound} .. >
... // other routes
</Route>
...
function myRedirectFunctionThatHasStoreBound(store, prev, next, replace, callback) {
const user = store.getState().user;
const redirectTo = getLastSubRouteForRoute(user, next);
if (redirectTo) {
replace(redirectTo);
}
// don't forget this is you list callback as a param
// your app might stop working, explanation below
callback();
}
If callback is listed as a 4th argument, this hook will run asynchronously, and the transition will block until callback is called.
EDIT: Keep in mind that this will only work if you are using react-router that's newer than or equal to in version to react-router 2.1

Resources