Does React Router necessarily complicate state management (without Redux)? - javascript

I'm fairly new to React to trying to wrap my head around routing via React Router while also passing required data to components. I will probably eventually incorporate Redux in my app, but I'm trying to avoid it initially.
It seems like using React Router as opposed to serving individual pages from the server means having to store state data in the App.js component since that's where the Router exists.
For example if I'm on site.com/x and I want to navigate to site.com/y and /x looks like this:
<div>
<XOuter >
<XInner />
</XOuter>
</div>
And App.js looks like this:
<BrowserRouter>
<Route exact path="/x" component={X} />
<Route exact path="/y" component={Y} />
</BrowserRouter>
... if the GET request is being called from XInner and the results will inform the content of /y, XInner will have to pass the response all the way back to App.js to properly render /y.
It seems like this could get messy quickly. Is there any way to avoid it?

This isn't as bad as you think, for two reasons:
If you use React Router's <Link> component to create links instead of using <a> directly, it will add event handlers that cancel the link's actual navigation and instead use history.pushState to do the navigation. To the user, they think they're on the new page (the URL bar shows this), but no GET request ever actually happened to load it.
React Router's paths are parsed via path-to-regexp. This lets you add parameters to the URL and then extract them from the router's props. You can also put data in the query string and then parse it later. This will let you pass state from one page to another without using any top-level React state, with the added benefit of making the browser's history and URL copying automatically work right.

The data is stored in the path instead of App.js. Path should be converted to props through pure function so the same path is always converted to the same props. That's the external state that chooses a <Route /> and sets its props.

Root of your problems lies in this design:
if the GET request is being called from XInner and the results will inform the content of /y, XInner will have to pass the response all the way back to App.js to properly render /y
Remove the if the GET request is being called from XInner... and all your concerns become moot.
Component A should not be responsible for fetching data for Component B. If /y needs data, fetch the data in Y's componentdidmount.
Example code showing the concept
fetchData(){
fetch(...) // or axios or whatever
.then(() => {
this.setState({
data: 'Hello World'
})
})
}
componentDidMount(){
this.fetchData()
}
render() {
return(
<div>
{this.state.data}
</div>
)
}

Related

how to re render component on Link click [duplicate]

Is there a way to force a React-Router <Link> to load a page from path, even when the current location is already that page? I can't seem to find any mention of this in the react-router documentations.
We have a page on a route for "apply" that loads up a landing page with a hero image, some explanatory text, etc., and an "apply for this program" button that swaps in content that acts as an application form. This all happens on the same "apply" route, because users should not be able to directly navigate to this form without first hitting the landing page.
However, when they have this form open, and they click on the apply link in the nav menu again, the entire page should reload as it would on first mount, getting them "back" (but really, forward) to the landing page again.
Instead, clicking the <Link> does nothing, because react-router sees we're already on the "apply" page, and so does not unmount the current page to then mount a different one.
Is there a way to force it to unmount the current page before then mounting the requested page, even if it's for the page users are supposedly already on? (via a <Link> property for instance?)
Note: this question was posted when React-Router meant v5, and while the problem in this post is independent of a specific React-Router versions, but the solutions are not. As such, the accepted answer is the solution for React-Router v6, so if you're still using v5, first and foremost upgrade your version of React-Router, but if you absolutely can't, the accepted answer won't work for you and you'll want this answer instead.
In the Route component, specify a random key.
<Route path={YOURPATH} render={(props) => <YourComp {...props} keyProp={someValue} key={randomGen()}/>} />
when react see a different key, they will trigger rerender.
A fix I used to solve my little need around this was to change the location that React-Router looks at. If it sees a location that we're already on (as in your example) it won't do anything, but by using a location object and changing that, rather than using a plain string path, React-Router will "navigate" to the new location, even if the path looks the same.
You can do this by setting a key that's different from the current key (similar to how React's render relies on key) with a state property that allows you to write clear code around what you wanted to do:
render() {
const linkTarget = {
pathname: "/page",
key: uuid(), // we could use Math.random, but that's not guaranteed unique.
state: {
applied: true
}
};
return (
...
<Link to={linkTarget}>Page</Link>
...
);
}
Note that (confusingly) you tell the Link which values you need pass as a state object, but the link will pass those values on into the component as props. So don't make the mistake of trying to access this.state in the target component!
We can then check for this in the target component's componentDidUpdate like so:
componentDidUpdate(prevProps, prevState, snapshot) {
// Check to see if the "applied" flag got changed (NOT just "set")
if (this.props.location.state.applied && !prevProps.location.state.applied) {
// Do stuff here
}
}
Simple as:
<Route path="/my/path" render={(props) => <MyComp {...props} key={Date.now()}/>} />
Works fine for me. When targeting to the same path:
this.props.history.push("/my/path");
The page gets reloaded, even if I'm already at /my/path.
Based on official documentation for 'react-router' v6 for Link component
A is an element that lets the user navigate to another page by clicking or tapping on it. In react-router-dom, a renders an accessible element with a real href that points to the resource it's linking to. This means that things like right-clicking a work as you'd expect. You can use to skip client side routing and let the browser handle the transition normally (as if it were an ).
So you can pass reloadDocument to your <Link/> component and it will always refresh the page.
Example
<Link reloadDocument to={linkTo}> myapp.com </Link>
At least works for me!
Not a good solution because it forces a full page refresh and throws an error, but you can call forceUpdate() using an onClick handler like:
<Link onClick={this.forceUpdate} to={'/the-page'}>
Click Me
</Link>
All I can say is it works. I'm stuck in a similar issue myself and hope someone else has a better answer!
React router Link not causing component to update within nested routes
This might be a common problem and I was looking for a decent solution to have in my toolbet for next time. React-Router provides some mechanisms to know when an user tries to visit any page even the one they are already.
Reading the location.key hash, it's the perfect approach as it changes every-time the user try to navigate between any page.
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
isFormSubmitted: false,
})
}
}
After setting a new state, the render method is called. In the example, I set the state to default values.
Reference: A location object is never mutated so you can use it in the lifecycle hooks to determine when navigation happens
I solved this by pushing a new route into history, then replacing that route with the current route (or the route you want to refresh). This will trigger react-router to "reload" the route without refreshing the entire page.
<Link onClick={this.reloadRoute()} to={'/route-to-refresh'}>
Click Me
</Link>
let reloadRoute = () => {
router.push({ pathname: '/empty' });
router.replace({ pathname: '/route-to-refresh' });
}
React router works by using your browser history to navigate without reloading the entire page. If you force a route into the history react router will detect this and reload the route. It is important to replace the empty route so that your back button does not take you to the empty route after you push it in.
According to react-router it looks like the react router library does not support this functionality and probably never will, so you have to force the refresh in a hacky way.
I got this working in a slightly different way that #peiti-li's answer, in react-router-dom v5.1.2, because in my case, my page got stuck in an infinite render loop after attempting their solution.
Following is what I did.
<Route
path="/mypath"
render={(props) => <MyComponent key={props.location.key} />}
/>
Every time a route change happens, the location.key prop changes even if the user is on the same route already. According to react-router-dom docs:
Instead of having a new React element created for you using the
component prop, you can pass in a function to be called when the
location matches. The render prop function has access to all the same
route props (match, location and history) as the component render
prop.
This means that we can use the props.location.key to obtain the changing key when a route change happens. Passing this to the component will make the component re-render every time the key changes.
I found a simple solution.
<BrowserRouter forceRefresh />
This forces a refresh when any links are clicked on. Unfortunately, it is global, so you can't specify which links/pages to refresh only.
From the documentation:
If true the router will use full page refreshes on page navigation. You may want to use this to imitate the way a traditional server-rendered app would work with full page refreshes between page navigation.
Here's a hacky solution that doesn't require updating any downstream components or updating a lot of routes. I really dislike it as I feel like there should be something in react-router that handles this for me.
Basically, if the link is for the current page then on click...
Wait until after the current execution.
Replace the history with /refresh?url=<your url to refresh>.
Have your switch listen for a /refresh route, then have it redirect back to the url specified in the url query parameter.
Code
First in my link component:
function MenuLink({ to, children }) {
const location = useLocation();
const history = useHistory();
const isCurrentPage = () => location.pathname === to;
const handler = isCurrentPage() ? () => {
setTimeout(() => {
if (isCurrentPage()) {
history.replace("/refresh?url=" + encodeURIComponent(to))
}
}, 0);
} : undefined;
return <Link to={to} onClick={handler}>{children}</Link>;
}
Then in my switch:
<Switch>
<Route path="/refresh" render={() => <Redirect to={parseQueryString().url ?? "/"} />} />
{/* ...rest of routes go here... */}
<Switch>
...where parseQueryString() is a function I wrote for getting the query parameters.
There is a much easier way now to achieve this, with the reloadDocument Link prop:
<Link to={linkTarget} reloadDocument={true}>Page</Link>
you can use BrowserRouter forceRefresh={true}
I use react-router-dom 5
Example :
<BrowserRouter forceRefresh={true}>
<Link
to={{pathname: '/otherPage', state: {data: data}}}>
</Link>
</BrowserRouter>
Solved using the Rachita Bansal answer but with the componentDidUpdate instead componentWillReceiveProps
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) { window.location.reload();
}
}
You can use the lifecycle method - componentWillReceiveProps
When you click on the link, the key of the location props is updated. So, you can do a workaround, something like below,
/**
* #param {object} nextProps new properties
*/
componentWillReceiveProps = (nextProps)=> {
if (nextProps.location.pathname !== this.props.location.pathname) {
window.location.reload();
}
};
To be honest, none of these are really "thinking React". For those that land on this question, a better alternative that accomplishes the same task is to use component state.
Set the state on the routed component to a boolean or something that you can track:
this.state = {
isLandingPage: true // or some other tracking value
};
When you want to go to the next route, just update the state and have your render method load in the desired component.
Try just using an anchor tag a href link. Use target="_self" in the tag to force the page to rerender fully.

Where is the best place to make API calls in a react/redux/reach router application?

I'm struggling to understand where the best place to make API calls in my app is?
I have my main component like this:
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<Router />
</Provider>
</React.StrictMode>,
document.getElementById('root'),
)
and inside Router I have all my routes. some of my other pages require making API calls to a server to get information so im wondering where the best place/best design would be to put these?
I have a component called PageOne and in there I make an API call on mount to retrieve some data and then add it to my redux state. but I also need the same data in my PageTwo component. obviously if the user goes via PageOne > PageTwo this is fine because that data is in the redux store so I just grab it. but im wondering if they went directly to PageTwo or refreshed the page whilst on PageTwo. that data would no longer be fetched.
so my options feel like:
fetch the data again on PageTwo. but this seems like I could be making a lot of requests (plus unnecessary requests as I might be fetching when I already have it)
can I put the fetch inside the router component? I guess this would always trigger no matter what page I went too. but again I would be doing unnecessary requests potentially
is there any "good"/best practice way of handling this?
You need to put your data fetching logic inside the redux action where you need to just call the same dispatch action whenever you want to fetch the data.
Like this:
return async dispatch => {
const res = await axios.post(`${rootUrl}/login`, data);
localStorage.setItem("authToken",
JSON.stringify(res.data.authToken));
// Set token
// setTokenToAxios(res.data.authToken);
dispatch({
type: USER_LOGIN_SUCCESS,
data: res.data.authToken
});
};
You can dispatch or invoke the action in componentDidMount or useEffect() whatever you use in.
You can put invoke or dispatch the action in both components with a flag to avoid unnecessary data fetching.

Next.js Fetching Data Inside Nested Child Component

Please consider my circumstance: In Next.js, I have built a component that is intended to be a child component that fetches data on its own (without any parent component) and now I have come to find this is not allowed by the authors of next.js. However, they mention the async-reactor library as a workaround:
May be you can try something like async-reactor
But I tried using async-reactor and was unable to render a fetch inside a nested child component in Next.js still. Here's what I tried:
// my child component
import React from 'react';
import {asyncReactor} from 'async-reactor';
import fetch from 'isomorphic-unfetch';
function Loader() {
return (
<div>
<h2>Loading ...</h2>
</div>
);
}
async function AsyncPosts() {
const data = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await data.json();
return (
<div>
<ul>
{posts.map((x) => <li key={x.id}>{x.title}</li>)}
</ul>
</div>
);
}
export default asyncReactor(AsyncPosts, Loader);
I expected this to work but it doesn't render anything except the word "Div" (which isn't even supposed to render "Div").
Is there a way to fetch within a child component in Next.js? Nothing I have tried so far worked but I find it hard to believe this is truly not possible.
As #Arunoda wrote:
We don't have plans to add support for calling getInitialProps in nested components.
The emphasis is on getInitialProps, you can make an ajax request inside any component, but know the benefits / drawback of it.
This ajax request will be implemented inside componentDidMount / useEffect hook which are not called at server-side.
One of the benefits can be lazy loading data, you don't need the entire page's data up front, that means less data => smaller network request.
One drawback can be that this section won't be passed to next's SSR mechanism, therefore won't be easily SEOed.

React - Passing state to parent component - too many AXIOS requests

I'm a beginner both in programming and React and I have to create a functioning google Map single page website. I'm using google-map-react.
I have a parent App.js (containing the call to and a HTML sidebar) and a child Map.js containing the map itself and axios request function.
I'm making axios requests to fetch data from foursquare api. It works without side effects. Then I want to pass those data to my app.js and update the parent state so that I can renderthe locations on the sidebar.
This is the function I used (in Map.js). I had to put the call in componentWillReceiveProps as a last resource because componentDidMount didn't work:
https://jsfiddle.net/kd1yuhe5/
I think this may be the issue, but it's also the only way I found to make the list show:
this.props.updateVenues(this.state.venues)
This is the code from App.js
updateVenues(venues) {
this.setState({
venues: venues,
});
}
Then I called the method like this:
<Map updateVenues={this.updateVenues.bind(this)} />
The code works, venues are shown in the sidebar (if you need the code let me know, but I don't think it's relevant), but the I keep making requests until I exceed quota.
Again: I'm a beginner. I just started 3 months ago.
EDIT:
Here are both components:
Map.js
https://jsfiddle.net/kd1yuhe5/5/
App.js
https://jsfiddle.net/xwzrm4bp/2/
When the state of a React component is updated (and without custom implementation of componentShouldUpdate), it triggers a re render of that component (ie call the render function).
If the props of the children of this component have changed since the last render, they will also re render.
They re render because they have received new props, and this will also call their componentWillReceiveProps function.
Since you are fetching data each time Map will receive props, you are fetching data each time something change (state change) on App.
First in Map.js, this.props.query is assigned to this.state.query.
This looks like an error, as in this case what you want are the new props receceived by componentWillReceiveProps, this is the first argument of this function.
So you should assign props.query to this.state.query instead.
Except that actually you should not:
this.state.query is only used in componentWillReceiveProps, therefore there is no need to put props.query into state.query.
Second since you have both this.props.query from the previous props update and props.query which is the new received query, you have the opportunity to fetch only when the query has actually changed:
// Receive the update query from parent and fetch the data
componentWillReceiveProps(nextProps){
if (this.props.query !== nextProps.query) {
this.fetchData(nextProps.query);
}
}
Now you may ask, "ok but why my Map component was always re rendered, even when its props didn't changed".
But they did:
in App.js
<Map
query={this.state.query}
center={this.state.center}
updateVenues={this.updateVenues.bind(this)}
getClickedMarker={this.getClickedMarker.bind(this)}
/>
By calling this.updateVenues.bind(this) and this.getClickedMarker.bind(this) in the render method, you are creating new values (actually new Function references)for the updateVenues and getClickedMarker props, at each render.
Instead, you should bind these method in the contructor of App:
constructor(props) {
super(props);
this.updateVenues = this.updateVenues.bind(this);
this.getClickedMarker = this.getClickedMarker.bind(this);
....
}
....
<Map
query={this.state.query}
center={this.state.center}
updateVenues={this.updateVenues}
getClickedMarker={this.getClickedMarker}
/>
This may limit your API calls a lot, you may also debounce them.

React Router Query Parameters

I have a React application which is using react-router v2.4. I have a route defined like so:
<Router history={this.props.history}>
<Route path="/:organisationId/objects"
component={ObjectsPage}
onEnter={(state) => {
this.props.dispatch(objectsFetch(
state.params.organisationId,
state.location.query
));
}}
/>
</Router>
Essentially, this route dispatches an action to fetch some objects from an API which are then rendered in a tabular format. I would like this table to be searchable, sortable and paginated and I think it would be appropriate to store the state of the table in the URL as query parameters so it can be refreshed and browser back/forward functionality isn't broken.
In order to update my table component I make a call to browserHistory with a URI similar to this (note the query parameters):
browserHistory.push('/0f08ac61-ddbd-4c73-a044-e71b8dd11edc/objects?page=2&search=query&sort=firstname|desc');
However, this doesn't trigger react-router into thinking the route has updated as the onEnter callback is never fired even though the browser history is updated with the new query string. I think I understand why this is happening; since the query string isn't part of the defined route I guess the package won't pick the change up.
Is there a way to use react-router with query parameters like this or will I have to make my filters part of the URI?
Need to use onChange in the route definition - onEnter is called the first time you hit a route and onChange is called when the route status changes:
<Route path="/:organisationId/patients"
component={PatientsPage}
onEnter={(state) => {
this.props.dispatch(patientsFetch(
state.params.organisationId,
state.location.query
));
}}
onChange={(state) => {
this.props.dispatch(patientsFetch(
state.params.organisationId,
state.location.query
));
}}
/>

Categories