How to call a function right before leaving a component? - reactjs

I am kind of new to react and I am developing a Frontend for a REST-API.
My Frontend for a REST-API is organized in 5 Sites(Components) which are routed with react-router-dom.
Every time I enter a Site, the ComponentDidLoad dispatches an action, which in turn calls the API in my reducer.
export function getData(pURL, ...pParams){
return (dispatch) => {
axios.get(pURL, {pParams})
.then(result => {
dispatch(getDataSuccess(result.data))
})
.catch(error => {
dispatch(getDataFailure(error))
})
}
}
One of my Main sites looks as follows
class Overview extends Component {
componentDidMount(){
this.props.getData(MyURLHere);
}
render(){
return(
<div>
{this.props.hasError ? "Error while pulling data from server " : ""}
{/* isLoading comes from Store. true = API-Call in Progress */}
{this.props.isLoading ? "Loading Data from server" : this.buildTable()}
</div>
)
}
}
let mapStateToProps = state => {
hasError: state.api.hasError,
isLoading: state.api.isLoading,
companies: state.api.fetched
}
let mapDispatchToProps = {
//getData()
}
export default connect(mapStateToProps, mapDispatchToProps)(Overview);
state.api.fetched is my store-variable I store data from every API-Call in.
this.buildTable() just maps over the data and creates a simple HTML-Table
My Problem is that I got the store-state variable isLoading set to truein my initialState.
But when I move to another site and then back to this one, it automatically grabs data from state.api.fetched and tries to this.buildTable() with it, because isLoading is not true. Which ends in a "is not a function" error, because there is still old, fetched data from another site in it.
My Solution would be to always call a function when leaving a component(site) that resets my store to it's initialState
const initialStateAPI = {
isLoading: true
}
or directly set isLoading to true, in Order to avoid my site trying to use data from old fetches.
Is there a way to do so?
I hope that I provided enough information, if not please let me know.

If you want to call the function when leaving a component you can use componentWillUnmount function. Read more about React Lifecycle methods.

Related

Initial State of Redux Store with JSON Data?

I am working on React app where the state is managed by redux. I am using actions.js file to fetch JSON data and store it directly in the store. The initial Store has just one key (data) in its obj with null as its value.
I use componentDidMount() Lifecycle to call the function which updates the store's data key with the JSON data I receive. However, whenever I load my app it gives an error because it finds the data value as null.
I get it. componentDidMount() executes after the app is loaded and the error doesn't let it execute. I tried using componentWillMount() but it also gives the same error. ( Which I use in JSX )
When I try to chanage the data's value from null to an empty obj it works for some level but after I use it's nested objects and arrays. I get error.
I wanna know what is the way around it. What should I set the vaue of inital State or should you use anyother lifecycle.
If your primary App component can't function properly unless the state has been loaded then I suggest moving the initialization logic up a level such that you only render your current component after the redux state has already been populated.
class version
class LoaderComponent extends React.Component {
componentDidMount() {
if ( ! this.props.isLoaded ) {
this.props.loadState();
}
}
render() {
if ( this.props.isLoaded ) {
return <YourCurrentComponent />;
} else {
return <Loading/>
}
}
}
export default connect(
state => ({
isLoaded: state.data === null,
}),
{loadState}
)(LoaderComponent);
Try something like this. The mapStateToProps subscribes to the store to see when the state is loaded and provides that info as an isLoaded prop. The loadState in mapDispatchToProps is whatever action creator your current componentDidMount is calling.
hooks version
export const LoaderComponent = () => {
const dispatch = useDispatch();
const isLoaded = useSelector(state => state.data === null);
useEffect(() => {
if (!isLoaded) {
dispatch(loadState());
}
}, [dispatch, isLoaded]);
if (isLoaded) {
return <YourCurrentComponent />;
} else {
return <Loading />
}
}
And of course you would remove the fetching actions from the componentDidMount of the current component.

React Redux - How to do a proper loading screen using React and Redux on url change

I am working on a web app that uses React + Redux. However, I am struggling with the loading screens. Basically, my initial state for the redux store is this.
const initialState = {
project: {},
projects: [],
customers: [],
vendors: [],
customer: {},
vendor: {},
loading: false
};
An example of one of my reducers is this
const fetchSalesProject = (state, action) => {
return updateObject(state, {
project: action.payload,
loading: false
});
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_SALES_PROJECT: return fetchSalesProject(state, action);
default:
return state;
where updateObject is this
export const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
}
}
here are my actions (axiosInstance is just a custom addon to axios to include the necessary headers for authentication, but works exactly the same like a normal axios api call)
export const fetchSalesProject = (id) => (dispatch) => {
axiosInstance
.get(`/sales-project/detail/${id}/`)
.then((res) => {
dispatch({
type: FETCH_SALES_PROJECT,
payload: res.data,
});
})
.catch((err) => dispatch(returnErrors(err.response.data, err.response.status)));
};
export const showLoader = (area) => (dispatch) => {
dispatch({
type: SHOW_LOADER,
area: area ? area : true
});
};
where the param area is just in the case where loading is meant to be rendered only on a specific area on the page (not relevant for this question hence when showLoader is called, no params would be passed in and hence the area would be set to true for this question)
this is my component
componentDidMount() {
this.props.showLoader()
this.props.fetchSalesProject(this.props.match.params.id)
};
this is my mapStateToProps
const mapStateToProps = state => ({
project: state.api.project,
loading: state.api.loading
})
this is my conditional rendering
render() {
return (
!this.props.loading ?
{... whatever content within my page }
: { ...loading screen }
Basically, the issue I am facing is that, when i first load this component, there would be an error, as my this.props.project would be an empty {} (due to initialState). The reason why is that before the showLoader action is dispatched fully by componentDidMount, the component renders, and hence since this.props.loading is still set to the initialState of false, the conditional rendering passes through and the this.props.project would be rendered (however since it is empty, the various keys that I try to access within the rendering would be undefined hence throwing an error).
I tried various methods to try to overcome this, but none seem like an optimum solution. For example, I have tried to set the initialState of loading to an arbitary number, like eg. 0, and only render the page when this.props.loading === false, which is only after the data is fetched. However, the issue arises when I go onto a new page, like for example, another component called CustomerDetail. When that component is mounted, this.props.loading would be false and not 0, as it was previously manipulated by the previous component that I was on (SalesProjectDetail). Hence, that same error would come up as the conditional rendering passes through before any data was actually fetched.
Another method I tried was to use component states to handle the conditional rendering. For example, I only set a component state of done : true only after all the dispatches are completed. However, I felt like it was not good practice to mix component states and a state management system like Redux, and hence was hoping to see if there are any solutions to my problem that uses Redux only.
I am looking for a solution to my problem, which is to only render the contents only after the data has been fetched, and before it is fetched, a loading screen should be rendered. All help is appreciated, and I am new to React and especially Redux, so do guide me on the correct path if I am misguided on how this problem should be approached. Thanks all in advance!
there can be more ways to do it. But to go ahead with your scheme of local state...
I think there is no problem if you use local state to keep track of load complete or underway.
You can have a local state variable as you already said.
Before dispatch set that variable to loading=true then use async/await to fetch data and afterwards set loading back to false.
I think it will work for you.

React-Router/Redux browser back button functionality

I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()

React: Component wont reload - even though new state has been set

I have the following problem: I've written a 'detailview' component, that takes a PK from a list and uses it to fetch the right object from the API. As this takes some time it will display 'Loading' by default.
My render looks like this:
render() {
if (!this.store_item) {
return <h2>loading</h2>
}
return(
<div>
<h2>{this.state.store_item.results.title}</h2>
<p>{this.state.store_item.results.description}</p>
</div>
)
}
The 'store_item' is loaded when the component mounts:
componentDidMount() {
this.LoadApi();
}
The 'LoadApi' method has the following code:
LoadApi() {
const new_url = encodeURI(this.state.store_item_api + this.props.detail_pk);
fetch(new_url, {
credentials: 'include',
})
.then(res => res.json())
.then(result =>
this.setState({
store_item: result,
}));
}
However, when I run this code in my browser all I see is 'Loading'. Yet when I check the state in the chromium react extension I can see that 'store_item' has been set (I also verified that the api call went as planned in the network tab). As the state has been set (and is being set in 'LoadApi') my understanding was that this should trigger an re-render.
Does anyone know why, despite all this, the component keeps returning 'loading'?
You have typo, missed state keyword
if (!this.store_item) { ... }
^^^^^^^^^^^^^^^^^^
Should be
if (!this.state.store_item) { ... }
Because it's defined as component state instead of component static property.

How is changed state supposed to be re rendered exactly in redux relative to flux?

Say my homepage renders a feed of all posts when no user is signed in, else renders a feed of posts relevant to signed in user. With Flux I would simply have a function
getNewFeedPosts(){
this.setState({posts: []});
ajaxFetchPostsFromServer();
}
And add it as a listener to my session store. I would also have a function listening to my post store that would set the state to the new posts once they came back in response to ajaxFetchPostsFromServer being called.
What is the best practice way to do this in Redux with connectors and a provider?
Currently I have a feed container:
const mapStateToProps = (state) => ({
tweets: state.tweets,
});
const mapDispatchToProps = (dispatch) => ({
getTweets: (currUserId) => {
return dispatch(fetchAllTweets(currUserId));
}
});
const FeedContainer = connect(mapStateToProps, mapDispatchToProps)(Feed);
A feed:
class Feed extends React.Component {
constructor(props) {
super(props);
}
componentWillMount(){
this.props.getTweets();
}
render(){
const tweets = this.props.tweets;
const originalTweetIds = Object.keys(tweets);
return (
<div>
<ul>
{ originalTweetIds.map((originalTweetId) => {
return (
<li key={originalTweetId}>
{post info goes here}
</li>);
})}
</ul>
</div>
)
}
};
1) Right now I'm calling getTweets when Feed's about to mount, but if I change my session status in the other component the feed doesn't change, I suppose because it is not remounting, so how should I be doing this to make it change.
2) Also in actuality in getTweets, before I make the ajax request I want to set tweets in my store to [] to prevent tweets from the previous page (say before I logged in) from remaining on the page until the response with the proper tweets comes back. What is best practice to accomplish this?
All input is greatly appreciated :)
1) Right now I'm calling getTweets when Feed's about to mount, but if I change my session status in the other component the feed doesn't change, I suppose because it is not remounting, so how should I be doing this to make it change.
You need to call dispatch(fetchAllTweets(currUserId)) from the other component when you "change the session status". It will update your Feed component as soon as state.tweets is updated.
2) Also in actuality in getTweets, before I make the ajax request I want to set tweets in my store to [] to prevent tweets from the previous page (say before I logged in) from remaining on the page until the response with the proper tweets comes back. What is best practice to accomplish this?
You can change your mapDispatchToProps to:
const mapDispatchToProps = (dispatch) => ({
getTweets: (currUserId) => {
dispatch(clearTweets());
return dispatch(fetchAllTweets(currUserId));
}
});
Create an action creator:
const clearTweets = () => ({ type: 'CLEAR_TWEETS' });
And in the reducer that updates the tweets state you can do something like this:
switch (action.type) {
case 'CLEAR_TWEETS':
return [];
// other actions
}

Resources