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

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.

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.

how to clear data of useState when route get changed

I have maintained state by using useState hook in react. I want to clean value that is getting maintain in state when route get changes.
For example - I have 4 routes declared in react based project. which are as below
<Router>
<Layout>
<Route exact path="/" component={Home}></Route>
<Route exact path="/defineFacilities" component={DefineFacilities}></Route>
**<Route exact path="/createNewModel/:id" component={ModelFormsContainer}></Route>**
<Route exact path="/viewExistingModels" component={ViewExistingModels}></Route>
<Route exact path="/importNewModel" component={ImportNewModel}></Route>
</Layout>
I have maintained state in ModelFormsContainer component. I want to clean state values when user move to other routes. Currently when I move to other route and back ModelFormsContainer component then I noticed that my state are still available.
I wasn't able to reproduce the issue you describe, but if you need to do something when the route changes you can listen for changes to the route/location via the history object.
history.listen
Starts listening for location changes and calls the given callback
with an Update when it does.
// To start listening for location changes...
let unlisten = history.listen(({ action, location }) => {
// The current location changed.
});
// Later, when you are done listening for changes...
unlisten();
In the ModelFormsContainer access the passed history prop instantiate a listener when the component mounts, pass it a callback that updates the state.
Example:
useEffect(() => {
const unlisten = history.listen(() => {
console.log("route changed!!");
// apply business logic to set any component state
});
console.log("ModelFormsContainer mounted");
return unlisten;
}, []);
If ModelFormsContainer is a class component then obviously use componentDidMount and componentWillUnmount and save unlisten as a class instance variable, i.e. this.unlisten.

React hooks: API failure results in component re rendering

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.

React - sharing variables through useContext, to update and re-render components, but not working

I am creating an exercise app (using MERN stack and graphql)
where it takes keywords from the user and fetches youtube videos matching those keywords.
After fetching the youtube data, I save that exercise in my mongoDB database, and also saved them in an array exerciseArr. This exerciseArr is a shared through different react components, through useContext.
The fetching request and receiving data happens in my ExerciseForm component.
I am using ExerciseList component to render those fetched exercises(saved in exerciseArr), whenever there is an update in exerciseArr, it should re-render items in exerciseArr to ExerciseVideo. That is happening in useEffect of ExerciseList component.
useEffect(() => {
console.log('exerciseArr in ExerciseList', exerciseArr)
const fetchedExercises = exerciseArr.map( exercise => (
<ExerciseVideo key={exercise._id}
id={exercise._id}
bodysection={exercise.bodysection}
duration={exercise.duration}
title={exercise.title}
videoUrl={exercise.videoUrl}
favorite={exercise.favorite}/>))
setVideoElements(fetchedExercises)
}, [exerciseArr])
The issue now is that ExerciseList component only renders with 1st request from ExerciseForm, and it does not re-render, unless you initiate some action(click event etc) with the already rendered video elements which is the prat of the ExerciseList component.
My understanding is that every time there is an update of exerciseArr, useEffect in ExerciseList would re-render. But this is not happening?
To force to re-render the ExrciseList component, I need to initiate click event or any sorts, with already existing ExerciseVideo elements which is part of the ExerciseList component.
This is how I render ExerciseList component in App
....
function App() {
const { isAuth, exerciseArr } = useContext(Context)
const hasExercise = exerciseArr.length > 0
// const authenticatedView = <><ExerciseForm/></>
return (
<div className="App">
<Switch>
<Route exact path="/">
<NavBar />
<Manual />
{ isAuth ? <ExerciseForm/> : <Login/> }
{ isAuth && hasExercise ? <ExerciseList /> : ''}
</Route>
....
I wonder if I am not using useEffect in ExerciseList component correctly, in order to re-render itself. Or how I am rendering ExerciseList component in App needs to change.
Can somebody help me with this issue?
I found the error which was causing the problem.
The useEffect function is correct, but the if statement where it should render and update the exerciseArr was returning false.
if(resData.data.exercises.__typename === 'ExerciseData') {
//then save those results in exerciseArr
const graphqlResponse = resData.data.exercises.exercises
return setExerciseArr(graphqlResponse)
}
this block's resData.data.exercises.__typename path was not correct before.
Thus not updating the exerciseArr at all.
*sidenote, I moved this useEffect function into Context and only render exerciseArr in ExerciseList to create video div elements now, to make component more simple.

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

Resources