Debouncing with multiple hooks - javascript

I am trying to implement debouncing in my app, however, the most I am able to achieve is to debounce the speed of the input. The gist of the App, is that it first takes input from the user, to generate a list of cities, according to the input and when selected, will provide a forecast for the whole week.
Here is the original code, without debouncing:
import React, { useState, useEffect } from 'react';
import * as Style from './Searchbar.styles';
import { weatherAPI } from '../API/api';
import endpoints from '../Utils/endpoints';
import { minCharacters } from '../Utils/minChars';
import MultiWeather from './MultiCard';
import useDebounce from '../Hooks/useDebounce';
export default function SearchBar() {
const [cityName, setCityName] = useState('');
const [results, setResults] = useState([]);
const [chooseCityForecast, setChooseCityForecast] = useState([]);
useEffect(() => {
if (cityName.length > minCharacters) {
loadCities();
}
}, [cityName, loadCities]);
//this is used to handle the input from the user
const cityValueHandler = value => {
setCityName(value);
};
//first API call to get list of cities according to user input
const loadCities = async () => {
try {
const res = await weatherAPI.get(endpoints.GET_CITY(cityName));
setResults(res.data.locations);
} catch (error) {
alert(error);
}
};
//second API call to get the forecast
const getCurrentCity = async city => {
try {
const res = await weatherAPI.get(endpoints.GET_DAILY_BY_ID(city.id));
console.log(res);
setChooseCityForecast(res.data.forecast);
setCityName('');
setResults([]);
} catch (error) {
alert(error);
}
};
return (
<Style.Container>
<h1>Search by City</h1>
<Style.Search>
<Style.SearchInner>
<Style.Input type="text" value={cityName} onChange={e => cityValueHandler(e.target.value)} />
</Style.SearchInner>
<Style.Dropdown>
{cityName.length > minCharacters ? (
<Style.DropdownRow results={results}>
{results.map(result => (
<div key={result.id}>
<span
onClick={() => getCurrentCity(result)}
>{`${result.name}, ${result.country}`}</span>
</div>
))}
</Style.DropdownRow>
) : null}
</Style.Dropdown>
</Style.Search>
{chooseCityForecast && (
<section>
<MultiWeather data={chooseCityForecast} />
</section>
)}
</Style.Container>
);
}
The code above works perfectly, aside from creating an API call everytime I add an additional letter. I have refered to this thread on implementing debouncing. When adjusted to my code, the implementation looks like this:
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
const debouncedHandler = useCallback(debounce(cityValueHandler, 200), []);
But, as mentioned before, this results in debouncing/delaying user input by 200ms, while still creating additional API calls which each extra letter.
If I try to debounce the loadCities function or add a setTimeout method, it will delay the function, but will still make the API calls with each additional letter.
I have a hunch, that I need to remake the logic, which is handling the input, but at this point I am out of ideas.

After some digging I found a simple solution, that will be refactored later, but the gist of it is to move the loadCities function inside the use effect and implenet useTimeout and clearTimeout methods within the useEffect and wrapping the API call function, as seen below:
useEffect(() => {
let timerID;
if (inputValue.length > minCharacters) {
timerID = setTimeout(async () => {
try {
const res = await weatherAPI.get(endpoints.GET_CITY(inputValue));
setResults(res.data.locations);
} catch (error) {
alert(error);
}
}, 900);
}
return () => {
clearTimeout(timerID);
};
}, [inputValue]);
Hope this will help someone.

Related

How to avoid unnecessary API calls with useEffect?

I'm still beginner to ReactJS and I'm having trouble rendering a list.
I don't know why, all the time calls are being made to my API. Since I don't put any dependency on useEffect, that is, I should only render my function once.
I don't understand why this is happening. Can you tell me what I'm doing wrong?
Here's my code I put into codesandbox.io
import React from "react";
import axios from "axios";
import "./styles.css";
const App = () => {
const BASE_URL = "https://pokeapi.co/api/v2";
const [pokemons, setPokemons] = React.useState([]);
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
data.results.map((pokemon) => getPokeType(pokemon));
};
const getPokeType = async (pokemon) => {
const { data } = await axios.get(pokemon.url);
setPokemons((prev) => [...prev, data]);
};
React.useEffect(() => {
getAllPokemons();
}, []);
console.log(pokemons);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{pokemons.map((pokemon) => (
<p key={pokemon.id} style={{ color: "blue" }}>
{pokemon.name}
</p>
))}
</div>
);
};
export default App;
Thank you very much in advance.
Your issue is that you are calling setPokemons inside getPokeType (which is called for each data in part). Your useEffect is called just once (as expected) and the ${BASE_URL}/pokemon call is executed just once too. But getPokeType is called 20 times and the pokemons state is changed 20 times as well (once for each instance from data.results).
What I would recommend in your case (instead of what you have now) is:
Create a list of all the pokemons and
Set the state just once at the end.
So something like:
...
const getPokeType = async (pokemon) => {
const { data } = await axios.get(pokemon.url);
return data;
};
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
const pokemons = await Promise.all(
data.results.map((pokemon) => getPokeType(pokemon))
);
setPokemons(pokemons);
};
React.useEffect(() => {
getAllPokemons();
}, []);
...
I was just having the same issue in my project the way I solved is by moving the function definition inside the useEffect
React.useEffect(() => {
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
data.results.map((pokemon) => getPokeType(pokemon));
};
getAllPokemons();
}, []);
If this solves your problem please accept the answer.

React testing library how to use waitFor

I'm following a tutorial on React testing. The tutorial has a simple component like this, to show how to test asynchronous actions:
import React from 'react'
const TestAsync = () => {
const [counter, setCounter] = React.useState(0)
const delayCount = () => (
setTimeout(() => {
setCounter(counter + 1)
}, 500)
)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={delayCount}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestAsync
And the test file is like this:
import React from 'react';
import { render, cleanup, fireEvent, waitForElement } from '#testing-library/react';
import TestAsync from './TestAsync'
afterEach(cleanup);
it('increments counter after 0.5s', async () => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'))
const counter = await waitForElement(() => getByText('1'))
expect(counter).toHaveTextContent('1')
});
The terminal says waitForElement has been deprecated and to use waitFor instead.
How can I use waitFor in this test file?
If you're waiting for appearance, you can use it like this:
it('increments counter after 0.5s', async() => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'));
await waitFor(() => {
expect(getByText('1')).toBeInTheDocument();
});
});
Checking .toHaveTextContent('1') is a bit "weird" when you use getByText('1') to grab that element, so I replaced it with .toBeInTheDocument().
Would it be also possible to wrap the assertion using the act
function? Based on the docs I don't understand in which case to use
act and in which case to use waitFor.
The answer is yes. You could write this instead using act():
import { act } from "react-dom/test-utils";
it('increments counter after 0.5s', async() => {
const { getByTestId, getByText } = render(<TestAsync />);
// you wanna use act() when there is a render to happen in
// the DOM and some change will take place:
act(() => {
fireEvent.click(getByTestId('button-up'));
});
expect(getByText('1')).toBeInTheDocument();
});
Hope this helps.
Current best practice would be to use findByText in that case. This function is a wrapper around act, and will query for the specified element until some timeout is reached.
In your case, you can use it like this:
it('increments counter after 0.5s', async () => {
const { findByTestId, findByText } = render(<TestAsync />);
fireEvent.click(await findByTestId('button-up'))
const counter = await findByText('1')
});
You don't need to call expect on its value, if the element doesn't exist it will throw an exception
You can find more differences about the types of queries here

React Hooks multiple alerts with individual countdowns

I've been trying to build an React app with multiple alerts that disappear after a set amount of time. Sample: https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The problem is if I create multiple alerts, it disappears in the incorrect order. For example, test 0, test 1, test 2 should disappear starting with test 0, test 1, etc but instead test 1 disappears first and test 0 disappears last.
I keep seeing references to useRefs but my implementations don't resolve this bug.
With #ehab's input, I believe I was able to head down the right direction. I received further warnings in my code about adding dependencies but the additional dependencies would cause my code to act buggy. Eventually I figured out how to use refs. I converted it into a custom hook.
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
You have two things wrong with your code,
1) the way you use effect means that this function will get called each time the component is rendered, however obviously depending on your use case, you want this function to be called once, so change it to
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
adding the empty array as a second parameter, means that your effect does not depend on any parameter, and so it should only be called once.
Your delete alert depends on the value that was captured when the function was created, this is problematic since at that time, you don't have all the alerts in the array, change it to
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
here is your sample working after i forked it
https://codesandbox.io/s/multiple-alert-countdown-02c2h
well your problem is you remount on every re-render, so basically u reset your timers for all components at time of rendering.
just to make it clear try adding {Date.now()} inside your Alert components
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
you will notice the reset everytime
so to achieve this in functional components you need to use React.memo
example to make your code work i would do:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
2nd fix your useEffect to not run cleanup function on every render
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
finally something that is about taste, but really do you need to destruct the {...m} object ? i would pass it as a proper prop to avoid creating new object every time !
Both answers kind of miss a few points with the question, so after a little while of frustration figuring this out, this is the approach I came to:
Have a hook that manages an array of "alerts"
Each "Alert" component manages its own destruction
However, because the functions change with every render, timers will get reset each prop change, which is undesirable to say the least.
It also adds another lay of complexity if you're trying to respect eslint exhaustive deps rule, which you should because otherwise you'll have issues with state responsiveness. Other piece of advice, if you are going down the route of using "useCallback", you are looking in the wrong place.
In my case I'm using "Overlays" that time out, but you can imagine them as alerts etc.
Typescript:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
Then where the components are rendered:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
Basically: Each "overlay" has a unique ID. Each "overlay" component manages its own destruction, the overlay communicates back to the overlayManger via prop function, and then eslint exhaustive-deps is kept happy by setting an "alive" state property in the overlay component that, when changed to false, will call for its own destruction.

react-select debounced async call not displaying suggestions

I'm using react-select loading the results from an api and debouncing the queries with lodash.debounce:
import React, {useState} from 'react';
import AsyncSelect from 'react-select/lib/Async';
import debounce from 'lodash.debounce';
import {search} from './api';
const _loadSuggestions = (query, callback) => {
return search(query)
.then(resp => callback(resp));
};
const loadSuggestions = debounce(_loadSuggestions, 300);
function SearchboxTest() {
const [inputValue, setInputValue] = useState("");
const onChange = value => {
setInputValue(value);
};
return (
<AsyncSelect
value={inputValue}
loadOptions={loadSuggestions}
placeholder="text"
onChange={onChange}
/>
)
}
It seems to work fine when I enter something in the searchbox for the first time (query is debounced and suggestions populated correctly), but if I try to enter a new value, a second fetch query is fired as expected, but the suggestions coming from that second call are not displayed (and I don't get the "Loading..." message that react-select displays).
When I don't debounce the call the problem seems to go away (for the first and any subsequent calls):
import React, {useState} from 'react';
import AsyncSelect from 'react-select/lib/Async';
import {search} from '../../api';
const loadSuggestions = (query, callback) => {
return search(query)
.then(resp => callback(resp));
};
function SearchboxTest() {
const [inputValue, setInputValue] = useState("");
const onChange = value => {
setInputValue(value);
};
return (
<AsyncSelect
value={inputValue}
loadOptions={loadSuggestions}
placeholder="text"
onChange={onChange}
/>
)
}
Any idea what is going on? Any help to understand this issue would be much appreciated.
M;
I was aslo facing the same issue and got this solution.
If you are using callback function, then You DON'T need to return the
result from API.
Try removing return keyword from _loadSuggestions function.
as shown below
import React, {useState} from 'react';
import AsyncSelect from 'react-select/lib/Async';
import debounce from 'lodash.debounce';
import {search} from './api';
const _loadSuggestions = (query, callback) => {
search(query)
.then(resp => callback(resp));
};
const loadSuggestions = debounce(_loadSuggestions, 300);
function SearchboxTest() {
const [inputValue, setInputValue] = useState("");
const onChange = value => {
setInputValue(value);
};
return (
<AsyncSelect
value={inputValue}
loadOptions={loadSuggestions}
placeholder="text"
onChange={onChange}
/>
)
}
Use react-select-async-paginate package - This is a wrapper on top of react-select that supports pagination. Check it's NPM page
React-select-async-paginate works effectively with internal denounce. You can pass debounce interval in the props.
<AsyncPaginate
value={value}
loadOptions={loadOptions}
debounceTimeout={300}
onChange={setValue}
/>
Here is codesandbox example
For all of you people still struggling with this and don't want to install lodash only for debounce function like I do, here's my solution
debounce function
export default function debounce(fn, delay = 250) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn(...args);
}, delay);
};
}
debounce implementation
const loadOptionsDebounced = useCallback(
debounce((inputValue: string, callback: (options: any) => void) => {
fetchOptions(inputValue).then(options => callback(options))
}, 500),
[]
);
fetchOptions() is my async fetch function that returns array of options
Maybe this will help someone.
freeze = false //mark delay
timer //saved timer
loadAddress = async (strSearch: string) => {
this.freeze = true //set mark for stop calls
return new Promise(async (res, err) => { //return promise
let p = new Promise((res, err) => {
if(this.freeze) clearTimeout(this.timer) //remove prev timer
this.timer = setTimeout(async () => {
this.freeze = false
const r = await this.load(strSearch)//request
res(r);
}, 2000)
})
p.then(function (x) {
console.log('log-- ', x);
res(x);
})
});
};
What worked for me was, instead of making use of the default debounce from lodash, making use of the debounce-promise package:
import React from 'react';
import debounce from 'debounce-promise';
import AsyncSelect from 'react-select/async';
const debounceFunc = debounce(async () => {
return [] // SelectOptions
}, 200);
export function MyComponent() {
return <AsyncSelect loadOptions={debounceFunc} defaultOptions />
}
In my solution help me following.
I have AsyncSelect where i want data from https://github.com/smeijer/leaflet-geosearch. My provider is:
const provider = new OpenStreetMapProvider({
params: {
countrycodes: "cz",
limit: 5
}
});
const provider you will see below one more time.
<AsyncSelect className="map-search__container"
classNamePrefix="map-search"
name="search"
cacheOptions
components={{DropdownIndicator, IndicatorSeparator, Control, NoOptionsMessage, LoadingMessage}}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
loadOptions={getData}
onChange={handleChange}
isClearable
placeholder={inputSave || ""}
value=""
inputValue={input}
onMenuClose={handleMenuClose}
onInputChange={handleInputChange}
onFocus={handleFocus}
blurInputOnSelect
defaultOptions={true}
key={JSON.stringify(prevInputValue)}
/>
cachceOptions - is basic
components - my components
loadOptions - is the most important!
anything else is not important
const getData = (inputValue: string, callback: any) => {
debouncedLoadOptions(inputValue, callback);
}
const debouncedLoadOptions = useDebouncedCallback(fetchData, 750);
import {useDebouncedCallback} from 'use-debounce';
This use was my key for solution. I tried debounce from lodash, but i had still problem with not working debouncing. This use help me and did my problem solve.
const fetchData = (inputValue: string, callback: any) => {
return new Promise((resolve: any) => {
resolve(provider.search({query: inputValue || prevInputValue}));
}).then((json) => {
callback(json);
});
};
prevInputValue is props from parent... (you don't need it)
Callbacks was second key. First was useDebouncedCallback.
#sorryForMyEnglish

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