React hooks: API failure results in component re rendering - reactjs

when my API returns error my page parent component rerenders and I lost all my data of child components. Strange is the case as when I try again to submit, similar error is thrown by API but my parent component doesn't re-render thus filled data remain preserve which I want for the first time error as well.
configured hooks in parent component called on first time api errors which results in broken behaviour while 2nd time as component doesn't rerender hooks doesn't call as well.
Tried to make code as minimum as possible.
Parent has ChildA and ChildA has ChildB as child component.
I filled some details in ChildB and then submit the form with button available in Parent. Form submission calls an API which results into error due to validation failure on backend. But this error re-renders Parent which causes all details filled in ChildB to be lost. when I again fill same details and submit the form...though same API error is thrown(as Expected) but this time Parent doesn't re-render and ChildB information remain preserve.
Any idea why Parent rerenders with first time API error
const Parent = ({ match, history, location }) => {
useEffect(() => {
let filterValuesUsedInChildBComponenet = someAPICall()
}, [dispatch, param]);
const saveHandler = (isExit) => {
dispatch(
campaignFormOperations.saveForm(
record,
null,
null,
isExit
)
);
};
return (
<ChildA
filterValuesUsedInChildComponenet={filterValuesUsedInChildComponenet}
updateFilterValuesUsedInChildComponenet={updateFilterValuesUsedInChildComponenet}
..
/>
);
};
const ChildA = (props) => {
...
return (
<ChildB
filterValuesUsedInChildComponenet={filterValuesUsedInChildComponenet}
updateFilterValuesUsedInChildComponenet={updateFilterValuesUsedInChildComponenet}
..
/>
);
};
Any suggestion are welcome.

Related

remix run - how to reload all child components of a component loaded through an <Outlet/>

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.

Can't access to updated localStorage in Child component (React)

In Parent component i'm getting data from an API, then I save it to the localStorage. In Parent there's React Router's Switch and Route to Child component. Right after i clean my localStorage and I refresh the page, Parent fetching data from API and save it to the localstorage but I cant get access to localstorage in the Child element.
Parent:
const getWarriorsData = () => {
axios
.get(APIAddress)
.then(response => response.data)
.then(({warriors}) => {
let warriors_numbers = [];
warriors.forEach((warrior) => {
let warriorString = JSON.stringify(warrior);
localStorage.setItem(warrior.number, warriorString);
warriors_numbers.push(warrior.number);
});
localStorage.setItem('warriorsNumbers', JSON.stringify(warriors_numbers));
localStorage.setItem('expire', Date.now() + 259200000);
});
};
useEffect( () => {
if(localStorage.getItem('expire') < Date.now() || localStorage.getItem('expire') === null ){
getWarriorsData();
};
},[]);
return (
<Router>
<MenuContext.Provider value={[linksContext, setLinksContext]}>
<Menu />
<Switch>
<Route path="/" exact component={Child} />
</Switch>
</MenuContext.Provider>
</Router>
);
Child
console.log({...localStorage}); //empty after cleaning localstorage and page refresh
The reason for your Problem:
As you know the child is a part of the render cycle of your parent.
So consider the parent here:
Being a functional component what happens is first the returned JSX is executed , then the useEffect hooks are executed as the components be it functional or class based component all will run render once before going on to the lifecycle of the component.
Hence now your child component has been rendered even before your API is called.
To debug this what you can do is have a console.log for your use effect in parent which will run on mount. Similarly in Child as well.
Also logs can be added in the starting of components as well as the function gets executed when the render is called.
This will result in :
// log from child useEffect
// log from parent useEffect
Which is the exact reason for you to not receive the data in child.
Solution for your Problem:
The main solution for you would be to delay the child from rendering until the API call is made which can be based on a flag or an enum or even the data itself which can become a part of the state for parent.
I would recommend you to choose the above options and also avoid usage of localstorage as this does not warrant the use case of a localStorage. You could rather have it as a state and pass it down to child as props or if your use case is a larger component tree then context or redux can be considered.

Adding a component to the render tree via an event handler, the component doesn't seem to receive new props. Why is this?

I have a context provider that I use to store a list of components. These components are rendered to a portal (they render absolutely positioned elements).
const A = ({children}) => {
// [{id: 1, component: () => <div>hi</>}, {}, etc ]
const [items, addItem] = useState([])
return (
<.Provider value={{items, addItem}}>
{children}
{items.map(item => createPortal(<Item />, topLevelDomNode))}
</.Provider>
)
}
Then, when I consume the context provider, I have a button that allows me to add components to the context provider state, which then renders those to the portal. This looks something like this:
const B = () => {
const {data, loading, error} = useMyRequestHook(...)
console.log('data is definitely updating!!', data) // i.e. props is definitely updating!
return (
<.Consumer>
{({addItem}) => (
<Button onClick={() => {
addItem({
id: 9,
// This component renders correctly, but DOESN'T update when data is updated
component: () => (
<SomeComponent
data={data}
/>
)
})
}}>
click to add component
</Button>
)}
</.Consumer>
)
}
Component B logs that the data is updating quite regularly. And when I click the button to add the component to the items list stored as state in the provider, it then renders as it should.
But the components in the items list don't re-render when the data property changes, even though these components receive the data property as props. I have tried using the class constructor with shouldComponentUpdate and the the component is clearly not receiving new props.
Why is this? Am I completely abusing react?
I think the reason is this.
Passing a component is not the same as rendering a component. By passing a component to a parent element, which then renders it, that component is used to render a child of the parent element and NOT the element where the component was defined.
Therefore it will never receive prop updates from where I expected it - where the component was defined. It will instead receive prop updates from where it is rendered (although the data variable is actually not coming from props in this case, which is another problem).
However, because of where it is defined. it IS forming a closure over the the props of where it is defined. That closure results in access to the data property.

Updating Parents state from Child without triggering a rerender of Child in React

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 :)

Relations setState and child component

I made some app to get asynchronously data from remote server.
The app just parent component - to get data, and two child components.
One child component for display asynchronous data.
Other child for filter functionality. Just input string where user typing and data in first component display appropriate items.
There are a lot code with console.log everywhere, but in simple scheme it:
class App extends Component {
state = {isLoading:true, query:''}
getData = (location) => {
axios.get(endPoint).then(response=>{ response.map((item) => { places.push(item)})
// ***** first setState
this.setState({isLoading:false})
})
}
updateQuery = (e) => {
// ***** second setState
this.setState({query:e.target.value.trim()})
}
componentDidMount(){
this.getData(location)
}
render() {
if (!this.state.isLoading){
if (this.state.query){
const match = new RegExp(escapeRegExp(this.state.query),'i')
searchTitles = places.filter(function(item){return match.test(item.name)})
}else{
searchTitles = places.slice();
}
}
return (
<div className="App">
<input type='text' onChange={this.updateQuery} value={this.state.query}/>
<List places = {searchTitles}/>
</div>
);
}
}
export default App;
When state change in case of using everything is OK - content refreshed in next child component.
But child component that display data - some items not full of content... no photos and some text information. So probably its rendered before getting remote data.
But why its not re-render it after state.isLoad toggled to 'false' (in code - after got response) ?
I put among code console.log to track processes ... and weird things: state.isLoad switched to false before some part of data came from server. (((
I dont use ShouldComponentUpdate() inside child component.
Per React's documentation for setState
setState() will always lead to a re-render unless
shouldComponentUpdate() returns false.
As mentioned, one way to avoid a re-render is shouldComponentUpdate returning false (shouldComponentUpdate takes in nextProps and nextState) but it's not clear why someone would trigger a state change with setState and then nullify that state change with shouldComponentUpdate.

Resources