I have a simple async function that I want to be called when a page loads which fetches data and updates my context.
import { useContext } from 'react'
import { ContentSlotsContext } from '../commerce-api/contexts'
export default async function getSlots() {
const { slots, setSlots } = useContext(ContentSlotsContext)
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
const data = await response.json();
setSlots(data);
}
I'm trying to call it like this:
useEffect(() => {
getSlots()
})
slots and setSlots are a setState the context is initialised with.
But it's not being called. When I call it outside of useEffect it works but infinitely loops due to constantly re-rendering.
The problem is that you're calling useContext from a function that is not a react function component or a custom hook (see the rules of hooks). There's a useful ESLint plugin eslint-plugin-react-hooks that will warn against this.
To fix the issue, you can instead pass setSlots as a parameter to getSlots:
export default async function getSlots(setSlots) {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
const data = await response.json();
setSlots(data);
}
and call useContext before useEffect, at the top level of your function component (also a rule of hooks), with an empty dependencies array as the second argument, so the effect only fires once (see the note in the useEffect docs):
..
const { setSlots } = useContext(ContentSlotsContext);
useEffect(() => {
getSlots(setSlots);
}, []);
..
Related
Cheers,
I have the bellow React code, where i need to send an HTTP Request to check if my actual user have permission or not to access my page.
For that, im using useEffect hook to check his permission every page entry.
But my actual code does not wait for authorize() conclusion. Leading for /Unauthorized page every request.
What i am doing wrong?
import React, { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { security } from "../../services/security";
export default function MypPage() {
const navigate = useNavigate();
const [authorized, setAuthorized] = useState(false);
const authorize = useCallback(async () => {
// it will return true/false depending user authorization
const response = await security.authorize("AAA", "BBB");
setAuthorized(response);
});
useEffect(() => {
authorize();
if (authorized) return;
else return navigate("/Unauthorized");
}, [authorize]);
return <div>MypPage</div>;
}
Thanks.
You are experiencing two issues:
Expecting a non-awaited asynchronous function to be awaited.
Expecting setState function to be synchronous.
Issue 1
authorize is asynchronous and is called within the useEffect but is not awaited. This means that the following code will be executed immediately before authorize has been completed.
Solution
A useEffect cannot be passed an asynchronous function, but a new asynchronous function can be created and then called from within the useEffect. This would allow you to create a proper asynchronous flow that works as you are expecting.
Example:
useEffect(() => {
// create new async function to be run.
const myNewAsyncMethod = async () => {
// async request.
const response = await request();
// logic after async request.
if (response) {
return;
}
}
// trigger new async function.
myNewAsyncMethod();
}, [])
Issue 2
setState functions are not synchronous, they do not update the state value immediately. They ensure that on the next render of the component that the value will be updated. This means that accessing authorized directly after calling setAuthorized will not result in the value you are expecting.
Solution
Just use the value you passed to the setState function.
Example:
// get new value (i.e. async request, some calculation)
const newValue = true;
// set value in state.
setAuthorized(newValue);
// do further logic on new value before next render.
if (newValue) {
// do something
}
Conclusion
Altogether you should end up with something similar too:
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { security } from "../../services/security";
export default function MypPage() {
const navigate = useNavigate();
const [authorized, setAuthorized] = useState(false);
useEffect(() => {
// don't need to memoize the authorize function using `useCallback`
// if you are only going to use it in a `useEffect`
const authorize = async () => {
// run async request.
const response = await security.authorize("AAA", "BBB");
// setAuthorized will not update authorized immediately.
// setAuthorized will ensure that on the next render the new
// value is available.
setAuthorized(response);
// access value from response of async request as authorized
// will not be updated yet.
if (!response) {
navigate("/Unauthorized");
}
};
// call the async function.
authorize();
}, [navigate, setAuthorized]);
return <div>MyPage</div>;
}
useEffect hooks do not wait for async code. Since you want to check every time you access this page, you really don't need to set a state for this.
import React, { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { security } from "../../services/security";
export default function MypPage() {
const navigate = useNavigate();
const authorize = useCallback(async () => {
// it will return true/false depending user authorization
const isAuthorized = await security.authorize("AAA", "BBB");
// If the user is authorized this function will finish
// If not, the user will be redirected.
if(isAuthorized) return;
navigate("/Unauthorized");
});
// With will only be called when the component is mounted
useEffect(() => {
authorize();
}, []);
return <div>MypPage</div>;
}
Whenever I needed to call async-wait function in useEffect, I always did like following.
I hope this would be helpful for you.
import React, { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { security } from "../../services/security";
export default function MypPage() {
const navigate = useNavigate();
useEffect(() => {
(async () => {
const authorized= await security.authorize("AAA", "BBB");
if (authorized) return;
else return navigate("/Unauthorized");
})()
}, []);
return <div>MypPage</div>;
}
Additionally to the other answers this functioality should be hanfled on the back end. You can modify js code on the fly from the browser, the authorization functionality is better handled with file permissions on the server side.
I have React component :
import { Hotels } from "./Hotels";
import WelcomePage from "./WelcomePage";
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js";
import {
getFirestore,
collection,
getDocs,
addDoc,
} from "https://www.gstatic.com/firebasejs/9.6.0/firebase-firestore.js";
import {
getAuth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
onAuthStateChanged,
signOut,
} from "https://www.gstatic.com/firebasejs/9.6.0/firebase-auth.js";
import { firebaseConfig, app, db, auth } from "../firebaseConfig";
import { useState } from "react";
function MainPage() {
const [hotels, setHotels] = useState([]);
const [authentication, setAuthentication] = useState(false);
async function fetchHotels() {
const _hotels = [];
const querySnapshot = await getDocs(collection(db, "reviews"));
querySnapshot.forEach((doc) => {
_hotels.push(doc.data());
});
console.log("fetched!");
setHotels(_hotels);
}
function isAuthenticated() {
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
const uid = user.uid;
setAuthentication(true);
} else {
// User is signed out
setAuthentication(false);
}
});
}
isAuthenticated();
fetchHotels();
return (
<main className="content">
<Hotels hotels={hotels} />
</main>
);
}
export default MainPage;
After the application starts, the fetchHotels function starts to be called endlessly (this is evidenced by console.log("fetched!") ).
Under the same conditions, in other components, other functions are called adequately.
You're calling fetchHotels at the top level of your function component, so it's called on every render. In fetchHotels, you eventually call setHotels with a new array, which causes a re-render (since the new array by definition is different from the current one). So when that render happens, it calls fetchHotels again, which eventually calls setHotels again, which causes...
You need to only call fetchHotels at appropriate times. For instance, it looks like you only need to do that when the component first mounts, in which case you'd do it inside a useEffect callback with an empty dependency array (so that it is only run on component mount). And since nothing else calls it, you can just do the fetch right there in the callback:
useEffect(() => {
let cancelled = false;
(async () => {
try {
const snapshot = await getDocs(collection(db, "reviews"));
if (!cancelled) {
const hotels = snapshot.map(doc => doc.data());
setHotels(hotels);
console.log("fetched!");
}
} catch(error) {
// ...handle/report error
}
})();
return () => {
// Flag so we don't try to set state when the component has been
// unmounted. Ideally, if `getDocs` has some way of being cancelled
// (like `AbortController`/`AbortSignal`), do that instead; using
// a flag like this doesn't proactively stop the process.
cancelled = true;
};
}, []);
Note I added error handling; don't let an async function throw errors if nothing is going to handle them. Also, I used map to more idiomatically build an array (hotels) from another array (snapshot).
(You have the same basic problem with isAuthenticated. The only reason it doesn't cause an infinite loop is that it calls setAuthentication with a boolean value, so the second time it does that, it's setting the same value that was already there, which doesn't cause a re-render.)
You can't invoke functions in a component like that. What you need to do is invoke it in useEffect and (optional) give it a parameter that triggers the useEffect function.
I am following along in a React course on Udemy. In this module, we have a simple task app to demonstrate custom hooks. I've come across a situation where the "task" state is being managed in the App.js file, the "useHttp" custom hook has a function "fetchTasks" which accepts "transformTasks" as a parameter when called inside App.js. The issue I am having is that "tranformTasks" manipulates the "tasks" state inside App.js, but it is actually being called and executed inside the "useHttp" custom hook. Would really love some help understanding the mechanism for how this works. How can the state be manipulated while called from another file without the state being passed in? The code does work as intended. Here's the github link to the full app, and below are the two relevant files: https://github.com/yanichik/react-course/tree/main/full-course/custom-hooks-v2
Here is the App.js file:
import React, { useEffect, useMemo, useState } from "react";
import Tasks from "./components/Tasks/Tasks";
import NewTask from "./components/NewTask/NewTask";
import useHttp from "./custom-hooks/useHttp";
function App() {
// manage tasks state here at top level
const [tasks, setTasks] = useState([]);
const myUrl = useMemo(() => {
return {
url: "https://react-http-104c4-default-rtdb.firebaseio.com/tasks.json",
};
}, []);
const { isLoading, error, sendRequest: fetchTasks } = useHttp();
useEffect(() => {
// func transforms loaded data to add id (firebase-generated), push to loadedTasks, then
// push to tasks state
const transformTasks = (taskObj) => {
let loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
};
fetchTasks(myUrl, transformTasks);
// if you add fetchTasks as a dependency this will trigger a re-render each time states
// are set inside sendRequest (ie fetchTasks) and with each render the custom hook (useHttp)
// will be recalled to continue the cycle. to avoid this, wrap sendRequest with useCallback
}, [fetchTasks, myUrl]);
const addTaskHandler = (task) => {
setTasks((prevTasks) => prevTasks.concat(task));
};
return (
<React.Fragment>
<NewTask onEnterTask={addTaskHandler} />
<Tasks
items={tasks}
loading={isLoading}
error={error}
onFetch={fetchTasks}
/>
</React.Fragment>
);
}
export default App;
And here is the "useHttp" custom hook:
import { useState, useCallback } from "react";
// NOTE that useCallback CANNOT be used on the top level function
function useHttp() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const sendRequest = useCallback(async (httpConfig, applyFunction) => {
setIsLoading(true);
setError(false);
try {
const response = await fetch(httpConfig.url, {
method: httpConfig.method ? httpConfig.method : "GET",
headers: httpConfig.headers ? httpConfig.headers : {},
body: httpConfig.body ? JSON.stringify(httpConfig.body) : null,
});
// console.log("response: " + response.method);
if (!response.ok) {
throw new Error("Request failed!");
}
const data = await response.json();
applyFunction(data);
// console.log("the formatted task is:" + applyFunction(data));
} catch (err) {
setError(err.message || "Something went wrong!");
}
setIsLoading(false);
}, []);
return { sendRequest, isLoading, error };
}
export default useHttp;
Sounds like you're learning from a decent course. The hook is using a technique called "composition". It knows you'll want to do some processing on the data once it has been fetched and let's you pass in (the applyFunction variable) your own snippet of code to do that processing.
Your snippet of code is just a function, but all parties agree on what parameters the function takes. (This is where using typescript helps catch errors.)
So you pass in a function that you write, and your function takes 1 parameter, which you expect will be the data that's downloaded.
The useHttp hook remembers your function and once it has downloaded the data, it calls your function passing in the data.
If you've used some of your own variables within the function you pass to the hook, they get frozen in time ... sort-of. This can of worms is a topic called 'closures' and I'm sure it will come up in the course if it hasn't already.
I am making an weather app with API, I am successfully receiving the data with API in the function but I am not able to take it out of the function
here is the code
import React, {useState} from 'react'
import {Text, View} from 'react-native'
const axios = require('axios');
let HomeScreen =() => {
let key = "XXXX"
axios.get(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
.then(function (response) {
// handle success
console.log(response)
})
return(
<Text>This is {response}</Text>
)
}
export default HomeScreen
If you want to simply return data from the API use a normal JS function, not a React component.
function getData() {
return axios(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
}
It will return a promise which you can then use somewhere else.
async main() {
const data = await getData();
}
If you want your component to render data retrieved from the API you need to do things differently.
Use useEffect to run once when the component is mounted, and use useState to store the data once it's been retrieved. The state will then inform the JSX how to render.
Note that the response from the API is an object with location and current properties. You can't just add that to the JSX because React won't accept it. So, depending on what value you need from the data, you need to target it specifically.
Here's an example that returns the text value from the condition object of the current object: "It is Sunny".
const { useEffect, useState } = React;
function Example() {
// Initialise your state with an empty object
const [data, setData] = useState({});
// Call useEffect with an empty dependency array
// so that only runs once when the component is mounted
useEffect(() => {
// Retrieve the data and set the state with it
async function getData() {
const key = 'XXX';
const data = await axios(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
setData(data.data);
}
getData();
}, []);
// If there is no current property provide a message
if (!data.current) return <div>No data</div>;
// Otherwise return the current condition text
return (
<p>It is {data.current.condition.text}</p>
);
}
Why is my useEffect react function running on every page load although giving it a second value array with a query variable?
useEffect( () => {
getRecipes();
}, [query]);
Shouldn't it only run when the query state variable changes? I have nothing else using the getRecipes function except of the useEffect function.
import React, {useEffect, useState} from 'react';
import './App.css';
import Recipes from './components/Recipes/Recipes';
const App = () => {
// Constants
const APP_ID = '111';
const APP_KEY = '111';
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
const [showRecipesList, setShowRecipesList] = useState(false);
// Lets
let recipesList = null;
// Functions
useEffect( () => {
getRecipes();
}, [query]);
// Get the recipie list by variables
const getRecipes = async () => {
const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free`);
const data = await response.json();
console.log(data.hits);
setRecipes(data.hits);
}
// Update the search constant
const updateSearch = e => {
console.log(e.target.value);
setSearch(e.target.value);
}
const runQuery = e => {
e.preventDefault();
setQuery(search);
}
// List recipes if ready
if (recipes.length) {
console.log(recipes.length);
recipesList = <Recipes recipesList={recipes} />
}
return (
<div className="App">
<form className='search-app' onSubmit={ runQuery }>
<input
type='text'
className='search-bar'
onChange={ updateSearch }
value={search}/>
<button
type='submit'
className='search-btn' > Search </button>
</form>
<div className='recipesList'>
{recipesList}
</div>
</div>
);
}
export default App;
Following this: https://www.youtube.com/watch?v=U9T6YkEDkMo
A useEffect is the equivalent of componentDidMount, so it will run once when the component mounts, and then only re-run when one of the dependencies defined in the dependency array changes.
If you want to call getRecipes() only when the query dependency has a value, you can call it in a conditional like so:
useEffect(() => {
if(query) {
getRecipes()
}
}, [query])
Also, as your useEffect is calling a function (getRecipes) that is declared outside the use effect but inside the component, you should either move the function declaration to be inside the useEffect and add the appropriate dependencies, or wrap your function in a useCallback and add the function as a dependency of the useEffect.
See the React docs for information on why this is important.
UseEffect hook work equivalent of componentDidMount, componentDidUpdate, and componentWillUnmount combined React class component lifecycles.but there is a different in time of acting in DOM.componentDidMount and useEffect run after the mount. However useEffect runs after the paint has been committed to the screen as opposed to before. This means you would get a flicker if you needed to read from the DOM, then synchronously set state to make new UI.useLayoutEffect was designed to have the same timing as componentDidMount. So useLayoutEffect(fn, []) is a much closer match to componentDidMount() than useEffect(fn, []) -- at least from a timing standpoint.
Does that mean we should be using useLayoutEffect instead?
Probably not.
If you do want to avoid that flicker by synchronously setting state, then use useLayoutEffect. But since those are rare cases, you'll want to use useEffect most of the time.