Persist data with localStorage - React.js - javascript

I'm confused about how to use localStorage to persist the data that's coming from calling the API.
I want whenever I refresh the page, the callApi inside useEffect to not render new data and keep the existing data unchanged.
Any help would be appreciated.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Layout, Loading, OverviewHeader, OverviewSubHeader, SiteCard } from '../components';
const Overview = () => {
const [loading, setLoading] = useState(true);
const [sites, setSites] = useState([]);
useEffect(() => {
async function callApi() {
const response = await axios.get(`https://randomuser.me/api/?results=3`);
const sites = response?.data?.results;
console.log('sites', sites);
setSites(sites);
await localStorage.setItem('sites', JSON.stringify(sites));
setLoading(false);
}
callApi();
}, []);
return (
<div>
<Layout>
<OverviewHeader />
<OverviewSubHeader />
<div className='overview-page-wrapper'>
{loading ? (
<Loading />
) : (
sites.map(site => {
return (
<React.Fragment>
<SiteCard
key={site.login.uuid}
siteId={site.login.uuid}
image={site.picture.large}
firstName={site.name.first}
lastName={site.name.last}
city={site.location.city}
country={site.location.country}
sensors={site.dob.age}
notifications={site.registered.age}
latitude={site.location.coordinates.latitude}
longitude={site.location.coordinates.longitude}
{...site}
/>
</React.Fragment>
);
})
)}
</div>
</Layout>
</div>
);
};
export default Overview;

I'm not too sure what you're trying to accomplish, seeing as you'd likely want to refresh that data at some point.
Maybe you could indicate what behaviour/scenario you're trying to cater for?
In any case, to answer your question, what you could do is smth like:
const [displayedSites, setDisplayedSites] = useState([])
// this does both setting the state for your UI
// and stores to localStorage
const setAndSaveDisplayedSites = (fetchedSites) => {
setDisplayedSites(sites)
localStorage.setItem('sites', JSON.stringify(sites))
}
useEffect(() => {
(async function () {
const localSites = localStorage.getItem(sites);
if (!localSites) {
// this will only ever fetch if it is your first time mounting this component
// I suppose you would need to call setAndSaveDisplayedSites
// from a "refresh" button
const fetchedSites = await getSitesFromAPI()
setAndSaveDisplayedSites(fetchedSites)
return
}
const parsedLocalSites = JSON.parse(localSites)
setDisplayedSites(parsedLocalSites)
})()
}, [])
also checkout this hook that takes care of some things for you: https://usehooks.com/useLocalStorage/

Use the useContext hook for this purpose OR if you really just want to use the local storage anyhow, then use it but manage different states/variables for that.
Your current state (that you want to render on the screen)
Your fetched data (the one that you want to keep)
Hope this makes sense. Thankyou!

Related

After refreshing, React props turn undefined

I'm fetching from an API where I want to pass the response object as a prop onto a child component from App.js and use it in my Tags.js file. However, it only works after one time and when I refresh it, it gives me an error saying the props.response.names.length is undefined. I tried using the useEffect function to try and update it but it didn't work. I would appreciate any help.
My App.js file (still some remnants of when you run "npx create-react-app my-app"):
import './App.css';
import Tags from './Tags.js';
import React, { useState, useEffect } from 'react';
function App() {
const makeRequest = async () => {
try {
let response = await fetch('RANDOM_API_URL');
let json = await response.json();
setResponse(json);
} catch (error) {
console.log(error);
}
}
const [response, setResponse] = useState(makeRequest);
useEffect(() => {
setResponse(makeRequest);
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<Tags response={response}></Tags>
</div>
);
}
export default App;
My Tags.js:
import './App.js';
function Tags(props) {
const makeButtons = () => {
let result = [];
for (let i = 0; i < props.response.names.length; i++) {
result.push(<button key={i}>hello</button>);
}
return result;
}
return (
<div>
{makeButtons()}
</div>
);
}
export default Tags;
Your makeRequest function is async, and sets state internally. The reason you get this bug is that you don't always have a response.names to read length from - you only have an empty response object.
Either make sure you always have the names array available in state, or avoid rendering your Tags component when names is not present in state.
Also, try to avoid being creative with your dependency array, it's there for a reason. I see why you didn't include makeRequest in it though, since you create a new function on every render. That's something to keep in mind when you transition from class components to functional components. Class methods are stable across renders, functions declared in a functional component are not. To mimic a class method, you can declare functions using the useCallback hook, but again you need to include the dependency array. In your case, you can just create the async function inside useEffect, and then call it.
const [response, setResponse] = useState({names: []});
// or const [response, setResponse] = useState();
// and then, in Tags:
// function Tags({response = {names: []}) {
useEffect(() => {
const makeRequest = async () => {
try {
let response = await fetch('RANDOM_API_URL');
let json = await response.json();
setResponse(json);
} catch (error) {
console.log(error);
}
}
makeRequest();
}, []);
Supplying a default response prop to Tags will make sure you can read the length of names even before you have a response.
Looking more closely on your Tags component, I think it should be something like this:
// don't import App.js here
export default function Tags({response = {names: []}){
return (
<div>
{response.names.map(name => {
return <button key={name}>Hello {name}</button>
})}
</div>
)
}
Don't use index as key, that will cause problems if you rearrange the names in your array. And I guess you want to supply an onClick function to your buttons.

using a JSX react variable after completion of a Promise

Here is the code:
import React from 'react';
import { Link } from 'react-router-dom';
import Main from '../layouts/Main';
import Cell from '../components/Stats/nCell';
import init from '../data/stats';
const promiseHandle = async () => {
Window.data = await init();
};
promiseHandle();
const Stats = () => (
<Main
title="Stats"
description="Some statistics about Ahammad Shawki and ahammadshawki8.github.io"
>
<article className="post" id="stats">
<header>
<div className="title">
<h2 data-testid="heading"><Link to="/stats">Publications</Link></h2>
</div>
</header>
<p className="rec-p"> Please accept my sincere apologies for the inconvenience.
At the moment, I am working on my Publication API for this website
so may not be able to find all of my articles here.
But the great news is that you can always read all of my articles on HashNode.
</p>
</article>
{Window.data.map((post) => (
<Cell
data={post}
key={post.title}
/>
))}
</Main>
);
export default Stats;
Since I am working with a promise, here Window.data has been declared async way. But in the react JSX {}, the code is sync. So, it runs before the completion of the promise which causes the bug. I want to run it after the promise. How to solve this? Thanks in advance.
You need to either move the call inside the component and use useEffect and useState or do a poll inside the component.
const Stats = () => {
const [data, setData] = useState([])
useEffect( () => {
const promiseHandle = async () => {
setData(await init());
};
promiseHandle();
}, [])

Why does my React app not re-render or display changes on the DOM except I relaod?

import { useEffect, useState } from "react";
function Popular() {
const [popular, setPopular] = useState([]);
useEffect(() => {
getPopular();
}, []);
const getPopular = async () => {
const api = await fetch(
`https://api.spoonacular.com/recipes/random?apiKey=${process.env.REACT_APP_RECIPE_API_KEY}&number=9`
);
const data = await api.json();
setPopular(data.recipes);
};
return (
<div>
{popular.map((recipe) => {
return (
<div>
<p>{recipe.title}</p>
</div>
);
})}
</div>
);
}
export default Popular;
I am pretty new to React, and I encountered this issue which I have been trying to fix to no avail. The code is a component that is to return a list of recipe title to my app. I am fetching data from an API in the getPopular() function which is set to the setPopular function variable of the useState() method. But when I save my work and return to the browser, the changes does not display. The list does not display, but if I console.log(data.recipes) it displays on the console.
Before now, if I made any change (maybe a text change) the React app renders it without reloading, but now I have to reload the page before I see the change.
Please how do I fix this issue? So that I can see changes without having to reload the page manually.
Not saying that this is the problem, but getPopular() should not be called after its declaration? By this I mean:
const getPopular = async () => {
const api = await fetch(
/...
};
useEffect(() => {
getPopular();
}, []);
Another thing that bugs me is, although JS/React is case sensitive, I really think you should avoid having a const called popular, since your functions is Popular.
Please, let me know if the order did matter for your problem. I will review some react classes soon, if i get another inside, i'll let you know.

This code doesn't show a compile error, but the console won't render

import React from 'react';
const url = 'https://randomuser.me/api/?results=10'
async function List() {
const data = await fetch (url)
const response = await data.json()
return (
<div>
{response.map((item)=>(
<div>{item.results[1].name.first}</div>
))}
</div>
)
}
It also throws this: The above error occurred in the component:
at List
at div
at App
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
If you working with a functional component. You need to use React hooks for API calls. You can use useEffect hooks to call the API.
Example:
const { useState } = React;
function useFetchData() {
const [data, setData] = React.useState([]);
React.useEffect(() => {
return fetch("https://randomuser.me/api/?results=10")
.then((response) => response.json())
.then((responseJson) => {
setData(responseJson.results);
})
.catch((error) => {
console.error(error);
});
}, []);
return { data };
}
function App() {
const { data } = useFetchData();
return (
<div id="quote-box">
{data.map((item) => (
<div>{item.name.first}</div>
))}
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Calling an API inside a component is a side effect and React doesn't want that. React components expect you to provide the data and render that data immediately. If you want to call an API before rendering the component, use a side effect method like useEffect() and call that API inside the lambda. The component now knows that there is an expected side effect and will re-render once the data has updated.

How to prevent useCallback from triggering when using with useEffect (and comply with eslint-plugin-react-hooks)?

I have a use-case where a page have to call the same fetch function on first render and on button click.
The code is similar to the below (ref: https://stackblitz.com/edit/stackoverflow-question-bink-62951987?file=index.tsx):
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { fetchBackend } from './fetchBackend';
const App: FunctionComponent = () => {
const [selected, setSelected] = useState<string>('a');
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const [data, setData] = useState<string | undefined>(undefined);
const query = useCallback(async () => {
setLoading(true)
try {
const res = await fetchBackend(selected);
setData(res);
setError(false);
} catch (e) {
setError(true);
} finally {
setLoading(false);
}
}, [])
useEffect(() => {
query();
}, [query])
return (
<div>
<select onChange={e => setSelected(e.target.value)} value={selected}>
<option value="a">a</option>
<option value="b">b</option>
</select>
<div>
<button onClick={query}>Query</button>
</div>
<br />
{loading ? <div>Loading</div> : <div>{data}</div>}
{error && <div>Error</div>}
</div>
)
}
export default App;
The problem for me is the fetch function always triggers on any input changed because eslint-plugin-react-hooks forces me to declare all dependencies (ex: selected state) in the useCallback hook. And I have to use useCallback in order to use it with useEffect.
I am aware that I can put the function outside of the component and passes all the arguments (props, setLoading, setError, ..etc.) in order for this to work but I wonder whether it is possible to archive the same effect while keeping the fetch function inside the component and comply to eslint-plugin-react-hooks?
[UPDATED]
For anyone who is interested in viewing the working example. Here is the updated code derived from the accepted answer.
https://stackblitz.com/edit/stackoverflow-question-bink-62951987-vxqtwm?file=index.tsx
Add all of your dependecies to useCallback as usual, but don't make another function in useEffect:
useEffect(query, [])
For async callbacks (like query in your case), you'll need to use the old-styled promise way with .then, .catch and .finally callbacks in order to have a void function passed to useCallback, which is required by useEffect.
Another approach can be found on React's docs, but it's not recommended according to the docs.
After all, inline functions passed to useEffect are re-declared on each re-render anyways. With the first approach, you'll be passing new function only when the deps of query change. The warnings should go away, too. ;)
There are a few models to achieve something where you need to call a fetch function when a component mounts and on a click on a button/other. Here I bring to you another model where you achieve both by using hooks only and without calling the fetch function directly based on a button click. It'll also help you to satisfy eslint rules for hook deps array and be safe about infinite loop easily. Actually, this will leverage the power of effect hook called useEffect and other being useState. But in case you have multiple functions to fetch different data, then you can consider many options, like useReducer approach. Well, look at this project where I tried to achieve something similar to what you wanted.
https://codesandbox.io/s/fetch-data-in-react-hooks-23q1k?file=/src/App.js
Let's talk about the model a bit
export default function App() {
const [data, setDate] = React.useState("");
const [id, setId] = React.useState(1);
const [url, setUrl] = React.useState(
`https://jsonplaceholder.typicode.com/todos/${id}`
);
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
fetch(url)
.then(response => response.json())
.then(json => {
setDate(json);
setIsLoading(false);
});
}, [url]);
return (
<div className="App">
<h1>Fetch data from API in React Hooks</h1>
<input value={id} type="number" onChange={e => setId(e.target.value)} />
<button
onClick={() => {
setIsLoading(true);
setUrl(`https://jsonplaceholder.typicode.com/todos/${id}`);
}}
>
GO & FETCH
</button>
{isLoading ? (
<p>Loading</p>
) : (
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
)}
</div>
);
}
Here I fetched data in first rendering using the initial link, and on each button click instead of calling any method I updated a state that exists in the deps array of effect hook, useEffect, so that useEffect runs again.
I think you can achieve the desired behavior easily as
useEffect(() => {
query();
}, [data]) // Only re-run the effect if data changes
For details, navigate to the end of this official docs page.

Categories