I have a task to call an API, save the API data into local storage, and retrieve that same data when a page is reloaded.
However, the API gets called on every F5 and the data it contains gets randomized, (because API returns a bunch of random facts). So i have the Mount() and Update(), the data is in local storage, but i can't get it to stay and retrieve from there.
I've tried some if statements inside Mount(), for example to check length, and the like, but none worked. Help, I don't know what I'm doing wrong. This is the code:
import React from "react"
class LogicContainer extends React.Component {
constructor() {
super()
this.state = {
isLoading: true,
allFacts: []
}
)
}
//Call to API, get facts, put them into local storage.
componentDidMount() {
fetch("https://catfact.ninja/facts?limit=1000")
.then(response => response.json())
.then(response => {
const allFactsApi = response.data
this.setState({
isLoading: false,
allFacts: allFactsApi
})
localStorage.setItem("allFacts", JSON.stringify(allFactsApi))
})
console.log(this.state.allFacts.length)
}
/*check if facts have changed on reload and save them in tempFacts*/
componentDidUpdate(prevProps, prevState) {
if(prevState.allFacts !== this.state.allFacts) {
const tempFacts = JSON.stringify(this.state.allFacts)
localStorage.setItem("allFacts", tempFacts)
}
}
render() {
return(
<div></div>
)
}
}
export default LogicContainer;
You are setting the data into the localStorage properly. But the problem is you are never actually retrieving it. You are just placing it in the state but the state will reset when the lifecycle of your component comes to an end (when F5 comes along for example). This is the code for retrieving data in localStorage:
localStorage.getItem("allFacts")
Bare in mind: When a user refreshes the website, the "componentDidUpdate" is not going to run, given that the page is actually reloading from scratch. What you should actually be doing is adding the snippet of code mentioned above on your "componentDidMount()" to check wether previous data exists in localStorage or if it should be retrieved from the API. If the data exists, then set the state to that. Otherwise, fetch the data from the API as you are doing now:
componentDidMount() {
// Check if localStorage has an assigned value for item "allFacts"
if(localStorage.getItem("allFacts")!== null){
// Set the state to that
this.setState({
isLoading: false,
allFacts: localStorage.getItem("allFacts")
})
// Otherwise, retrieve it from the API and set the state as well as the localStorage
}else{
fetch("https://catfact.ninja/facts?limit=1000")
.then(response => response.json())
.then(response => {
const allFactsApi = response.data
this.setState({
isLoading: false,
allFacts: allFactsApi
})
localStorage.setItem("allFacts", JSON.stringify(allFactsApi))
})
}
console.log(this.state.allFacts.length)
}
Related
I'm building a multi-step form in React and one of my objectives is to save the user's input if they haven't finished filling in the form. I have saved the user's input in the browser's localStorage by using setItem().
The input fields set the local state which in turn get saved to the localStorage.
However, when the page is refreshed, I want to retrieve the data from localStorage and set the state variables from there so that it pre-fills the input fields with the saved data (if that makes sense)
I'm using setState() in componentDidMount() to do this, although I think that's creating an anti-pattern and I'm not entirely sure what that is. It works fine when I use UNSAFE_componentWillMount but I don't want to use a deprecated lifecycle method.
This is my code :
componentDidMount() {
this.formData = JSON.parse(localStorage.getItem('form'));
this.setState({
type: this.formData.type,
subtype: this.formData.subtype,
brand: this.formData.brand
})
}
the idea to use componentDidMount is correct. There is another anti-pattern.
Don't use this.formData = ... outside of component's constructor - https://reactjs.org/docs/react-component.html
Whole working example would look like this. I added callback after setState to show that loading & saving to localStorage actually works.
export default class Hello extends React.Component {
state = {
type: undefined,
subtype: undefined,
brand: 0,
}
componentDidMount() {
const formData = JSON.parse(localStorage.getItem('form')) ?? {};
if (formData) {
formData.brand += 5
this.setState({
type: formData.type,
subtype: formData.subtype,
brand: formData.brand,
}, () => {
console.log('newState', this.state)
localStorage.setItem('form', JSON.stringify(this.state))
})
}
}
render() {
return <h1>Hello {this.state.brand} </h1>
}
}
you can use constructor function if you do not want to retrieve local storage data in componentDidMount()
constructor(){
const formData = JSON.parse(localStorage.getItem('form'));
const { type, subtype, brand } = formdata;
this.setState({ type, subtype, brand });
}
Though I'd suggest to go with didMount.
componentDidMount() {
const formData = JSON.parse(localStorage.getItem('form'));
const { type, subtype, brand } = formdata;
this.setState({ type, subtype, brand });
}
I have the following code for a main view and user login:
export class MainView extends React.Component {
constructor(){
super();
this.state = {
array: [],
user: null,
userData: {},
};
}
setUserData(user) {
if (user) {
this.setState({ userData: user.user });
console.log(user.user);
console.log (userData); /* errors in console */
} else {
console.log('userData not set');
}
}
onLoggedIn(authData) {
this.setState({ user: authData.user.Username });
/* setting localstorage key:item */
// console.log(authData);
localStorage.setItem('token', authData.token);
localStorage.setItem('user', authData.user.Username);
this.getArrayObjects(authData.token);
this.setUserData(authData);
}
using Passport to get the auth data and token. I don't get why it will log user.user in the setUserData function but will log an "undefined" error for the userData variable in the same function. Any ideas?
You are trying to access an undefined variable.
userData is present inside the state.
So you should access it with this.state.userData.
But even it you write console.log(userData);, it will print {} because in React, setState is asynchronous. Meaning, the state is not updated immediately.
If you want to check whether state has been update or not, check it like this.
this.setState({ userData: user.user }, () => {
console.log(this.state.userData);
});
The, second parameter to setState is a function that is called once state is successfully updated. So, inside there you can see the value in console.
You can't access the state directly as a var. You need to access the state, and the the properties:
console.log(state.userData);
Also, in your code, when you print state.userData, probably you will see the old value, since the setState is an async function.
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.
I am creating a blog application in rest framework and reactjs. On the home page, under componentDidMount, I send an API call using axios to get all the articles and setState of articles to the return. As I have studied, axios works on the idea of promise such that the code doesnt proceed, if the API is not fetched for a particular component. Please tell me, if I am wrong.
Then, I send a GET call to get the writer's name, who wrote the article by the id. Though, I assumed that the axios works as a promise. But, it doesnt work that way. Now, I am not sure how to move ahead.
Here is a snippet. So, in mainBody.js, I make the api call as:
class MainBody extends Component {
state = {};
componentDidMount () {
this.get_all_articles();
};
get_writer_name (id) {
let authstr = 'Bearer ' + window.localStorage.token;
let writer_url = "http://localhost:8000/api/writer/" + id.toString() + "/";
axios.get(writer_url, { headers: { Authorization: authstr }})
.then(response => {
console.log(response.data['name'])
return response.data['name'];
})
.catch(err => {
console.log("Got error")
})
};
get_all_articles () {
let authstr = 'Bearer ' + window.localStorage.token;
axios.get("http://localhost:8000/api/articles/", { headers: { Authorization: authstr }})
.then(response => {
this.setState({articles: response.data});
})
.catch(err => {
console.log("Got error")
})
}
render () {
return (
{this.state.articles.map((article, key) =>
<ArticleView key={article.id} article={article} writer_name={this.get_writer_name(article.created_by)} />
)}
)
}
}
In articleview2, I print all the data that is present in each of the articles along with the writer's name.
My articleview class is:
class ArticleView extends Component {
state = {article: this.props.article};
componentDidMount() {
console.log(this.props.writer_name;
}
render () {
return (
<React.Fragment>
<h2>{article.title}</h2>
<p>{article.body}</p>
<span>{this.props.writer_name}</span>
</React.Fragment>
)
}
}
If you see closely, I wrote two console.log statements to get the writer names. Based on the order, first the console log present in articleview class runs, which is undefined, and thenafter the data is fetched from the API call and the console log runs which returns the correct writer name.
I wanted to know, where is the error? Also, as I noticed, there are too many API calls being made to get the writer's name multiple time for all the listed articles. What are the industry best practices for these cases?
I want to know where is the error.
When you are writing this.state.articles.map(), means you're using property map of the Array articles which may be undefined before the data is fetched that will cause you the error Cannot read property map of undefined.
Solution
Now, as the API request is asynchronous, means render method will not wait for the data to come. So what you can do is use a loader variable in the state, and set it to true as long as the request is being made, and when the response has come, make it false, and show the loader in render when this.state.loader is true, and show articles when it is false.
Or you can initialize this.state.articles with an empty array that won't cause you the error.
Also, as I noticed, there are too many API calls being made to get the writer's name multiple time for all the listed articles. What are the industry best practices for these cases?
It is extremely bad practice to make an API request in the loop. Even myself has been scolded on it once I did it in my company.
Solution
You have tell your backend engineer to provide you filter for including the writer's name in each object of the article. We use Loopback on our backend, which provides a filter for including the related model in each object internally.
Since your API calls have a lot of things in common, you should first set up an axios instance that re-uses those common features:
const api = axios.create({
baseURL: 'http://localhost:8000/api/',
headers: { Authorization: `Bearer ${localStorage.token}` }
});
Now since your MainBody needs to fetch the resources from the API asynchronously, there will be a short period where the data is not yet available. There are two ways you can handle this. Either the MainBody can be responsible for making all the calls, or it can be responsible for just making the call to get all the articles, then each of the ArticleView components can be responsible for getting the writer's name. I'll demonstrate the first approach below:
class MainBody extends Component {
state = { articles: null, error: null, isLoading: true };
async componentDidMount () {
try {
const response = await api.get('articles/');
const articles = await Promise.all(
response.data.map(async article => {
const response = await api.get(`writer/${article.created_by}/`);
return { ...article, writer_name: response.data.name };
})
);
this.setState({ articles, isLoading: false });
} catch (error) {
this.setState({ error, isLoading: false });
}
}
render () {
const { articles, error, isLoading } = this.state;
return isLoading ? 'Loading...' : error
? `Error ${error.message}`
: articles.map(article => (
<ArticleView
key={article.id}
article={article}
writer_name={article.writer_name}
/>
)
);
}
}
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.