React won't rerender after useEffect axios response updates state with useState. The query works, and if I click to another endpoint and back again, it does render successfully, but it doesn't successfully render on mount.
This is an expensive query so I just want to run it once when the website is loaded or refreshed.
const App: React.FC = () => {
const [cards, setCards] = useState<Card[]>([]);
// query server for list of cards on mount
useEffect(() => {
axios.get("http://localhost:3001/cards")
.then(response => setCards(response.data))
.catch(error => alert(`Error fetching cards. ${error}`))
}, []);
return (
<Routes>
<Route path='/' element={<LandingPage cards={cards} />} />
{/* This does not show all cards! */}
{cards.map((card, idx) => <Route key={idx} path={'/' + card.endpoint} element={<Card card={card}/>} />)}
</Routes>
)
}
First things first, you should add a key to each element of your map function to render the Route components correctly. This will also help to force a re-render of that component whenever the keychanges. Let's try that first and see what happens
Also, the console.log(cards) after your setCards(response.data) won't print because the state update operation hasn't been finished yet
Related
We have a problem with the react router v6. When the URL parameters change, it is already using the mount component. Never unmount and mount the component.
Produced code example - if switching between link 1 and link 2(url parameter (id) changes), the Link component never unmount and mount:
https://stackblitz.com/edit/github-agqlf5-gohmbu?file=src/routes/link.jsx
So how do we get it to be unmount and mount component when the url parameter changes?
This is by-design. If you look at the Route rendering the Invoices component
<Route path="link/:id" element={<Invoices />} />
There is nothing here to cause (a) the Route to remount, and (b) the routed component Invoices to remount. If the route path updates it will only trigger a rerender of the routed component, only values in the routing context have changed really. Think of this more like an update to props triggering a component to rerender, not remount.
If the routed component needs to "listen" to and respond to route param changes then it should use a useEffect hook to issue any side-effects.
Example:
import { Outlet, useParams } from 'react-router-dom';
export default function Invoices() {
const { id } = useParams();
React.useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
React.useEffect(() => {
console.log("Route parameter changed", { id });
}, [id]);
return (
<div style={{ display: 'flex', paddingTop: '1rem' }}>
<nav>Invoice ID: {id}</nav>
<Outlet />
</div>
);
}
If you really want to remount the routed component then create a wrapper component that reads the id route path param and sets a React key on the routed component. The InvoicesWrapper will remain mounted but when the id route param updates the Invoices component will remount since it has a new React key.
Example:
const InvoicesWrapper = () => {
const { id } = useParams();
return <Invoices key={id} />
};
...
...
<Route path="link/:id" element={<InvoicesWrapper />} />
...
I want to show a loader while my Analysis component (and its children) load but I cannot for the life of me get this to work.
The main idea is:
<Context>
<Admin>
<Analysis (with other child components) />
</Admin>
</Context>
The Admin component has a sidebar and the main view that is based upon a Switch/Router as seen here:
const Admin = () => {
const classes = useStyles();
const { pageLoading } = useContext();
return (
<>
<Box display="flex">
<Sidebar
routes={sidebarRoutes}
logo={{
innerLink: "/",
imgAlt: "...",
}}
/>
<Box position="relative" flex="1" className={classes.mainContent}>
<Switch>
{ pageLoading ?
<Loader />
: (<Route
path="/admin/stats"
component={Analysis}
key={"stats"} />) }
</Switch>
<Container
maxWidth={false}
component={Box}
classes={{ root: classes.containerRoot }}
>
</Container>
</Box>
</Box>
</>
);
};
However, since the Analysis component and its children take a while to load, I want to display a loader like this (no API calls are made):
My current loading screen
See, the problem is that I tried setting the context loading state in my useEffect hook inside the Sidebar component, like this:
const handleTabClick = () => {
setPageLoading(true);
};
And then stopping the loader using the same context after the Analysis component mounts:
React.useEffect(() => {
setPageLoading(false);
}, []);
...And the loader gets stuck forever...
My conclusion is that when the Context component has its state changed and re-renders, the Admin component then has pageLoading field set to "true" and doesn't display the Analysis component.
What can I do to fix this? It's not crucial for my website but I'm trying to find a solution to this clunkiness. Clicking on the "Analysis" tab changes URLs in my browser search bar above, but nothing happens for a few seconds while the Analysis component loads. Looks stupid.
I managed to solve this with following ugly solution:
import React, { useEffect, useState } from "react";
import { Route } from "react-router-dom";
import Loader from "./Loader";
function SubPageLoader({path, component, key}) {
const [updates, setUpdates] = useState(0);
useEffect(() => {
// stupid solution to postpone loading of children, so we can show a loading screen. TODO: find a better method
const interval = setInterval(() => {
setUpdates(updates => updates + 1);
}, 1);
if (updates > 1) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [updates]);
return (
<>
{updates == 0 ? <Loader />
: <Route
path={path}
component={component}
key={key}
/> }
</>
);
}
export default SubPageLoader;
This basically postpones the children load by 1 millisecond and that way we can instantly enter the new tab and show a loader without having to update parent states.
I am trying to conceptualize redux and its working, and after some testing, I have noticed a thing. I would like to quote this example
lets say, I have a single reducer (a boolean variable). based on that variable, the following code happens.
reducer
const initState = { isLoggedIn: false };
const isLoggedInReducer = (state = initState, action) => {
switch (action.type) {
case "LOG_IN":
return { ...state,isLoggedIn: true };
case "LOG_OUT":
return { ...state,isLoggedIn: false };
default:
return state;
}
};
export default isLoggedInReducer;
action
export const logIn = () => {
return {
type:'LOG_IN'
}
}
export const logOut = () => {
return {
type:'LOG_OUT'
}
}
index.js
<Provider store={createStore(isLoggedInReducer)}>
<Router>
<Switch>
<Route path="/" exact>
<AppScreen />
</Route>
<Route path="/auth">
<AuthScreen />
</Route>
<Route path="*">
<NotFoundScreen />
</Route>
</Switch>
</Router>
</Provider>
so, my app firstly directs the user to a component called "mainScreen" , which is as follows
const AppScreen = () => {
let isLoggedIn = useSelector((state) => state.isLoggedIn);
if (isLoggedIn)
return (
<>
<button onClick={() => dispatch(logOut())}>unauthenticate</button>
<NavBar />
<Content />
<BottomBar />
</>
);
else{
return (
<>
<Redirect to="/auth" push />
</>
);
}
};
so if the reducer state has value TRUE , my navbar and stuff is shown, else the user is redirected to the "authScreen" , which is as
const AuthScreen = () => {
let isLoggedIn = useSelector((state) => state.isLoggedIn);
const dispatch = useDispatch();
return isLoggedIn ? (
<>
<Redirect to="/" push />
</>
) : (
<>
<h1> auth is {isLoggedIn?"true":"false"}</h1>
<button onClick={() => dispatch(logIn())}>authenticate</button>
</>
);
};
This creates a setup where "authScreen" can toggle the reducer to TRUE and it re-renders, and finds that reducer is TRUE, so it renders the "mainScreen". Vice versa for "MainScreen"
Now, what components actually re-render ? If I place my authenticate button in the "navbar" instead as a sibling to "navbar" , will it re-render the "navbar" or the "mainScreen" ?
How does redux calculate what component to re-render when a peice of state changes ? How does the useSelector fit in, when I did not even use "connect".
Using hooks with redux made it very confusing. I am sorry if my explanation is hard to understand. The code actually works, I just don't know how.
Any piece of information is appreciated!
Using Redux with a UI always follows the same basic steps:
Render components using initial state
Call store.subscribe() to be notified when actions are dispatched
Call store.getState() to read the latest data
Diff old and new values needed by this component to see if anything actually changed. If not, the component doesn't need to do anything
Update UI with the latest data
React-Redux does that work for you internally.
So, useSelector decides whether a component should re-render based on whatever data you return in your selector functions:
https://redux.js.org/tutorials/fundamentals/part-5-ui-react#reading-state-from-the-store-with-useselector
If the selector return value changes after an action was dispatched, useSelector forces that component to re-render. From there, normal React rendering behavior kicks in, and all child components are re-rendered by default.
Please read my post The History and Implementation of React-Redux and talk A Deep Dive into React-Redux for details on how React-Redux actually implements this behavior.
When using React with React Router I run in some mounting issues.
This might not even be a problem with React Router itself.
I want to pass some additional data along with the child routes.
This seems to be working, however the changes on the main page trigger grandchildren to be remounted every time the state is changed.
Why is this and why doe this only happen to grandchildren an not just the children ?
Code example:
import React, { useEffect, useState } from 'react';
import { Route, Switch, BrowserRouter as Router, Redirect } from 'react-router-dom';
const MainPage = ({ ChildRoutes }) => {
const [foo, setFoo] = useState(0);
const [data, setData] = useState(0);
const incrementFoo = () => setFoo(prev => prev + 1);
useEffect(() =>{
console.log("mount main")
},[]);
useEffect(() =>{
setData(foo * 2)
},[foo]);
return (
<div>
<h1>Main Page</h1>
<p>data: {data}</p>
<button onClick={incrementFoo}>Increment foo {foo}</button>
<ChildRoutes foo={foo} />
</div>
);
};
const SecondPage = ({ ChildRoutes, foo }) => {
const [bar, setBar] = useState(0);
const incrementBar = () => setBar(prev => prev + 1);
useEffect(() =>{
console.log("mount second")
},[]);
return (
<div>
<h2>Second Page</h2>
<button onClick={incrementBar}>Increment bar</button>
<ChildRoutes foo={foo} bar={bar} />
</div>
);
};
const ThirdPage = ({ foo, bar }) => {
useEffect(() =>{
console.log("mount third")
},[]);
return (
<div>
<h3>Third Page</h3>
<p>foo: {foo}</p>
<p>bar: {bar}</p>
</div>
);
};
const routingConfig = [{
path: '/main',
component: MainPage,
routes: [
{
path: '/main/second',
component: SecondPage,
routes: [
{
path: '/main/second/third',
component: ThirdPage
},
]
}
]
}];
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
</Route>
);
})}
</Switch>
);
};
export const App = () => {
return(
<Router>
<Routing routes={routingConfig}/>
<Route exact path="/">
<Redirect to="/main/second/third" />
</Route>
</Router>
)
};
export default App;
Every individual state change in the MainPage causes ThirdPage to be remounted.
I couldn't create a snippet with StackOverflow because of the React Router. So here is a codesandbox with the exact same code: https://codesandbox.io/s/summer-mountain-unpvr?file=/src/App.js
Expected behavior is for every page to only trigger the mounting once.
I know I can probably fix this by using Redux or React.Context, but for now I would like to know what causes this behavior and if it can be avoided.
==========================
Update:
With React.Context it is working, but I am wondering if this can be done without it?
Working piece:
const ChildRouteContext = React.createContext();
const ChildRoutesWrapper = props => {
return (
<ChildRouteContext.Consumer>
{ routes => <Routing routes={routes} {...props} /> }
</ChildRouteContext.Consumer>
);
}
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<ChildRouteContext.Provider value={routes}>
<Component {...rest} ChildRoutes={ChildRoutesWrapper}/>
</ChildRouteContext.Provider>
</Route>
);
})}
</Switch>
);
};
To understand this issue, I think you might need to know the difference between a React component and a React element and how React reconciliation works.
React component is either a class-based or functional component. You could think of it as a function that will accept some props and
eventually return a React element. And you should create a React component only once.
React element on the other hand is an object describing a component instance or DOM node and its desired properties. JSX provide
the syntax for creating a React element by its React component:
<Component someProps={...} />
At a single point of time, your React app is a tree of React elements. This tree is eventually converted to the actual DOM nodes which is displayed to our screen.
Everytime a state changes, React will build another whole new tree. After that, React need to figure a way to efficiently update DOM nodes based on the difference between the new tree and the last tree. This proccess is called Reconciliation. The diffing algorithm for this process is when comparing two root elements, if those two are:
Elements Of Different Types: React will tear down the old tree and build the new tree from scratch // this means re-mount that element (unmount and mount again).
DOM Elements Of The Same Type: React keeps the same underlying DOM node, and only updates the changed attributes.
Component Elements Of The Same Type: React updates the props of the underlying component instance to match the new element // this means keep the instance (React element) and update the props
That's a brief of the theory, let's get into pratice.
I'll make an analogy: React component is a factory and React element is a product of a particular factory. Factory should be created once.
This line of code, ChildRoutes is a factory and you are creating a new factory everytime the parent of the Component re-renders (due to how Javascript function created):
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
Based on the routingConfig, the MainPage created a factory to create the SecondPage. The SecondPage created a factory to create the ThirdPage. In the MainPage, when there's a state update (ex: foo got incremented):
The MainPage re-renders. It use its SecondPage factory to create a SecondPage product. Since its factory didn't change, the created SecondPage product is later diffed based on "Component Elements Of The Same Type" rule.
The SecondPage re-renders (due to foo props changes). Its ThirdPage factory is created again. So the newly created ThirdPage product is different than the previous ThirdPage product and is later diffed based on "Elements Of Different Types". That is what causing the ThirdPage element to be re-mounted.
To fix this issue, I'm using render props as a way to use the "created-once" factory so that its created products is later diffed by "Component Elements Of The Same Type" rule.
<Component
{...rest}
renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>
Here's the working demo: https://codesandbox.io/s/sad-microservice-k5ny0
Reference:
React Components, Elements, and Instances
Reconciliation
Render Props
The culprit is this line:
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
More specifically, the ChildRoutes prop. On each render, you are feeding it a brand new functional component, because given:
let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>
a === b would always end up false, as it's 2 distinct function objects. Since you are giving it a new function object (a new functional component) on every render, it has no choice but to remount the component subtree from this Node, because it's a new component every time.
The solution is to create this functional component once, in advance, outside your render method, like so:
const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />
... and then pass this single functional component:
<Component {...rest} ChildRoutes={ChildRoutesWrapper} />
Your components are remounting every time because you're using the component prop.
Quoting from the docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
The solution you probably need in your case is to edit your Routing component to use render instead of children.
I'm using react-router (v.4.3.1) to render the main part of my application and I have a drawer on the left side with the menu. When a button is toggled in the app header I'm changing the state of the collapsed variable so that the components re-render the css accordantly. My problem is this variable needs to be stored on the component rendering all my Route and when the component is updated Route is unmounting and mounting it's component.
I've already tried to provide a key to my Route but it's not working.
My code looks like this and the parent of this component is the one being updated which re-renders my Main component:
class Main extends Component {
constructor(props) {
super(props);
this.observer = ReactObserver();
}
getLayoutStyle = () => {
const { isMobile, collapsed } = this.props;
if (!isMobile) {
return {
paddingLeft: collapsed ? '80px' : '256px',
};
}
return null;
};
render() {
const RouteWithProps = (({index, path, exact, strict, component: Component, location, ...rest}) =>
<Route path={path}
exact={exact}
strict={strict}
location={location}
render={(props) => <Component key={"route-" + index} observer={this.observer} {...props} {...rest} />}/>
);
return (
<Fragment>
<TopHeader observer={this.observer} {...this.props}/>
<Content className='content' style={{...this.getLayoutStyle()}}>
<main style={{margin: '-16px -16px 0px'}}>
<Switch>
{Object.values(ROUTES).map((route, index) => (
<RouteWithProps {...route} index={index}/>
))}
</Switch>
</main>
</Content>
</Fragment>
);
}
}
I would like the Route just to update and not to unmount the component. is this possible?
you are having this issue due to defining RouteWithProps inside of render method. This causes React to unmount old and mount a new one each time render method is called. Actually creating component dynamically in the render method is a performance bottleneck and is considered a bad practice.
Just move the definition of RouteWithProps out of Main component.
Approximate code structure will look like:
// your impors
const RouteWithProps = ({observer, path, exact, strict, component: Component, location, ...rest}) =>
<Route path={path}
exact={exact}
strict={strict}
location={location}
render={(props) => <Component observer={observer} {...props} {...rest} />}/>;
class Main extends Component {
...
render(){
...
{Object.values(ROUTES).map((route, index) => (
<RouteWithProps key={"route-" + index} {...route} observer={this.observer}/>
))}
^^^ keys should be on this level
...
}
}