useEffect re-renders more times than I would like - javascript

//Fetch method:
import { useEffect, useState } from "react";
export const useFetch = (url) => {
const [state, setState] = useState({ data: null });
useEffect(() => {
setState((state) => ({ data: state.data }));
fetch(url)
.then((res) => res.json())
.then((json) => {
setState({ data: json });
});
}, [url]);
return state;
};
//My actual code:
function AdminDashboard() {
const { data } = useFetch(
//GET data
"https://jsonplaceholder.typicode.com/posts"
);
console.log(data)
The console prints my data 3 times. The first and second it prints null, then it prints the actual data.

Related

How can i test custom fetch hook

I am struggling with an issue with the custom fetch hook.Simply i am trying to test my fetch hook if the data already fetched the hook needs to get data from cache instead of api.
The test case fails and looks like caching mechanism not working, but if i try on the browser with manual prop change caching mechanism works properly.
import { render, waitFor } from "#testing-library/react";
const renderList = (filterParams = testFilterParamsList[0]) =>
render(<List filterParams={filterParams} />);
it("should re-render without fetch", async () => {
const { rerender } = renderList(testFilterParamsList[0]);
rerender(<List filterParams={testFilterParamsList[1]} />);
expect(window.fetch).toHaveBeenCalledTimes(1);
});
// useFetch.js
import {useEffect, useReducer} from "react";
const cache = {};
const FETCH_REQUEST = "FETCH_REQUEST";
const FETCH_SUCCESS = "FETCH_SUCCESS";
const FETCH_ERROR = "FETCH_SUCCESS";
const INITIAL_STATE = {
isPending: false,
error: null,
data: [],
};
const useFetch = ({url, filterOptions}) => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case FETCH_REQUEST:return {...INITIAL_STATE, isPending: true};
case FETCH_SUCCESS: return {...INITIAL_STATE, isPending: false, data: action.payload};
case FETCH_ERROR: return {...INITIAL_STATE, isPending: false, error: action.payload};
default: return state;
}
}, INITIAL_STATE);
useEffect(() => {
const fetchData = async () => {
dispatch({type: FETCH_REQUEST});
if (cache[url]) {
const data = cache[url];
dispatch({type: FETCH_SUCCESS, payload: data});
} else {
try {
const response = await window.fetch(url);
let data = await response.json();
cache[url] = data
dispatch({type: FETCH_SUCCESS, payload: data});
} catch (err) {
dispatch({type: FETCH_ERROR, payload: err});
}
}
};
fetchData();
}, [filterOptions, url]);
return state;
};
export default useFetch;
// List.js
import useFetch from "../hooks/useFetch";
export const RocketsList = ({ filterParams }) => {
const { isPending, error, data } = useFetch({
url: "https://api.spacexdata.com/v3/launches/past",
name:filterParams.name,
});
return (
<div>
Doesn't matter
</div>
);
};

Updating useReducer state from another function - React Pagination

I'm making a movie and I want to add pagination in the MoviesFromGenre component. Initially, MoviesFromGenre gets rendered based on the id from GenresList.
I want to add Pagination but I don't know how to update the state in useReducer when I click next/prev buttons?
import React, { useEffect, useReducer, useState } from 'react';
import { useParams } from 'react-router-dom'
import axios from 'axios';
import { API_URL, API_KEY } from '../api/config.js';
import Movies from './Movies'
const initialState = {
loading: true,
error: '',
movies: []
};
const reducer = (state, action ) => {
switch(action.type) {
case 'FETCH_SUCCESS':
return {
loading: false,
movies: action.payload,
error: '',
};
case 'FETCH_ERROR':
return {
loading: false,
movies: [],
error: 'Error'
};
default:
return state;
}
};
function MoviesFromGenre () {
const [state, dispatch] = useReducer(reducer, initialState);
const { id } = useParams();
const [pageNumber, setPageNumber] = useState(1)
useEffect(() => {
axios
.get(
`${API_URL}discover/movie?api_key=${API_KEY}&language=en-US&with_genres=${id}`
)
.then(response => {
dispatch({
type: 'FETCH_SUCCESS',
payload: response.data
})
})
.catch(err => {
dispatch({
type: 'FETCH_ERROR'
})
})
}, [])
const nextPage = () => {
axios
.get(
`${API_URL}discover/movie?api_key=${API_KEY}&language=en-US&with_genres=${id}&page=${pageNumber}`
)
.then(response => {
console.log(response.data)
})
setPageNumber(pageNumber+1)
}
const prevPage = () => {
axios
.get(
`${API_URL}discover/movie?api_key=${API_KEY}&language=en-US&with_genres=${id}&page=${pageNumber}`
)
.then(response => {
console.log(response.data)
})
setPageNumber(pageNumber-1)
}
return (
<div>
<Movies state={state}/>
<button onClick={prevPage}>Prev</button>
<button onClick={nextPage}>Next</button>
</div>
)
}
export default MoviesFromGenre;
I created a repository on GitHub.
I want to update the movies state when I click on next or prev buttons.
A Reddit user managed to solve my problem.
He suggested that I include pageNumber as a dependency in my useEffect hook, so it will run whenever pageNumber changes.
function MoviesFromGenre () {
const [state, dispatch] = useReducer(reducer, initialState);
const { id } = useParams();
const [pageNumber, setPageNumber] = useState(1)
useEffect(() => {
axios
.get(
`${API_URL}discover/movie?api_key=${API_KEY}&language=en-US&with_genres=${id}&page=${pageNumber}`
)
.then(response => {
dispatch({
type: 'FETCH_SUCCESS',
payload: response.data
})
})
.catch(err => {
dispatch({
type: 'FETCH_ERROR'
})
})
}, [pageNumber])
const nextPage = () => {
setPageNumber(pageNumber+1)
}
const prevPage = () => {
setPageNumber(pageNumber-1)
}
//....
}

change variable value with axios, useeffect, and usestate

i'm newbie here, i'm stuck. i want to change value from false to true, to stop shimmering when data sucessfully to load.
i have action like this
import axios from "axios";
import { CONSTANT_LINK } from "./constants";
import { GET } from "./constants";
import { ERROR } from "./constants";
import { connect } from 'react-redux';
export const addData = () => {
return (dispatch) => {
axios
.get(CONSTANT_LINK)
.then((res) => {
dispatch(addDataSuccess(res.data));
})
.catch((err) => {
dispatch(errorData(true));
console.log("error");
});
};
};
const addDataSuccess = (todo) => ({
type: GET,
payload: todo,
});
const errorData = (error) => ({
type: ERROR,
payload: error,
});
and this is my homepage which influential in this matter
const [shimmerValue, setShimmerValue] = useState(false)
useEffect(() => {
setShimmerValue(true)
dispatch(addData());
}, []);
<ShimmerPlaceholder visible={shimmerValue} height={20}>
<Text style={styles.welcomeName}>Welcome,Barret</Text>
</ShimmerPlaceholder>
i dont understand how it works
You can pass callback like this
const [shimmerValue, setShimmerValue] = useState(false);
const updateShimmerValue = () => {
setShimmerValue(true);
}
useEffect(() => {
// setShimmerValue(true) // remove this from here
dispatch(addData(updateShimmerValue)); // pass callback as param here
}, []);
Callback call here like
export const addData = (callback) => {
return (dispatch) => {
axios
.get(CONSTANT_LINK)
.then((res) => {
....
callback(); // trigger callback like this here
})
.catch((err) => {
....
});
};
};
you can use it:
const [shimmerValue, setShimmerValue] = useState(false)
useEffect(() => {
setState(state => ({ ...state, shimmerValue: true }));
dispatch(addData());
}, [shimmerValue]);

How to fetch data from a custom React hook (API) with onClick and display it in a Div using TypeScript?

I'm new to TypeScript and even React Hooks. I'm trying to fetch data continuously from a custom hook called useFetch(), which takes in a string (the URL) parameter:
import { useEffect, useState } from "react";
export interface Payload {
data: null | string,
loading: boolean
}
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
useEffect(() => {
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
}, [url, setState])
return state;
}
export default useFetch;
I import that hook into App():
import React, { useState, useEffect } from "react";
import useFetch from './utils/useFetch'
import {Payload} from './utils/useFetch';
const App = () => {
const [quote, setquote] = useState<Payload>({data: null, loading: true})
const handleClick = () => setquote(data)
const data = useFetch(
"/api/rand"
);
return (
<div>
<h1>Quotes</h1>
<button onClick={handleClick}>Get Quote</button>
<div>{quote.data}</div>
</div>
)
}
export default App;
On the first time the app loads, it works. I get data (a quote) when I click the button.
However, when I click it multiple times, the API isn't called again, and new data doesn't come through. I believe I'm supposed to use the useEffect() react hook and maintain the state (maybe I'm mistaken) but all my attempts so far have been to no avail. Any help would be much appreciated. If I figure it out, I'll definitely answer this question. Thanks!
It seems like you are mixing two ideas a little bit here.
Do you want to fetch data continuously? (Continuously meaning all the time, as in, periodically, at an interval)
Or do you want to fetch data upon mount of the component (currently happens) AND when the user taps the "Get Quote" button (not working currently)?
I will try to help you with both.
Continuously / periodically
Try changing the setup a bit. Inside the useFetch do something like:
import { useEffect, useState } from "react";
export interface Payload {
data: null | string,
loading: boolean
}
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
useEffect(() => {
const interval = setInterval(() => {
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
}, 2000); // Something like 2s
return () => {
clearInterval(interval); // clear the interval when component unmounts
}
}, [url])
return state;
}
export default useFetch;
This will fetch the endpoint every 2s and update the state variable, this will then be reflected in the App (or any place that uses the hook for that matter).
Therefore you do not need the quote state anymore in the App. Also, you don't need a button in the UI anymore.
App will looks something like:
import React, { useState, useEffect } from "react";
import useFetch from './utils/useFetch'
import {Payload} from './utils/useFetch';
const App = () => {
const quote = useFetch(
"/api/rand"
);
return (
<div>
<h1>Quotes</h1>
<div>{quote.data}</div>
</div>
)
}
export default App;
Fetch data upon mount of the component (currently happens) AND when the user taps the "Get Quote" button
Then the useFetch hook isn't really needed/suited in my opinion. A hook can be used for this application, but transforming it into a more simple function would make more sense to me. I suggest omitting the useFetch hook. The App component would look something like:
import React, { useState, useEffect, useCallback } from "react";
const App = () => {
const [quote, setQuote] = useState<Payload>({data: null, loading: true})
const handleGetQuote = useCallback(() => {
setQuote(state => ({ data: state.data, loading: true }));
fetch("/api/rand")
.then(x => x.text())
.then(y => {
setQuote({ data: y, loading: false });
});
}, []);
useEffect(() => {
handleGetQuote();
}, [handleGetQuote]);
return (
<div>
<h1>Quotes</h1>
<button onClick={handleGetQuote}>Get Quote</button>
<div>{quote.data}</div>
</div>
)
}
export default App;
The problem here is, that you are not refetching the quotes.
const useFetch = (url:string) => {
const [loading, setLoading] = useState(false);
const [quote, setQuote] = useState('');
const refetch = useCallback(() => {
setLoading(true);
fetch(url)
.then((x) => x.text())
.then((y) => {
//setQuote(y);
setQuote(String(Math.random()));
setLoading(false);
});
}, [url]);
return { quote, refetch, loading };
};
const App = () => {
const { quote, refetch, loading } = useFetch('/api/rand');
useEffect(() => {
refetch();
}, []);
return (
<div>
<h1>Quotes</h1>
<button onClick={refetch}>Get Quote</button>
{loading ? ' loading...' : ''}
<div>{quote}</div>
</div>
);
};
See this example
https://stackblitz.com/edit/react-ts-kdyptk?file=index.tsx
Here's another possibility: I added a reload function to the return values of useFetch. Also, since the useFetch hook already contains its own state, I removed the useState from App
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
const [reloadFlag, setReloadFlag] = useState(0)
useEffect(() => {
if(reloadFlag !== 0){ // remove this if you want the hook to fetch data initially, not just after reload has been clicked
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
}
}, [url, setState, reloadFlag])
return [state, ()=>setReloadFlag((curFlag)=>curFlag + 1)];
}
const App = () => {
const [data, reload] = useFetch(
"/api/rand"
);
return (
<div>
<h1>Quotes</h1>
<button onClick={reload}>Get Quote</button>
<div>{quote.data}</div>
</div>
)
}
Just change the code as below:
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
return state;
}
export default useFetch;

Redux + Hooks useDispatch() in useEffect calling action twice

I'm beginner in redux & hooks. I am working on form handling and trying to call an action through useDispatch hooks but it is calling my action twice.
I'm referring this article.
Here is the example:
useProfileForm.js
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchProfile } from '../../../redux/profile/profile.actions';
const useProfileForm = (callback) => {
const profileData = useSelector(state =>
state.profile.items
);
let data;
if (profileData.profile) {
data = profileData.profile;
}
const [values, setValues] = useState(data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProfile());
}, [dispatch]);
const handleSubmit = (event) => {
if (event) {
event.preventDefault();
}
callback();
};
const handleChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
return {
handleChange,
handleSubmit,
values,
}
};
export default useProfileForm;
Action
export const FETCH_PROFILE_BEGIN = "FETCH_PROFILE_BEGIN";
export const FETCH_PROFILE_SUCCESS = "FETCH_PROFILE_SUCCESS";
export const FETCH_PROFILE_FAILURE = "FETCH_PROFILE_FAILURE";
export const ADD_PROFILE_DETAILS = "ADD_PROFILE_DETAILS";
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
function getProfile() {
return fetch("url")
.then(handleErrors)
.then(res => res.json());
}
export function fetchProfile() {
return dispatch => {
dispatch(fetchProfileBegin());
return getProfile().then(json => {
dispatch(fetchProfileSuccess(json));
return json;
}).catch(error =>
dispatch(fetchProfileFailure(error))
);
};
}
export const fetchProfileBegin = () => ({
type: FETCH_PROFILE_BEGIN
});
export const fetchProfileSuccess = profile => {
return {
type: FETCH_PROFILE_SUCCESS,
payload: { profile }
}
};
export const fetchProfileFailure = error => ({
type: FETCH_PROFILE_FAILURE,
payload: { error }
});
export const addProfileDetails = details => {
return {
type: ADD_PROFILE_DETAILS,
payload: details
}
};
Reducer:
import { ADD_PROFILE_DETAILS, FETCH_PROFILE_BEGIN, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from './profile.actions';
const INITIAL_STATE = {
items: [],
loading: false,
error: null
};
const profileReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ADD_PROFILE_DETAILS:
return {
...state,
addProfileDetails: action.payload
}
case FETCH_PROFILE_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_PROFILE_SUCCESS:
return {
...state,
loading: false,
items: action.payload.profile
};
case FETCH_PROFILE_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
export default profileReducer;
**Component:**
import React from 'react';
import { connect } from 'react-redux';
import useProfileForm from './useProfileForm';
import { addProfileDetails } from '../../../redux/profile/profile.actions';
const EducationalDetails = () => {
const { values, handleChange, handleSubmit } = useProfileForm(submitForm);
console.log("values", values);
function submitForm() {
addProfileDetails(values);
}
if (values) {
if (values.error) {
return <div>Error! {values.error.message}</div>;
}
if (values.loading) {
return <div>Loading...</div>;
}
}
return (
<Card>
...some big html
</Card>
)
}
const mapDispatchToProps = dispatch => ({
addProfileDetails: details => dispatch(details)
});
export default connect(null, mapDispatchToProps)(EducationalDetails);
Also when I'm passing data from const [values, setValues] = useState(data); useState to values then ideally I should receive that in component but I'm not getting as it is showing undefined.
const { values, handleChange, handleSubmit } = useProfileForm(submitForm);
values is undefined
The twice dispatch of action is probably because you have used React.StrictMode in your react hierarchy.
According to the react docs, in order to detect unexpected sideEffects, react invokes a certain functions twice such as
Functions passed to useState, useMemo, or useReducer
Now since react-redux is implemented on top of react APIs, actions are infact invoked twice
Also when I'm passing data from const [values, setValues] = useState(data); useState to values then ideally I should receive that in component but I'm not getting as it is showing undefined.
To answer this question, you must know that values is not the result coming from the response of dispatch action from reducer but a state that is updated when handleChange is called so that is supposed to remain unaffected by the action
I think you mean to expose the redux data from useProfileForm which forgot to do
const useProfileForm = (callback) => {
const profileData = useSelector(state =>
state.profile.items
);
let data;
if (profileData.profile) {
data = profileData.profile;
}
const [values, setValues] = useState(data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProfile());
}, [dispatch]);
const handleSubmit = (event) => {
if (event) {
event.preventDefault();
}
callback();
};
const handleChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
return {
handleChange,
handleSubmit,
values,
data // This is the data coming from redux store on FetchProfile and needs to logged
}
};
export default useProfileForm;
You can use the data in your component like
const { values, handleChange, handleSubmit, data } = useProfileForm(submitForm);

Categories

Resources