useState variable is called before useEffect API call - reactjs

From what I understand useEffect hook runs last as a sideEffect. I am attempting to console log data.main.temp. I can understand that it doesn't know what that is yet, because it is fetching the data from the api in the useEffect hook which runs after.
How would I be able to access or console log data.main.temp AFTER the api call? (I feel like setTimout is the cheating way?)
import React, { useState, useEffect } from "react";
import Button from "../UI/Button";
import styles from "./Weather.module.css";
import moment from "moment";
import Card from "../UI/Card";
export default function Weather() {
//State Management//
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
const [data, setData] = useState([]);
//openWeather API key
const key = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
useEffect(() => {
const fetchData = async () => {
//get coordinates//
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
//fetch openWeather api//
await fetch(`https://api.openweathermap.org/data/2.5/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${key}`)
.then((res) => res.json())
.then((result) => {
setData(result);
console.log(result);
});
};
fetchData();
}, [lat, long]);
//Examples of what I want, they run too early before api//
console.log(data.main.temp);
const Farenheit = data.main.temp * 1.8 + 32;
return (
<Card>
{typeof data.main != "undefined" ? (
<div className={`${styles.weatherContainer} ${styles.clouds}`}>
<h2>Weather</h2>
<p>{data.name}</p>
<p>{data.main.temp * 1.8 + 32} °F</p>
<p>{data.weather[0].description}</p>
<hr></hr>
<h2>Date</h2>
<p>{moment().format("dddd")}</p>
<p>{moment().format("LL")}</p>
</div>
) : (
<div></div>
)}
</Card>
);
}

You're right, the effect function is run after the first render which means you need to wait somehow until your api call is done. One common way to do so is to introduce another state flag which indicate whether the data is available or not.
Another thing which does not follow react good practices is the fact that you're effect function does more than one thing.
I also added trivial error handling and cleaned up mixed promises and async await
here is your refactored code
import React, { useState, useEffect } from "react";
import Button from "../UI/Button";
import styles from "./Weather.module.css";
import moment from "moment";
import Card from "../UI/Card";
//openWeather API key
const key = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
export default function Weather() {
//State Management//
const [lat, setLat] = useState();
const [long, setLong] = useState();
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
}, []);
useEffect(() => {
const fetchData = async () => {
if (lat && long && key) {
try {
setLoading(true);
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${key}`
);
const data = await response.json();
setData(data);
setLoading(false);
} catch (err) {
setError(err);
setLoading(false);
}
}
};
fetchData();
}, [lat, long]);
if (error) {
return <div>some error occurred...</div>;
}
return (
<Card>
{loading || !data ? (
<div>loading...</div>
) : (
<div className={`${styles.weatherContainer} ${styles.clouds}`}>
<h2>Weather</h2>
<p>{data.name}</p>
<p>{data.main.temp * 1.8 + 32} °F</p>
<p>{data.weather[0].description}</p>
<hr></hr>
<h2>Date</h2>
<p>{moment().format("dddd")}</p>
<p>{moment().format("LL")}</p>
</div>
)}
</Card>
);
}

You can use another useEffect, which depends on changing the data state
useEfect(() => {
if (data) {
// do something with data
}
}, [data])

You can create a simple function and call it in your API call response and pass in the data directly from the api response, that way you will have access to the data immediately there's a response.
E.g
...
.then((result) => {
setData(result);
getDataValue(result) // this function will be called when the response comes in and you can use the value for anything
console.log(result);
});
METHOD 2:
You can use a useEffect hook to monitor changes in the data state, so that whenever there's an update on that state, you can use the value to do whatever you want. This is my less preferred option.
useEffect(() => {
//this hook will run whenever data changes, the initial value of data will however be what the initial value of the state is
console.log(data) //initial value = [] , next value => response from API
},[data])

Related

Problem with fetching data in React, Assignment to constant variable

I just started to learn React and was trying to fetch some random data. i created a useState and have two values : const [name, setName] = useState([]);
when i try to do name : response.json();
I get an error that assignment to a constant variable, I'm following a tutorial which is doing the same.
surprisingly when I create a constant variable with a different name, it works. I only can't assign the name = await response.json();
Thank you
import React, { useEffect, useState } from "react";
import { ReactDOM } from "react";
const FetchData = () =>{
const [name, setName] = useState([]);
const fetchNames = async () =>{
const url = `https://randomuser.me/api`;
try {
const response = await fetch(url);
name = await response.json();
console.log(name);
setName(name);
} catch (error) {
console.log(error);
}
}
useEffect(()=>{
fetchNames();
},[])
return(
<div>
</div>
);
}
export default FetchData;
Check my example, as I understand you want to use name inside the try as variable not from state so you should add const. I also mapped the response in your render which you can see the results. Here also codesandbox example https://codesandbox.io/embed/friendly-ishizaka-57i66u?fontsize=14&hidenavigation=1&theme=dark
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [name, setName] = useState([]);
const fetchNames = async () => {
const url = `https://randomuser.me/api`;
try {
const response = await fetch(url);
constname = await response.json();
console.log(name);
setName(name?.results);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
fetchNames();
}, []);
return (
<div>
{name.map((el) => (
<>
<p>{el.email}</p>
<p>{el.phone}</p>
</>
))}
</div>
);
}
there are 2 things that might help with this confusion
any variable created using const will forever hold the value it had when it was declared, so for eg if we have const x = "😀", we later can't do something like x = "something else"
in the snippet you shared name is a "state variable". state variables should NEVER be updated directly, state updates must happen through setState function only(i.e. setName in this case)
this is the part in official docs that talks about it https://reactjs.org/docs/state-and-lifecycle.html#do-not-modify-state-directly , https://reactjs.org/docs/hooks-state.html#updating-state

Render page on react after loading json

I have load() function:
async function load() {
let url = `www.com/file.json`
let data = await (await fetch(url)).json()
return data
}
I need to render my page after loading my json file from server:
export const Page = async () => {
const data_ = await load()
return (
<div className="page">
content
</div>
)
}
How can i do it?
You can use the useEffect hook to make the call when the component mounts, it will be called once and fetch the data you need, then it will set it in the data state to be used in the return inside the divs.
As you did not provide the structure of the response I cannot give a more detailed explanation on how to render the data itself. Also id_ has no use in your example but I kept it there to closely resemble your example.
import React, {useState, useEffect} from "react";
export const Page = () => {
const [data, setData] = useState(null)
useEffect(() => {
const load = async (id_) => {
let url = `www.com/file.json`
let data = await (await fetch(url)).json()
const manipulatedData = ...
// do manipulation
setData(manipulatedData)
}
load()
}, [])
return (
<div className="page">
{data ? data : null}
</div>
);
}
export default Page;
You need to use useEffect and useState
Here's how you could achieve this:
import {useState, useEffect} from 'react';
export const Page = () => {
const [data, setData] = useState();
useEffect(() => {
async function load() {
let url = `www.com/file.json`;
let data = await (await fetch(url)).json();
setData(data);
}
load();
}, []);
return <div className="page">{JSON.stringify(data)}</div>;
}
Replace JSON.stringify with data.something to show a particular field in the data
A couple of tips:
React components cannot be async functions
useState carries variables you need to render the page
useEffect is a function that lets your component handle calling async code or any other kind of side effect
You can use the hook useEffect() & useState() to load your data :
function load() {
let url = `www.com/file.json`
let data = await (await fetch(url)).json()
return data
}
export const Page = async () => {
const [data, setData] = useState(null)
useEffect(() => {
load().then((_data) => {
setData(_data)
})
}, [])
if (!data) {
return <div>loading data...</div>
}
return (
<div className="page">
data is ready to use !
</div>
)
}

How to process data received from an AJAX request in React

I have a custom hook named "useFetch" which makes an AJAX request and stores the result in the state. I simply want to format the data received from the ajax using a function in my component but not sure how to do this since the function needs to be called only after the data is received.
An example is below:
import React, { Component, useState } from "react";
import useFetch from "../../../Hooks/useFetch";
const Main = () => {
const { data, isPending, error } = useFetch(
"http://127.0.0.1:8000/api/historic/1"
);
function formatData(data){
//Do some processing of the data after it's been received
}
//This doesn't work of course because it runs before the data has been received
const formatted_data=formatData(data);
return (
//Some display using the formatted data
);
};
export default Main;
This is the custom hook, useFetch, which is used in the above component. I'd prefer to not have to do the formatting in here because the formatting is specifically related to the above component and this custom hook is designed to have more universal utility.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isPending, setisPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
fetch(url, { signal: abortCont.signal })
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw Error("could not fetch data for that resource");
}
})
.then((data) => {
setData(data);
setisPending(false);
setError(null);
})
.catch((er) => {
if (er.name === "AbortError") {
console.log("fetch aborted");
} else {
setError(er.message);
setisPending(false);
}
});
return () => abortCont.abort();
}, [url]);
return { data, isPending, error };
};
export default useFetch;
You should wrap it with useEffect hook with data as it's deps.
const [formattedData, setFormattedData] = useState();
useEffect(() => {
if (!data) return;
const _formattedData = formatData(data);
setFormattedData(_formattedData);
}, [data]);

How to pass state data from custom hooks to react component?

I have custom hook(useFetch) that takes URL as input and fetch data from that URL and returns data. I want to implement spinner (already made Spinner component ) on my other components and I tried by making state for the isLoading and setIsLoading of spinner.
my custom hook code:
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return dataArray;
};
export default useFetch;
This is the component that I want to implement spinner.
import React, { useState } from 'react';
import CONSTANTS from '../../constants/constants';
import CompanyLists from '../../components/company-lists/CompanyLists';
import Pagination from '../../components/pagination/Pagination';
import useFetch from '../../components/effects/use-fetch.effect';
import Spinner from '../../components/spinner/Spinner';
const CompanyListing = () => {
const [counter, setCounter] = useState(1);
const companies = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
return (
<>
<Container>
<div style={userStyle}>
{companies ? companies.map((company) => <CompanyLists key={company.id} {...company} />) : 'No companies'}
</div>
<Pagination props={companies} counter={counter} name="companies" setCounter={setCounter} />
<Spinner />
</Container>
</>
);
};
const userStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(1, 1fr)',
gridGap: '1rem',
};
export default CompanyListing;
Problem here is: How can I send those loading state from hook to CompanyListing component. Any help will be appreciated.
EDIT:
I have other component that also calls same hook and I want them not to be broken. As I didn't mention on original question .
My another case:
const jobsUrl = `${CONSTANTS.BASE_URL}/jobs?page=${counter}`;
const jobs = useFetch(jobsUrl);
AND
const { city, company_name, company_id, department, description, job_type, position, posted_at, url } = useFetch(
`${CONSTANTS.BASE_URL}/jobs/${id}`
);
How can I destructure in these two cases ?
You do not need anything special here. Just return the isLoading state with the dataArray from the useFetch hook.
As mentioned from the edit you need the useFetch to be more reusable and return data in different formats depending on the API response, hence the state should be initialized as null.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
// it is best to initialize the state as null because response.data
// may be an object or an array depending on the API response
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const value = await res.json();
setData(value.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return {data, isLoading};
};
export default useFetch;
In the components you want to use the custom hook, you can destructure
the value for data and isLoading but to futher destructure values from the returned data we have to check if data is null
// destructure the values
const {data, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
// in this case data will be an array based on your API response
// please make sure to check data.length before trying to loop over
// and render the content, for example
return (
<div>
{
data.length && data.map(company => (
<CompanyLists key={company.id} {...company} />
));
}
</div>
)
For the second case where you will be fetching data using useFetch(`${CONSTANTS.BASE_URL}/jobs/${id}`); you have to check if the returned data is not null before destructuring further. Example
const { data, isLoading } = useFetch(`${CONSTANTS.BASE_URL}/jobs/${id});
if (isLoading) {
return <div>Loading...</div>
}
if (data) {
const {
city,
company_name,
company_id,
department,
description,
job_type, position, posted_at, url } = data;
return (
// your jsx code
// for example
<h3>{company_name}</h3>
<p>{department}</p>
)
}
You need to return isLoading as well from the hook.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return {dataArray, isLoading};
};
export default useFetch;
And use this in your component like this
import React, { useState } from 'react';
import CONSTANTS from '../../constants/constants';
import CompanyLists from '../../components/company-lists/CompanyLists';
import Pagination from '../../components/pagination/Pagination';
import useFetch from '../../components/effects/use-fetch.effect';
import Spinner from '../../components/spinner/Spinner';
const CompanyListing = () => {
const [counter, setCounter] = useState(1);
const {dataArray: companies, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
return (
<>
<Container>
<div style={userStyle}>
{companies ? companies.map((company) => <CompanyLists key={company.id} {...company} />) : 'No companies'}
</div>
<Pagination props={companies} counter={counter} name="companies" setCounter={setCounter} />
{isLoading && <Spinner />}
</Container>
</>
);
};
const userStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(1, 1fr)',
gridGap: '1rem',
};
export default CompanyListing;
From your custom hook you can return both like
return {companies: dataArray, isLoading };
And destruct both
const {companies, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
First, you might need to change your useFetch effect a bit to update isLoading correctly. Then you could return both dataArray and isLoading :
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
await fetchData();
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
}, [url]);
return [dataArray, isLoading];
};
export default useFetch;
And use it like the following :
const [companies, isLoading] = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);

useEffect within a custom hook doesn't rerender on delete

I am trying to use hooks and implement a custom hook for handling my data fetching after every update I send to the API.
My custom hook, however, doesn't fire on change like I want it too. Delete has to be clicked twice for it to rerender. Note: I removed some functions from this code as they don't pertain to the question.
import React, { useState, useEffect } from "react"
import {Trash} from 'react-bootstrap-icons'
import InlineEdit from 'react-ions/lib/components/InlineEdit'
function Board(){
const [render, setRender] = useState(false)
const [boards, setBoards] = useState([]);
const [isEditing, setEdit] = useState(false)
const [value, setValue] = useState("")
const[newValue, setNewValue] = useState("")
const [error, setError] = useState("")
function useAsyncHook(setState, trigger) {
const [result] = useState([]);
const [loading, setLoading] = useState("false");
useEffect(() => {
async function fetchList() {
try {
setLoading("true");
const response = await fetch(
`http://localhost:8080/api/boards`
);
const json = await response.json();
setState(json)
} catch (error) {
//console.log(error)
setLoading("null");
}
}
fetchList()
}, [trigger]);
return [result, loading];
}
useAsyncHook(setBoards, render)
const handleDelete = (id) => {
console.log("delete clicked")
setLoading(true);
fetch(`http://localhost:8080/api/boards/` + id, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
},
})
setRender (!render)
}
return(
<div>
<ul>
{boards.map(board => (
<li key={board.id}>
<InlineEdit value={board.size} isEditing={isEditing} changeCallback={(event)=>handleSave (event, board.id)} />
<Trash onClick={()=>handleDelete(board.id)}/>
</li>
</ul>
</div>
);
}
export default Board
OPTION 1:
Maybe you wanna have a hook that tells you when to fetch the board, right? For example:
const [auxToFetchBoard, setAuxToFetchBoard] = useState(false);
Then, in a useEffect you execute the function fetchBoard everytime that hook changes:
useEffect(fetchBoard, [auxToFetchBoard]);
Finally, in your handleDelete function, if your delete request returns correctly, you have to update auxToFetchBoard. Something like this:
const handleDelete = (id) => {
setIsLoading(true);
setError("");
fetch(yourURL, yourOptions)
.then(res => {
// check if response is correct and
setIsLoading(false);
setAuxToFetchBoard(!auxToFetchBoard);
})
.catch(() => {
setIsLoading(false);
setError("Error while deleting stuff");
});
};
Note: I changed the names of isLoading and setIsLoading because they are more explicit.
OPTION 2:
Instead of fetching the board again and again, you can update your board (in this case your code would be in 8th line inside the handleDeletefunction).
Hope it helps.

Resources