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

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.

Related

React, Typescript - Print fetched data

I am trying to print data from fetched JSON but somehow i am unable to do it.
interface IFetched {
fetchedData: any;
error: any;
}
export default class Testing100 extends React.Component<
ITesting100Props,
IFetched,
{}
> {
constructor(props: ITesting100Props) {
super(props);
this.state = {
error: null,
fetchedData: [],
};
}
public componentDidMount() {
fetch("https://api.randomuser.me/")
.then((res) => res.json())
.then(
(result) => {
this.setState({
fetchedData: result,
});
},
(error) => {
this.setState({
error,
});
}
);
}
public render(): React.ReactElement<ITesting100Props> {
console.log(this.state.fetchedData.results);
return (
<div>
<a>
{this.state.fetchedData.map(function (fetchedDataX) {
return fetchedDataX.results[0].name.first;
})}
</a>
</div>
);
}
}
With console log i am able to print data. But when i change console log from console.log(this.state.fetchedData.results); to console.log(this.state.fetchedData.results[0]); i get nothing. And even that console log is called twice as you can see in console output i dont know why.
But my goal is to print the first name of person into <a> element and I just don't know how. Hope somebody can help me with this. Thanks for your time.
Think about the state of the app before the fetch occurs - the fetchedData array is empty. Then when you fetch, you are converting it into an object, with a results field that is an array.
Your code needs to be able to handle both of these states. Don't try to use or log a field of something that you haven't first verified actually exists, or it will crash.
First step is to clean it up so you directly just update the array in the state -
Your map is not working because fetchedData has an inner results field - try this.setState({fetchedData: result.results});, and then console.log(this.state.fetchedData).
Also you might want to add some guards to the top of your render so that things don't crash when the fetchedData is empty or errored:
if (this.state.fetchedData === []) return <p>"Nothing Loaded"</p>;
if (this.state.error !== null) return <p>{this.state.error.message}</p>;
As for the double output to the console, that is because the render method get run first when the component is mounted, and you see the output where the fetchedData is empty, and then componentDidMount runs (which fetches the data and updates the state) and then React re-renders with the new state, causing the second console log.
Your console log that tries to access the .result[0] fails because it doesn't exist for this first pre-fetch state. Check (with an if) it is there before logging, or log the whole state obj.

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 : How to make the SAME component re-render?

I've found myself at a bit of dead end here. I'll try to explain it as best as I can.
I'm using base username routing on my app, meaning that website.com/username will route to username's profile page. Everything works fine apart from one issue that I'll get to below.
This is how I'm doing it (very short version) :
This route looks for a potential username and renders the Distribution component.
<Route exact path="/([0-9a-z_]+)" component={Distribution} />
Distribution component then extracts the potential username from the pathname...
username = {
username: this.props.location.pathname.substring(1)
};
...And then fires that off to my API which checks to see if that username actually exists and belongs to a valid user. If it does it returns a user object, if not it returns an error.
if (username) {
axios
.post(API_URI + '/users/get/profile', JSON.stringify(username))
.then(res => {
this.setState({
ready: true,
data: res.data,
error: ''
});
})
.catch(err => {
this.setState({
ready: true,
data: '',
error: err.response
});
});
}
All of the above is happening inside componentWillMount.
I then pass the relevant state info as props to the relevant child component in the render :
render() {
if (this.state.ready) {
if (this.state.data) {
return <UserProfile profile={this.state.data} />;
}
if (this.state.error) {
return <Redirect to="notfound" />;
}
}
return null;
}
As I mentioned, this all works perfectly when moving between all of the other routes / components, but it fails when the Distribution component is called while already IN the Distribution component. For example, if you are already looking at a valid profile (which is at Distribution > UserProfile), and then try to view another profile, (or any other malformed username route that would throw an error), the API call isn't getting fired again so the state isn't being updated in the Distribution component.
I originally had it all set up with a Redux store but had the exact same problem. I wrongly thought that componentDidMount would be fired every single time the component is called for the first time, and I assumed that throwing a new url at it would cause that but it doesn't.
I've tried a bunch of different ways to make this work (componentWillReceiveProps etc) but I just can't figure it out. Everything I try throws depth errors.
Am I missing a magical piece of the puzzle here or just not seeing something really obvious?
Am I going about this entirely the wrong way?
You on the right path when you tried to use componentWillReceiveProps. I would do something like the following:
componentDidMount() {
this.refresh()
}
componentWillReceiveProps(prevProps) {
if(prevProps.location.pathname !== this.props.location.pathname) {
this.refresh(prevProps)
}
}
refresh = (props) => {
props = props || this.props
// get username from props
// ...
if (username) {
// fetch result from remote
}
}

How to call a function right before leaving a component?

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.

React Parent Component not re-rendering

I have a parent component that renders a list of children pulled in from an API (which functions correctly). Each child has an option to delete itself. When a child deletes itself, I cannot get the parent to re-render. I have read about 50 answers on here related to this topic and tried all of them and nothing seems to be working. I am missing something and stuck.
The component has redux wired in to it, but I have tried the code with and without redux wired up. I have also tried this.forceUpdate() in the callback, which also does not work (I've commented it out in the example code below).
class Parent extends React.Component {
constructor(props) {
super(props)
this.refresh = this.refresh.bind(this)
this.state = {
refresh: false,
}
}
componentDidMount(){
this.getChildren()
}
refresh = () => {
console.log("State: ", this.state)
this.setState({ refresh: !this.state.refresh })
// this.forceUpdate();
console.log("new state: ", this.state)
}
getChildren = () => {
axios.get(
config.api_url + `/api/children?page=${this.state.page}`,
{headers: {token: ls('token')}
}).then(resp => {
this.setState({
children: this.state.children.concat(resp.data.children)
)}
})
}
render(){
return (
<div>
{_.map(this.state.children, (chidlren,i) =>
<Children
key={i}
children={children}
refresh={() => this.refresh()}
/>
)}
</div>
)
}
}
And then in my Children component, which works perfectly fine, and the delete button successfully deletes the record from the database, I have the following excerpts:
deleteChild = (e) => {
e.preventDefault();
axios.delete(
config.api_url + `/api/children/${this.state.child.id}`,
{headers: {token: ls('token')}}
).then(resp => {
console.log("The response is: ", resp);
})
this.props.refresh();
}
render() {
return(
<button class="btn" onClick={this.deleteChild}>Delete</button>
)}
}
I am sure I am missing something simple or basic, but I can't find it.
Your parent render method depends only on this.state.children which is not changing in your delete event. Either pass in the child id to your this.props.refresh method like this.props.refresh(this.state.child.id) and update this.state.children inside the refresh method or call the get children method again once a delete happens
Code for delete method in child
this.props.refresh(this.state.child.id)
Code for parent refresh method
refresh = (childIdToBeDeleted) => {
console.log("State: ", this.state)
this.setState({ refresh: !this.state.refresh })
// this.forceUpdate();
console.log("new state: ", this.state)
//New code
this.setState({children: this.state.children.filter(child => child.id !== childIdToBeDeleted);
}
Few notes about the code. First removing from db and then reloading might be slow and not the best solution. Maybe consider adding remove() function which can be passed to the children component to update state more quickly.
Second if you want to call setState that depends on previous state it is better to use the callback method like this (but i think you need something else see below)
this.setState((prevState,prevProps) =>
{children: prevState.children.concat(resp.data.children)})
Lastly and what i think the issue is. You are not actually calling getChildren from refresh method so the state is not updated and if you want gonna reload the whole state from db you shouldn't concat but just set it like this
.then(resp => {
this.setState({children: resp.data.children})
}
Hope it helps.
Edit:
As mentioned in the comments the call to refresh from children should be in promise then

Resources