API inside child dynamic component doesn't fetch data - javascript

I have an api (photoFetch) that fetches a JSON that holds photo data from an online JSONplaceholder. It is located inside my child component, that is a dynamic component based on id (the parent component is my home page)
userPhotoPage.js (child component)
export const UserPhotoPage = async ({match}) => {
const [data, setData] = useState("");
const photoFetch = await fetch('https://jsonplaceholder.typicode.com/photos')
.then(res => res.json())
.then(result => setData(data.albums))
.catch(error => console.log("error"))
const userPhoto = photoFetch.find((userPhoto) =>{
return parseInt(match.params.id) == userPhoto.id
})
return <>
{match.isExact && <>
<h1>{userPhoto.title}</h1>
<p>image = {userPhoto.url}</p>
</>}
</>
}
I also tried wrapping it in a try catch statement, but nothing works. It doesn't fetch data and I always get a "(TypeError): Cannot read property 'find' of undefined" because my const userPhoto isn't fetching any data from photoFetch.find((userPhoto). I'm new to REACT so I don't really know how to proceed to fixing this. Have been working on this for a while now.
I am able to fetch data with a user JSON on my parent component (home page), and I use a very similar method. I am trying to recreate it for my child component but it doesn't work. This is how it looks like on the home page. (I'm only posting a snippet of the code here, the top portion)
homePage.js (parent component, just an example)
class homePage extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
isLoading: false,
search: ""
};
}
async componentDidMount() {
this.setState({isLoading: true});
await fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(result => this.setState({users: result}))
.catch(error => console.log("error"))
}
Thank you again stack overflow for helping out a noob like me!

First of all you must not have a direct fetch request in the functional component body, instead you must use useEffect hook.
Secondly, instead of getting the data and then filtering on id, you can simply request /photos/:id
Lastly, the data returned is an array and doesn't contains albums key
You code will look like
export const UserPhotoPage = async ({match}) => {
const [data, setData] = useState("");
useEffect(() => {
await fetch(`https://jsonplaceholder.typicode.com/photos/${match.params.id}`)
.then(res => res.json())
.then(result => setData(result))
.catch(error => console.log("error"))
}, [match.params.id])
return <>
{match.isExact && <>
<h1>{data.title}</h1>
<p>image = {data.url}</p>
</>}
</>
}

When you do const photoFetch = await (...) your code awaits to get the result of the promise and sets the const's value with it. However, your promise is not returning anything, because the body of the last .then() executes a void operation that just changes the value of the hook "data" by calling setData(...). That's why your error says Cannot read property 'find' of undefined": photoFind does not have an assigned value at that point.
You should return a value in the arrow function of the last .then() in order to set that value to the constant. (I do not know the API you are using so I am unsure what this value should be. Maybe your userPhoto is trying to search for a value in your hook called data?)
P.D: I would also suggest you to remember setting the value of isLoading to false again when the result is fetched or an error is catched in the method ComponentDidMount of your homePage!

React introduces hooks in version 16.8 which can be use in our functional components we have a hook called useEffect to call apis or any asynchronous call which we used to call in our componentDidMount or componentWillMount. So your child component will look something like this.
Also we don't need to .then when we are using async-await.
export const UserPhotoPage = async ({match}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/photos')
.then(res => res.json())
.then(result => {
setData(data.albums)
})
.catch(error => console.log("error"))
}
const userPhoto = photoFetch.find((userPhoto) =>{
return parseInt(match.params.id) == userPhoto.id
})
return (<>
{match.isExact && <>
<h1>{userPhoto.title}</h1>
<p>image = {userPhoto.url}</p>
</>}
</>)
}
Please have a read using useEffect Effectively

Related

React returning empty state

I understand that useEffect() hook is supposed to fire on the first render. In my code, I am getting errors for not being able to use my States properties. This is because the State doesn't have the API data before the DOM renders. I guess I am just not understanding if I am missing something, but the console log shows that the questions array is empty twice before actually getting any data. If I am not trying to access the data anywhere, no data is thrown, but as soon as I try to use it, the code says that the data doesn't exist. I tried to use async but get the same error.
export default function Quiz(props) {
const [questions, setQuestions] = React.useState([])
React.useEffect(() => {
fetch("https://opentdb.com/api.php?amount=10")
.then(res => res.json())
.then(data => setQuestions(data.results))
}, [])
console.log(questions)
return (
<div>
<div className="quiz--question">
<h1>{questions.category}</h1>
</div>
</div>
)
}
questions is an array , so you need to iterate that to display the value. You can use map
import * as React from 'react';
import './style.css';
export default function App() {
const [questions, setQuestions] = React.useState([]);
React.useEffect(() => {
fetch('https://opentdb.com/api.php?amount=10')
.then((res) => res.json())
.then((data) => {
setQuestions(data.results);
console.log(questions);
});
}, []);
return (
<div>
{questions.map((elem) => (
<div className="quiz--question">
<h1>{elem.category}</h1>
</div>
))}
</div>
);
}
Demo Here

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: Implementing a hook to fetch data and return loading state

I'm trying to implement a hook (useFetch(route, dependencies=[])) which will be used to fetch data and return a scalar value, loading, which is used to determine if the user is currently fetching new data.
This works as expected on the initial render (as the default state for loading is true).
The problem occurs on every subsequent render. Due to the loading state being currently set to false (after the initial render), the loading state will remain false for an extra render cycle before it is updated to true at the top of the useEffect().
How can I prevent that extra loading=false render cycle, so the component I use this useFetch() in is immediately aware that data is currently being fetched and that I can display the '...loading' text in the render() (without having to render twice to get to this point).
useFetch
function useFetch(route, successHandler, dependencies = []) {
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(route,
{
method: 'POST',
body: JSON.stringify({}),
headers: {"Content-Type": "application/json"}
})
.then(res => res.json())
.then(
() => {
// Set state data from component here
successHandler();
},
() => {/*...*/})
.finally(() => {
setLoading(false);
});
}, dependencies);
return loading;
}
Component
function Component(){
const [toggle, setToggle] = useState(false);
const [options, setOptions] = useState({});
const loading = useFetch('endpoint',
() => setOptions({foo: 'bar'}),
[toggle]);
return <div>
<button onClick={()=>{setToggle(!toggle)}}>Trigger useFetch()</button>
{loading ? '...loading': options.foo}
</div>;
}
Any help or design suggestions would be appreciated,
Your code has two mistakes:
const loading = useFetch('endpoint',
() => setOptions({foo: 'bar'}),
[toggle]);
The dependency array should be the variables that are used in the useEffect, in your case it can be [route]
[toggle] as a dependency parameter will send [false] or [true], depending on your state variable. Which won't be under control of the useEffect of the hook you created.
Check out this example, it might help you understand better:

How to update a function with useEffect (like componentDidUpdate) and use .map function to display from JSON file ReactJS

I've been trying to update a function once data has been recieved and set to a state (using useState). After that the function will use the .map function display the data into a template.
However i am getting two errors, one is 'projects.map is not a function' (btw projects is my state name, where data is stored) and inside the useEffect function which updates when projects is changed 'Expected an assignment or function call and instead saw an expression'
import React, { useState, useEffect } from 'react';
import ProjectSummary from './projectSummary';
function ProjectList() {
// setting my state
const [projects, setProjects] = useState([])
// getting the data from some dummy online data when the app starts
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(data => setProjects({ data }))
}, []);
// makeing a function call postList, which stores a ternery operator
const postList = () => {
// The ternery operator asks if there is anything inside the porjects state
projects.length ? (
// If there is something in the state, it will map out the JSON array in the 'projectSummary template
projects.map(projects => {
return(
<div >
<ProjectSummary key={projects.id} title={projects.title} author={projects.userId} date='30 september, 2019' content={projects.body}/>
</div>
)
})
) : (
// If there isnt anything in the state is prints out 'Loading Data'
<h1>Loading Data</h1>
);
}
// useEffect updates when the 'projects' stae is updated (like componentDidUpdate, and runs the function again
useEffect(() => {
postList()
}, [projects]);
return(
<div className="ProjectList">
// The component should output the postList function, which should map out the array, in the template
{ postList }
</div>
)
}
export default ProjectList
You need to make some corrections to your component
import React, { useState, useEffect } from 'react';
import ProjectSummary from './projectSummary';
function ProjectList() {
const [projects, setProjects] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(data => setProjects(data))
}, []);
return(
<div className="ProjectList">
{
projects.length ?
projects.map(projects => (
<div>
<ProjectSummary key={projects.id} title={projects.title} author={projects.userId} date='30 september, 2019' content={projects.body} />
</div>
))
:
<h1>Loading Data</h1>
}
</div>
)
}
export default ProjectList;
You don't need the postList function and the second useEffect.
You may want to add additional checks to determine when the posts are loading and when they're empty after loading is done, so you don't just get a loading message
try this
const [projects, setProjects] = useState([])
and
.then(data => setProjects(data))
assuming that the data is an array
First you have to set the initial projects to an empty array like this:
const [projects, setProjects] = useState([])
, because currently projects is an empty string, which does not have the map function.
For the fetch, you should write it like this:
useEffect(async () => {
const data = await fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
setProjects(data);
}, []);
Hope this helps.
Array.prototype.map() is for Array.
const [projects, setProjects] = useState('');
projects not an array.

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