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.
Related
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.
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.
I couldn't find the answer anywhere because nothing was working for me, so I'm starting a new topic. Please, don't mark it as a duplicate
Router.js:
<Switch>
<Route path="/" exact component={Home} />
<Route exact path="/p/:uid" component={Profile} />
</Switch>
Profile.js
constructor(props) {
super(props);
this.state = {
loading: true,
profile_user_id: this.props.match.params.uid,
};
}
Then, later in Profile.js, I trigger a fetch to get data from backend and save this data to state using this.setState({ ... }). When the Profile component is rendered, everything looks fine.
In Router.js, there is also a Link:
<Link to={"/p/" + this.state.ntuser.tag}>Profile</Link>
.. which should go to your own profile. So when your user ID is 1, and you visit the profile of the user with id 22, your URL will be /p/user22 and the link will point to /p/user1.
The problem is that even though profiles are rendered nicely, Profile component does not become re-rendered when you click the link (which should direct you to /p/user1 and show your profile instead). I tried to save location from react-router to state as well, so every time URL changes it will be caught in componentWillReceiveProps() and inside I update state. But still nothing. Any ideas?
PS: I'm using React.Component
console.log(this.props.match.params.uid) in constructor, componentDidMount() and componentDidUpdate() (UNSAFE_componentWillReceiveProps() is deprecated)
Number and places (of log calls) will tell you if component is recreated (many construcor calls) or updated (cDM calls). Move your data fetching call accordingly (into cDM or cDU ... or sCU).
You can save common data in component above <Router/> (f.e. using context api) - but this shouldn't be required in this case.
Solution
You can update state from changed props using componentDidUpdate() or shouldComponentUpdate(). componentDidUpdate should be protected with conditions to prevent infinite loop. See docs.
Simplest solution:
componentDidUpdate(prevProps) {
if (this.props.some_var !== prevProps.some_var) {
// prop (f.e. route '.props.match.params.uid') changed
// setState() - f.e. for 'loading' conditional rendering
// call api - use setState to save fetched data
// and clearing 'loading' flag
}
}
shouldComponentUpdate() can be used for some optimalizations - minimize rerenderings.
Had same problem in my project, Didn't find any good workable idea exept making somethink like this:
import {Profile} from "../Profile "
<Link to={`your href`}
onClick={userChanged}/>
..........
function userChanged() {
const userCard= new Profile();
userCard.getProfileData();
}
First of all you need to make your Profile component as export class Profile extends React.Component (export without default).
getProfileData is method where i get data from my api and put it state. This will rerender your app
Here is what happens:
You go to /p/user22.
React renders <Route exact path="/p/:uid" component={Profile} />.
The Route renders the Profile component for the first time.
The Profile component calls the constructor, at the time, this.props.match.params.uid is equal to "user22". Therefore, this.state.profile_user_id is now "user22".
You click on the link to /p/user1.
React renders <Route exact path="/p/:uid" component={Profile} />, which is the same Route.
The Route then rerenders the same Profile component but with different props.
Since, its the same Profile component, it does not update the state.
This explains why you still see the profile of user22
I am using react-router-redux and I'm trying to update the header of my app, that receives it's state from the store, whenever the route changes (##router/UPDATE_LOCATION)
Currently I'm dispatching actions in a componentWillMount like:
componentWillMount() {
this.props.appActions.setHeader('New Block')
}
When I manually set the header in componentWillMount on route /blocks/new, and it is a child of a route 'blocks', who both have a different header, it doesn't work when I go back in history, because the component of route blocks does not mount again, it is still mounted. Thus the header is still New Block. And not what its own header was before, when blocks mounted, and new was still unmounted as child.
(And when I try to reverse time with the redux-devtools, what seems to happen then, every time I go back to a point where a component mounts again, it will dispatch the action again, and the devtool will receive another dispatch.)
The routes:
<Route path="begin" component={PlayerBeginContainer}>
<IndexRoute component={PlayerOverview}/>
<Route path="blocks" component={PlayerBlocks}>
<Route path="new" component={PlayerNewBlock}/>
</Route>
</Route>
...
I've tried to sync the store whenever a route changes, but:
if (action && action.type === UPDATE_LOCATION) {
let path = action.payload.pathname.split('/')
// Laboriously iterate through array to figure out what the new header state should be.
// i.e. if (1 in split && split[1] === 'routeName')
// or let lastPath = path[path.length - 1]
// and getting parentPath would require more checking of whether it is the parent itself or not etc.
// appHeader = 'routeHeader'
return Object.assign({}, state, { appHeader: appHeader});
}
This gets very tedious when you just need it to trigger on a specific sub-route,
And I want to avoid making another nested structure, while I already have that defined in the router.
In the header I can't use anything other than this.props.location.pathname either to try and figure out which route i'm on, and the components themselves should not bother with setting the header themselves (i.e. in componentWillMount).
Last option would be to use the router onEnter, but I'd like to keep the router clean, but perhaps I need to compromise on this.
Is there something I'm missing here? Or some sort of lib that can help me with this?
TL;DR: How can I make my header component aware of which route we are on, without having to break down the location.pathname to figure out where we are?
This is code from one of my codebases. In this app i use hash history, but i think you could do same thing with other history objects too.
import {hashHistory} from 'react-router';
import {syncHistoryWithStore} from 'react-router-redux';
const history = syncHistoryWithStore(hashHistory, store);
history.listen(location => {
// here you can do something after location is updated
});
<Router history={history}>
....
</Router>
and then in you components you can get some info from state.routing:
function mapStateToProps(state) {
return {
current: state.routing.locationBeforeTransitions.pathname,
};
}
export default connect(mapStateToProps)(App);
To change route from some component, do this:
import {push} from 'react-router-redux';
this.props.dispatch(push('/route'));
I have a component, which receives a certain prop from a route - let's call it day. Then, there is a part of redux state - calendar, which I get using connect() and mapStateToProps. Now, when a date is outside of the calendar, in componentWillReceiveProps I dispatch a redux action, to recalculate calendar again to include a date in it. The action is fully synchronous, no requests are being sent. In that case, I get the invariant violation from react:
findComponentRoot(..., .0.1.0.$20160122): Unable to find element
What in what I'm doing is causing the problem? I'm not doing any tables or nested paragraphs/forms. When I dispatch action with setTimeout(..), everything works as expected, so I guess it's some concurrency issue.
This is how my component code looks like:
componentWillReceiveProps(props) {
if (!dateInArray(props.day, props.calendar)) {
props.actions.calculateCalendar(props.day);
// This works:
// setTimeout(() => props.actions.calculateCalendar(props.day), 0);
}
}
#autobind
dateToDiv(date) {
const date8 = toDate8(date);
return <Link className="daySelect" activeClassName="active" key={date8} to={`/date/${date8}`}>{toNiceDate(date)}</Link>
}
render () {
return <div>{this.props.calendar.map(this.dateToDiv)}</div>
}
And this is more or less how my routes look like
<Route path='/' component={App}>
<IndexRedirect to={`date/${today8()}`}/>
<Route path='date'>
<IndexRedirect to={`${today8()}`}/>
<Route path=":day" component={DayView}/>
</Route>
</Route>