Displayed data does not update - javascript

I am using a function to fetch some data and display the data returned. The problem: The data does not display.
function Profile() {
const profileUser = getData(userId)
return (
<div>
<p>{profileUser?.name}</p>
</div>
)
}
The getData is an asynchronous function returning fetching data from firebase while using await and returning an object.

larz answer is good, but you may get a warning/error about useEffect being async. You aren't supposed to have an asyncronous useEffect method. There is a workaround though.
You need to define an async function inside of the useEffect hook that calls your async function. Then use that data to set the state. You need the state since that is what makes it reactive.
function Profile() {
const [profileUser, setProfileUser] = useState({});
useEffect(() => {
const fetchUserProfile = async () => {
const user = await getData()
setProfileUser(user);
}
fetchUserProfile()
}, []);
return (
<div>
<p>{profileUser?.name}</p>
</div>
)
}
Importantly, dont await your fetchUserProfile call, since again, the useEffect method is not async.

You'll probably want to store profileUser in state and then use useEffect to fetch that data when the component mounts
function Profile() {
const [profileUser, setProfileUser] = useState();
useEffect(async () => {
const user = await getData(userId);
setProfileUser(user);
}, []);
return (
<div>
<p>{profileUser?.name}</p>
</div>
)
}

Related

ReactJS how to update page after fetching data

I'm new to ReactJS and I'm now trying to do an interactive comments section (taken from frontendmentor.io), but the App component just doesn't show what it's supposed to show
This is my App component:
function App() {
const [data, setData] = useState([]);
useEffect(() => {
const getComm = async () => {
await fetchData();
};
getComm();
}, []);
console.log(data);
const fetchData = async () => {
const res = await fetch("db.json").then(async function (response) {
const comm = await response.json();
setData(comm);
return comm;
});
};
return (
<Fragment>
{data.length > 0 ? <Comments data={data} /> : "No Comments to Show"}
</Fragment>
);
}
export default App;
The console.log(data) logs two times:
the first time it's an empty Array;
the second time it's the Array with my datas inside.
As it follows:
If I force the App to print the Comments it just says that cannot map through an undefined variable
This is my Comments component:
function Comments({ data }) {
return (
<div>
{data.map((c) => (
<Comment key={c.id} />
))}
</div>
);
}
export default Comments;
I'm wondering why the page still displays No Comments to Show even if the log is correct
#Cristian-Irimiea Have right about response get from fetch. Response is an a object and can't be iterate. You need to store in state the comments from response
But you have multiple errors:
Take a look how use async function. Your function fetchData looks bad.
// Your function
const fetchData = async () => {
const res = await fetch("db.json").then(async function (response) {
const comm = await response.json();
setData(comm);
return comm;
});
};
// How can refactor
// fetchData function have responsibility to only fetch data and return a json
const fetchData = async () => {
const response = await fetch("./db.json");
const body = await response.json();
return body;
};
You are updating state inside fetch function but a good solution is update state then promise resolve:
useEffect(() => {
// here we use .then to get promise response and update state
fetchData().then((response) => setData(response.comments));
}, []);
The initial state of your data is an array.
After you fetch your data from the response you get an object. Changing state types is not a good practice. You should keep your data state as an array or as an object.
Considering you will keep it as an array, you need use an array inside of setData.
Ex.
comm && Array.isArray(comm.comments) && setData(comm.comments);
As for your Comments component you should consider expecting an array not an object.
Ex.
function Comments(data) {
return (
<div>
{data.map((c) => (
<Comment key={c.id} />
))}
</div>
);
}
export default Comments;

Custom react hook with useeffect, cant use in non-component function

I made a custom react hook, which has a useEffect and, for now, returns a set of different states. It's a hook for axios, and the gist of it is this:
export default function useAxios({ url, data = {}, method = "GET"} ) {
var [loading, setLoading] = useState(true)
var [data, setData] = useState(null)
useEffect(function() {
(async function() {
// do axios request and set loading and data states
})()
return () => // cleanup function that cancels axios request
}, [])
return {
loading,
data
}
}
Now, in a simple component I can easily use this custom hook - but my question is: What if I want to use my hook inside an event handler, say:
export default MyComponent() {
function handleSubmit(e) {
var { data } = useAxios({
url: "/my-end-point",
data: {
testInput: e.target.testInput.value
}
})
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="testInput" />
<button type="submit">Submit</button>
</form>
)
}
Problem is my useAxios hook has a useEffect, and so I cannot use it inside a non-component function, i.e. handleSubmit. So what is the work around? Is there even one? Thanks in advance.
As to React's
Only Call Hooks from React Functions, you should always:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks.
Fail to satisfy these two rules leads to unexpected render result out of React.
With those rules in mind, you should return a submitHanlder from react hook instead of just passing the hook function into another component as a callback function.
I might guess that your intention is to trigger the axios request on the submit event. If so, it is possible to achieve that without passing whole hook into event handler.
First of all, as the rules say, you have to make sure your hook got called in every render. So the MyComponent can be rewrite in the below way:
export default function MyComponent() {
var startRequest = useAxios({url: "/my-end-point"}) //<---- useAxios now returns the startRequest function, and will always be called on every render
return (
<form onSubmit={(e) => {
e.preventDefault()
startRequest({testInput: e.target.testInput.value}) // <----- call your startRequest here in the submit hanlder
.then(data => {
//process your data here
})
}}>
<input type="text" name="testInput" />
<button type="submit">Submit</button>
</form>
)
}
Please note that now the hook returns a function startRequest which you can put in your handler, and trigger that handler any time appropriated.
And rearrange your hook's code like below:
export function useAxios({ url, method = "GET"} ) {
var [loading, setLoading] = useState(true)
// <------ no setData here
var startRequest = async function(body = {}) { // <------ Move your input here
// do axios request and set loading and data states
setLoading(true)
await data = axios.post(body)
setLoading(false)
return data // <------- return data as promise
}
var cancelRequest = () => // cleanup function that cancels axios request
useEffect(function() {
return cancelRequest
}, []) // useEffect only helps your cancel request on unmounted.
return startRequest
}
The useEffect now only helps you cleanup axios request without the need to start one, since firing a request should be an event handler's job.
And since the data return by axios is in a promise, you don't need to explicitly setData to store your response data so I removed the line of useState(null).
I would take a look at popular libraries like SWR (useSWR) and apollo-client (useQuery). They're approach is something like this when making get requests
const MyComponent = () => {
const [shouldSkip, setShouldSkip] = useState(true);
const queryResult = useQuery('my-url', {skip: shouldSkip});
const handleSubmit = () => {
setShouldSkip(false);
// this will cause the component to rerender, and skip will now be false
}
}
When making post requests, its something like this:
const MyComponent = () => {
//useMutation returns a callable function whenever you want
const callFunction = useMutation('my-url');
const handleSubmit = () => {
await callFunction()
}
}
You can also take a look at axios-specific hooks like https://github.com/simoneb/axios-hooks, another common pattern they use is to include a refetch function as a result of the hook, that can be called at anytime (like in an event handler)
The point of the hook is not to make the request for you, the point of the hook is to communicate the internal state of stuff (the axios request, in your case) to the component, so that you can render stuff based around that state (like loading states, or the data).
In your case, you can change the value of the query based on the component state, and have the hook return the data to the component based on its parameters. Something like this:
const useAxios = ({ query }) => {
var [loading, setLoading] = useState(true)
var [data, setData] = useState(null)
useEffect(function () {
(async function () {
setLoading(true)
// do axios request and set loading and data states
const request = await axios.get('endpoint', { query })
setData(request.data)
setLoading(false)
})()
return () => { }// cleanup function that cancels axios request
}, [])
return {
loading,
data
}
}
const Component = () => {
const [query, setQuery] = useState('')
const { loading, data } = useAxios({ query });
const submitHandler = (event) => { setQuery(event.target.testInput.value) }
return (
<>
<form onSubmit={submitHandler}>
<input name="testInput" />
<input type="submit" />
</form>
{loading && (
<>a spinner</>
)}
{data && (
<DataRenderer data={data} />
)}
</>
)
}

React useEffect looping many times when fetching data and setState using fetch API

Why is this triggering fetchData multiple times? The console.log seems to loop almost infinitely? How do I get this to run just once onload and trigger only once when fetchData() is called later? What am I doing wrong here or missing?
const [data, setData] = useState(null);
let fetchData = React.useCallback(async () => {
const result = await fetch(`api/data/get`);
const body = await result.json();
setData(body);
console.log(data)
},[data])
useEffect(() => {
fetchData();
},[fetchData]);
Update (Additional Question): How to wait for data to populate before return() the below this now gives the error because it is null at first?: data.map is not a function
return (
<select>
{data.map((value, index) => {
return <option key={index}>{value}</option>
})}
</select>
)
In the useCallback hook you pass data as a dependency and also simultaneously change the value of data inside the callback by calling setData, which means every time the value of data changes fetchData will be reinitialized.
In the useEffect hook fetchData is a dependency which means every time fetchData changes useEffect will be triggered. That is why you get an infinite loop.
Because you want to fetch data once when the component is mounted, I think useCallback is unnecessary here. There is no need to memoize the function fetchData unnecessarily.
Solution
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await fetch(`api/data/get`);
const body = await result.json();
setData(body);
} catch(err) {
// error handling code
}
}
// call the async fetchData function
fetchData()
}, [])
If you want to log the value of data when it changes, you can use another useEffect to do that instead. For example,
useEffect(() => {
console.log(data)
}, [data])
P.S. - Also don't let the promise get unhandled, please use a try...catch block to handle the error. I have updated the solution to include the try..catch block.
Edit - solution for the additional question
There are two possible solutions,
Because you expect the value of data to be an array after the API call you can initialize the value of data as an empty array like,
const [data, setData] = useState([]);
But if for some reason you have to initialize the value of data as null. Here is how you can render the information returned from the API call.
// using short-circuit evaluation
return (
<select>
{data && data.length && data.map((value) => {
return <option key={`select-option-${value}`}>{value}</option>
})}
</select>
)
// using a ternary
return (
<div>
{ data && data.length
? (<select>
{
data.map(value => <option key={`select-option-${value}`}>{value}</option>)
}
</select>
)
: <div>Data is still loading...</div>
}
</div>
)
Do not use indexes as key, use something unique for the key.

useEffect spamming requests

State
const [user, setUser] = useState({});
checkIfUserIsEnabled()
async function checkIfUserIsEnabled() {
const res = await fetch("http://localhost:8080/users/finduserbytoken?id=" +
getTokenIdFromURL);
res.json()
.then(res => setUser(res))
.catch(err => setErrors(err));
}
useEffect When i call my checkIfUserIsEnabled() in the useEffect below it gets rendered once and displays the false version in the return method.
useEffect(() => {
verifyEmail(getTokenIdFromURL);
checkIfUserIsEnabled();
return () => {
/* cleanup */
};
}, [/* input */])`
useEffect (2th) If i do it like this instead, it keeps spamming the requests towards my API and displays true.
useEffect(() => {
checkIfUserIsEnabled();
});
Return
return (
<div className="emailVerificationWrapper">
{user.enabled
? <h1>Thank you for registrating, {user.firstName}. Account is verified!</h1>
: <h1>Attempting to verify account...</h1>}
</div>
)
To my question(s): Why does the second useEffect spam the request? and is there a way i can make the request being rendered every ~2-3 second instead of the spam? and could i make it stop doing the request once it actually returns true?
The effect hook runs when the component mounts but also when the component updates. Because we are setting the state after every data fetch, the component updates and the effect runs again.
It fetches the data again and again. That's a bug and needs to be avoided. We only want to fetch data when the component mounts. That's why you can provide an empty array(or something which doesn't change) as second argument to the effect hook to avoid activating it on component updates(or only when that parameter changes) but only for the mounting of the component.
let URL = `http://localhost:8080/users/finduserbytoken?id=`;
async function checkIfUserIsEnabled() {
const res = await fetch(`$(URL)` +
getTokenIdFromURL);
res.json()
.then(res => {setUser(res); return Promise.resolve()})
.catch(err => {setErrors(err); return Promise.reject()});
}
useEffect(() => {
const abortController = new AbortController();
const fetchData = async() => await checkIfUserIsEnabled();
fetchData();
return () => {
abortController.abort();
};
}, [URL]);
useEffect(() => {
checkIfUserIsEnabled();
}); <-- no dependency
As your useEffect doesn't have any dependency it will run on every render, so every time you change some state and your component re-renders it will send requests.

React Hooks - Making an Ajax request

I have just began playing around with React hooks and am wondering how an AJAX request should look?
I have tried many attempts, but am unable to get it to work, and also don't really know the best way to implement it. Below is my latest attempt:
import React, { useState, useEffect } from 'react';
const App = () => {
const URL = 'http://api.com';
const [data, setData] = useState({});
useEffect(() => {
const resp = fetch(URL).then(res => {
console.log(res)
});
});
return (
<div>
// display content here
</div>
)
}
You could create a custom hook called useFetch that will implement the useEffect hook.
If you pass an empty array as the second argument to the useEffect hook will trigger the request on componentDidMount. By passing the url in the array this will trigger this code anytime the url updates.
Here is a demo in code sandbox.
See code below.
import React, { useState, useEffect } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch(url);
const json = await response.json();
setData(json);
}
fetchData();
}, [url]);
return data;
};
const App = () => {
const URL = 'http://www.example.json';
const result = useFetch(URL);
return (
<div>
{JSON.stringify(result)}
</div>
);
}
Works just fine... Here you go:
import React, { useState, useEffect } from 'react';
const useFetch = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const fetchUser = async () => {
const response = await fetch(url);
const data = await response.json();
const [user] = data.results;
setData(user);
setLoading(false);
};
useEffect(() => {
fetchUser();
}, []);
return { data, loading };
};
const App = () => {
const { data, loading } = useFetch('https://api.randomuser.me/');
return (
<div className="App">
{loading ? (
<div>Loading...</div>
) : (
<React.Fragment>
<div className="name">
{data.name.first} {data.name.last}
</div>
<img className="cropper" src={data.picture.large} alt="avatar" />
</React.Fragment>
)}
</div>
);
};
Live Demo:
Edit
Updated based on version change (thanks #mgol for bringing it to
my attention in the comments).
Great answers so far, but I'll add a custom hook for when you want to trigger a request, because you can do that too.
function useTriggerableEndpoint(fn) {
const [res, setRes] = useState({ data: null, error: null, loading: null });
const [req, setReq] = useState();
useEffect(
async () => {
if (!req) return;
try {
setRes({ data: null, error: null, loading: true });
const { data } = await axios(req);
setRes({ data, error: null, loading: false });
} catch (error) {
setRes({ data: null, error, loading: false });
}
},
[req]
);
return [res, (...args) => setReq(fn(...args))];
}
You can create a function using this hook for a specific API method like so if you wish, but be aware that this abstraction isn't strictly required and can be quite dangerous (a loose function with a hook is not a good idea in case it is used outside of the context of a React component function).
const todosApi = "https://jsonplaceholder.typicode.com/todos";
function postTodoEndpoint() {
return useTriggerableEndpoint(data => ({
url: todosApi,
method: "POST",
data
}));
}
Finally, from within your function component
const [newTodo, postNewTodo] = postTodoEndpoint();
function createTodo(title, body, userId) {
postNewTodo({
title,
body,
userId
});
}
And then just point createTodo to an onSubmit or onClick handler. newTodo will have your data, loading and error statuses. Sandbox code right here.
use-http is a little react useFetch hook used like: https://use-http.com
import useFetch from 'use-http'
function Todos() {
const [todos, setTodos] = useState([])
const { request, response } = useFetch('https://example.com')
// componentDidMount
useEffect(() => { initializeTodos() }, [])
async function initializeTodos() {
const initialTodos = await request.get('/todos')
if (response.ok) setTodos(initialTodos)
}
async function addTodo() {
const newTodo = await request.post('/todos', {
title: 'no way',
})
if (response.ok) setTodos([...todos, newTodo])
}
return (
<>
<button onClick={addTodo}>Add Todo</button>
{request.error && 'Error!'}
{request.loading && 'Loading...'}
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
or, if you don't want to manage the state yourself, you can do
function Todos() {
// the dependency array at the end means `onMount` (GET by default)
const { loading, error, data } = useFetch('/todos', [])
return (
<>
{error && 'Error!'}
{loading && 'Loading...'}
{data && data.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
Live Demo
I'd recommend you to use react-request-hook as it covers a lot of use cases (multiple request at same time, cancelable requests on unmounting and managed request states). It is written in typescript, so you can take advantage of this if your project uses typescript as well, and if it doesn't, depending on your IDE you might see the type hints, and the library also provides some helpers to allow you to safely type the payload that you expect as result from a request.
It's well tested (100% code coverage) and you might use it simple as that:
function UserProfile(props) {
const [user, getUser] = useResource((id) => {
url: `/user/${id}`,
method: 'GET'
})
useEffect(() => getUser(props.userId), []);
if (user.isLoading) return <Spinner />;
return (
<User
name={user.data.name}
age={user.data.age}
email={user.data.email}
>
)
}
image example
Author disclaimer: We've been using this implementation in production. There's a bunch of hooks to deal with promises but there are also edge cases not being covered or not enough test implemented. react-request-hook is battle tested even before its official release. Its main goal is to be well tested and safe to use as we're dealing with one of the most critical aspects of our apps.
Traditionally, you would write the Ajax call in the componentDidMount lifecycle of class components and use setState to display the returned data when the request has returned.
With hooks, you would use useEffect and passing in an empty array as the second argument to make the callback run once on mount of the component.
Here's an example which fetches a random user profile from an API and renders the name.
function AjaxExample() {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUser(data.results[0]);
});
}, []); // Pass empty array to only run once on mount.
return <div>
{user ? user.name.first : 'Loading...'}
</div>;
}
ReactDOM.render(<AjaxExample/>, document.getElementById('app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
I find many wrong usages of useEffect in the answers above.
An async function shouldn't be passed into useEffect.
Let's see the signature of useEffect:
useEffect(didUpdate, inputs);
You can do side effects in didUpdate function, and return a dispose function. The dispose function is very important, you can use that function to cancel a request, clear a timer etc.
Any async function will return a promise, but not a function, so the dispose function actually takes no effects.
So pass in an async function absolutely can handle your side effects, but is an anti-pattern of Hooks API.
Here's something which I think will work:
import React, { useState, useEffect } from 'react';
const App = () => {
const URL = 'http://api.com';
const [data, setData] = useState({})
useEffect(function () {
const getData = async () => {
const resp = await fetch(URL);
const data = await resp.json();
setData(data);
}
getData();
}, []);
return (
<div>
{ data.something ? data.something : 'still loading' }
</div>
)
}
There are couple of important bits:
The function that you pass to useEffect acts as a componentDidMount which means that it may be executed many times. That's why we are adding an empty array as a second argument, which means "This effect has no dependencies, so run it only once".
Your App component still renders something even tho the data is not here yet. So you have to handle the case where the data is not loaded but the component is rendered. There's no change in that by the way. We are doing that even now.

Categories

Resources