import React from 'react';
const App = () => {
console.log('app render');
return (
<div>
<Children />
</div>
);
};
const Children = () => {
const [update, setUpdate] = React.useState(false);
console.log('children render');
return (
<div>
<button onClick={() => setUpdate(!update)}>update</button>
</div>
);
};
export default App;
Above code, at first, two messages are printed out 'app render' and 'children render.'
Then, when I click the update button, a message is printed 'children render'.
I learned the following rules when I started to learn React at beginning.
when props are updated
when states are updated
when the parent component is updated
I've been working on React Projects well since then.
Today, somehow, I got a question.
How does react know which components have been updated?
setUpdate tells React that Children is updated?
I think I can understand that React can notice the props changing.
because, if render functions are called, it means they can know whether props of their children are changed or not.
But I don't understand how react recognizes state-changing.
The components form a tree. In the first render, the React framework renders from the root node. Any component functions that call useState will have the set state function registered and associated with the component instance. In the future, if you call a set state function, the React framework finds the associated component instance to re-render the sub-tree.
Related
I have a component that gets loaded through it's parent's .
This component renders its own components - ActionsBar and Collection.
When the URL changes and the Outlet gets rerendered - my assumption would be that everything within the comonent would get rerendered, however it appears to only rerender the child components if they have changed.
export default function ComponentLoadedInOutlet() {
const collectionData = useLoaderData();
return (
<div>
<ActionsBar />
<Collection data={collectionData} />
</div>
)
}
In the above snippet, when the outlet gets reloaded when the URL changes, it is only the Collection component that is being re-rendered because the collectionData has changed.
I would like to know if there's a way to force all the Child components within the outlet to reload, so that I don't have to mess around with resetting lots of state.
Your assumption is correct, everything within the component is re-rendered when the url changes. However there is a difference between re-rendering and re-mounting.
When navigating from one url to another which routes to the same component (i.e. a $dynamic.tsx segment), React will merely re-render the component, rather than unmount / mount the component.
This can be useful if you want to preserve state, however this isn't always desirable.
It's therefore common in React to add a key prop to reset a component hierarchy.
// /products/$slug.tsx
export const loader = ({ request, params }: LoaderArgs) =>
json({ slug: params.slug });
export default function ProductDetail() {
const { slug } = useLoaderData<typeof loader>();
return (
// NOTE: `key={slug}`
<div key={slug}>
<ActionsBar />
</div>
);
}
const ActionsBar = () => {
useEffect(() => {
// This will re-mount whenever the `slug` changes
alert("<ActionBar /> mounted");
}, []);
return <div>ActionsBar</div>;
};
This will tell React to avoid diffing the new tree with the old tree and instead just blow away the old one, creating a brand new tree & therefore mount cycle.
I'm trying to understand how React "displays and update" the code below, assuming I've understood the differences and the vocabulary explained here
https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html
import React from "react";
export default function App() {
console.log("App is rerendered")
const [time, setTime] = React.useState(0)
React.useEffect(() => {
const lol = setTimeout(() => setTime(prev => prev + 1), 100)
return () => clearTimeout(lol)
}, [time]
)
function ShowTime() {
console.log("ShowTime is rerended")
return (
<div> Time : {time / 10} sec</div>
)
}
function ShowButton() {
console.log("ShowButton is rerended")
return (
<button
onClick={() => console.log("I'm hard to click cuz rerendered the whole time :/")}>
Button created with ShowButton component
</button>
)
}
return (
<main>
<ShowTime />
<ShowButton />
</main>
)
}
React create the virtual dom with the App element, the ShowTime element, and the ShowButton element inside
It's the first render so React renders everything, creating an instance of App, containing a main DOM element, containing one instance of ShowTime and one instance of ShowButton
After 100ms, time state in App changed !
React update the virtual dom again taking account time state has changed
It's rerendering, so there is reconciliation
https://reactjs.org/docs/reconciliation.html#component-elements-of-the-same-type
says "When a component updates, the instance stays the same (...). Next, the render() method is called (...)"
React does't care if App changed or not. It's a component, and when he encounters a component in the virtual dom, when commiting, the instance stays the same, and React runs App.render()
In this case it's nice, because time state has changed.
Recursing process of reconciliation on children
In the same way, React does't care if ShowTime and ShowButton changed or not. They're components, so React keeps their instance, runs ShowTime.render() and ShowButton.render()
My two questions :
Is my understanding of the reconciliation process (concerning the components part) is right ?
So a component inside a component that has to be rendered will be rendered, whatever if it is concerned about any props or state changes or not ? (it's the case of my ShowButton component)
That's weird no ? Because of that it's very hard to click it !
The declaration of ShowButton is right inside the App render function. So React not only rerender it but inserts a new DOM button element 10 times a second. That's why it is hard to click. Move ShowButton out of App.
Reconciliation happens after all rendering.
In some cases React doesn't rerender components. We use React.memo, PureComponent and shouldComponentUpdate for the optimization. For more information please read this answer.
There are ShowTime.render() and ShowButton.render() in your text but functional components doesn't have methods.
In the same way, React does't care if ShowTime and ShowButton changed or not. They're components, so React keeps their instance, runs ShowTime.render() and ShowButton.render()
React doesn't care, as long as it's the same component. The problem is that ShowTime and ShowButton are a new component with the same name, created every time App is rerendered. It's like saying
const ShowTime = () => {
// ... (btw, don't do this either)
}
So while the ultimate structure is the same, React sees new components every rerender of App.
To solve this problem, pull the components out of App:
function ShowTime() { ... }
function ShowButton() { ... }
function App() { ... }
in React Typescript i have a parent component with two independant child components:
export const Editor = () => {
return (
<>
<TableTotal />
<hr />
<TableDetail />
</>
);
};
And after some actions in TableDetail i have to completely reload data in TableTotal component.
In TableTotal i have code like this:
useEffect(() => {
getData();
}, []);
where getData is a function with fetch from the server.
How can i told the TableTotal component to reload data from the TableDetail Component (calling the getData() function) ?
I use React with Typescript.
Thanks
The concept you need to solve this is called lifting state up in the React documentation. You store the data shared by TableTotal and TableDetail inside Editor, and then pass the data to each child component as props. That way, if one component causes the data to change, the other one will re-render with the new data, too.
There are 2 solutions out there. Either use the prop drilling method or take your app states to global (context/redux).
Prop Drilling: Initialize state in the Editor Component, if you change something in any component, make that state dependency of the useEffect and it will call getData() and re-render the components.
So I'm trying to build a single page app in react.
What I want:
On the page you can visit different pages like normal. On one page (index) i want a button the user can click that expands another component into view with a form. This component or form should be visible on all pages once expanded.
The Problem:
The index page loads some data from an api, so when the index component gets mounted, an fetch call is made. But when the user clicks the "Expand form"-Button, the state of the Parent component gets updated as expected, but the children get rerendered which causes the index component to fetch data again, which is not what I want.
What I tried
// Parent Component
const App => props => {
const [composer, setComposer] = useState({
// ...
expanded: false,
});
const expandComposer = event => {
event.preventDefault();
setComposer({
...composer,
expanded: true
});
return(
// ...
<Switch>
// ...
<Route
exact path={'/'}
component={() => (<Index onButtonClick={expandComposer}/>)}
// ....
{composer.expanded && (
<Composer/>
)};
);
};
// Index Component
const Index=> props => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(()=> {
// load some data
}, []);
if(isLoading) {
// show spinner
} else {
return (
// ...
<button onClick={props.onButtonClick}>Expand Composer</button>
// ...
);
};
};
So with my approach, when the button is clicked, the Index component fetched the data again and the spinner is visible for a short time. But I dont want to remount Index, or at least reload the data if possible
Two problems here. First, React will by default re render all child components when the parent gets updated. To avoid this behavior you should explicitly define when a component should update. In class based components PureComponent or shouldComponentUpdate are the way to go, and in functional components React.memo is the equivalent to PureComponent. A PureComponent will only update when one of it's props change. So you could implement it like this:
const Index = () =>{/**/}
export default React.memo(Index)
But this won't solve your problem because of the second issue. PureComponent and React.memo perform a shallow comparison in props, and you are passing an inline function as a prop which will return false in every shallow comparison cause a new instance of the function is created every render.
<Child onClick={() => this.onClick('some param')} />
This will actually create a new function every render, causing the comparison to always return false. A workaround this is to pass the parameters as a second prop, like this
<Child onClick={this.onClick} param='some param' />
And inside Child
<button onClick={() => props.onClick(props.param)} />
Now you're not creating any functions on render, just passing a reference of this.onClick to your child.
I'm not fully familiar with your style of React, I do not use them special state functions.
Why not add a boolean in the parent state, called "fetched".
if (!fetched) fetch(params, ()=>setState({ fetched: true ));
Hope this helps
Silly me, I used component={() => ...} instead of render={() => ...} when defining the route. As explained in react router docs, using component always rerenders the component. Dupocas' answer now works perfectly :)
I'm using React 16.8.2, and I'm having a problem with children of my component unmounting whenever state is changed in the app component.
Here's the scenario:
I have App.jsx (a functional component) with a number of state variables (useState)
The setters for some of these state variables are passed down the tree through a Context provider (useContext in the descendent)
I have a menu component (descendent of app), that invokes these setters to (for example) show a modal dialog
I have a modal dialog component (child of App), that uses the state variable as a property to determine whether it is open or not -- standard React stuff I think.
My problem: when any state variables in App are changed (through hooks of course), the children of App are unmounted and remounted- even if they have no connection to the state being changed. They aren't just re-rendered - the children are unmounted and their state is re-initialized. So the fields are cleared on my dialog, when they shouldn't be, for example.
This is already a fairly complex application, so I've spent a lot of time today isolating the problem. I then set up a simple create-react-app to try to replicate this behavior there - but this test app behaves like it should. Changing parent state, whether through a prop callback, or through a context-provided callback from the child - re-renders but does not unmount/remount and child state remains intact.
But in my real app, the components re-mount and child state gets re-initialized.
I've simplified it down to the barest that I can - I'm setting a fake state variable "foo" with "setFoo" through the Context from the child. Even though foo is not used by any component, changing the value of foo causes the children of App to unmount/remount.
In App.jsx:
const App = props => {
const [foo, setFoo] = useState(false);
// ...
const appControl = {
toggleFoo: () => setFoo(!foo);
};
// ...
return (
<AppContext.Provider value={appControl}>
... a bunch of stuff not using foo anywhere
... including, deep down:
<Menu />
</AppContext.Provider>
);
};
In Menu.jsx:
const Menu = props => {
const appControl = useContext(AppContext);
// ...
return (
... super simplified for test
<div onClick={appControl.toggleFoo}>
Toggle Foo
</div>
);
};
If I understand state properly, I do believe that changing state should result in children being re-rendered, but not re-mounted. This is what I'm seeing in my simple create-react-app test, but not in my real app.
I do see that I'm not on the latest React - perhaps upgrading will fix this?
Thanks for any insight on what I may be doing wrong, or misunderstanding here.
Solved. This is an interesting one. Here's what happened.
In my App component, I had a fairly deep tree of HOC's. Due to some dubious decisions on my part, I ended up breaking App into two components. App and AppCore. I had a reason for it, and it seemed to make sense at 3am. But to be both quick and dirty, I stuck AppCore as a const, inside my App function. I remember thinking to myself "I wonder what problems this will cause?" Now I know. Perhaps a React expert can fully explain this one to me though, as I don't see the difference between JSX assigned to a constant, and JSX returned directly. But there clearly is, and this is simple to reproduce.
To reproduce, create-react-app a test app:
create-react-app test
cd test
Then replace the contents of App.js with:
import React, { useState, useEffect } from "react";
const Menu = props => <div onClick={props.click}>Toggle Foo</div>;
const Test = props => {
useEffect(() => {
console.log("mounted");
return () => console.log("unmounted");
}, []);
return null;
};
const App = props => {
const [foo, setFoo] = useState(false);
// this is the root of the problem
// move this line outside of the function body
// and it mounts/unmounts correctly
const AppCore = props => <Test />;
return (
<>
<Menu click={() => setFoo(!foo)} />
<AppCore />
</>
);
};
export default App;
Then npm start, and when you click on "Toggle Foo" you'll see that the Test component is unmounted/remounted.
The solution here, is to simply move AppCore out of the function body. In my real app, this means I have some refactoring to do.
I wonder if this would be considered a React issue?