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.
Related
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 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);
}, []);
..
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>
);
}
I have a function in a separate js file that checks status codes received from Api requests, and depending on the code this function needs to perform some actions:
function handleResponseCodes(res) {
try {
if (res.status ===200 ) {
return res.json();
} else if (res.status === 404) {
// here I need to redirect to /help
} else if (!res.ok) {
alert("Error")
} else {
if (res.ok) {
return res.data;
}
}
} catch (error) {
console.log(error);
}
}
and then this function is used like this with fetch requests.( project will have 100+ api requests so this way makes the process easy to follow).
fetch(url, obj)
.then((res) => handleResponseCodes(res)
If res.code === 404 I need to redirect the user to /help url, problem is that when I try to use useHistory() hook like this:
import {useHistory) from 'react-router-dom'
const history = useHistory()
//and in the function
else if (res.status === 404) {
// here I need to redirect to /help
history.push("/help")
I get error saying that useHistory hook must be used only in functional components. Is there a React way to redirect/push user to the /help from outside functional component?(basically from inside a function)
You can also use document.location.href = '/help'
You can just pass history as a parameter like this
...inside function component
const history = useHistory();
console.log(history)
//call function here
yourFunc(history)
Good question. We have so many api calls and have to handle those responses accordingly.
What I want to mention here is the close relation between routing and UI(for example, ProfilePage component).
According to Hook rules, we can't use hook function outside the React component, so we have to use the ways like #Zhang and #istar's answers.
But split the routing and component isn't a good practice. Of course some developers use routing config file for routing, but changing the url is usually being done in component.
I think routing is also one part of component.
So I want to recommend you that do the routing inside the component. Please take the result of response handling function and do the routing according to its result.
I had a same problem because of previous build functions.
import { useHistory } from 'react-router-dom';
function ReactComponent () {
const history = useHistory();
// some arrow function inside main react component
const someHandler = ( response, history ) => {
outsideFunction( response, history );
};
}
export default ReactComponent;
you can do as you like, but i am breaking it to two functions.
export function outsideFunction ( response, history ) {
if ( response.status === 201 ) {
checkedSubmitTypeHandler( location );
redirectPage(history);
return true;
}
}
export function redirectPage (history) {
return history.push(`/help`);
}
So, here is what happening that, you getting response wherever, Inside react main component or you handling in some other exported function. Passing the parameter through the arrow functions used inside react component.
It's not best practice to do it like this.
You should create a useHistoryHook which will be resuable in your application and you can call it like this.
// history.js page inside src
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
// useHistorypush component
import React from 'react';
import history from '../../history';
const useHistoryPush = (pathname,onLoadWait = 30) => {
const refContainer = React.useRef(false);
setTimeout(() => {
refContainer.current = true;
}, onLoadWait);
return (link) => {
if (refContainer.current) {
history.push(link);
}
};
};
export default useHistoryPush;
// implementation inside page
import useHistoryPush from '../use-history-push';
const Reactcomponent = () => {
const push = useHistoryPush();
React.useEffect(() => {
if (successResponse) {
// on some condition you can call it inside useeffect and it will redirect to the url mention inside *push
push(`/help`);
}
}, [successResponse, push]);
}
export default Reactcomponent;