I´m new to react. I´m trying to fetch an endpoints array. and I want to update the api's status every 15 seconds. I´m trying to do this
export const endpoints: string[] = [
"accounts/health/status",
"assets/health/status",
"customers/health/status",
"datapoints/health/status",
"devices/health/status",
"documents/health/status",
"forms/health/status",
"invites/health/status",
"media/health/status",
"messages/health/status",
"namespaces/health/status",
"orders/health/status",
"patients/health/status",
"relationships/health/status",
"rules/health/status",
"templates/health/status",
"users/health/status",
"workflows/health/status",
];
and I have this proxy in my package.json
"proxy": "https://api.factoryfour.com/",
Here the rest of my code
const [data, setData] = useState<Response[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string[] | null[]>([]);
const effectRan = useRef(false);
const fetching = async () => {
setLoading(true);
endpoints.map(async (endpoint) => {
return await axios
.get(endpoint)
.then((res) => {
setData((prev) => [...prev, res.data]);
})
.catch((err) => {
setError([...error, err.message]);
});
});
setLoading(false);
};
useEffect(() => {
if (!effectRan.current) {
fetching();
}
return () => {
effectRan.current = true;
};
});
useEffect(() => {
setTimeout(async () => {
setData([]);
setLoading(true);
setError([]);
await fetching();
}, 15000);
}, []);
but when the seTimeout runs every card duplicates and the state gets more data than before. even though I´m reseting the state to setData([]) I just want to update the api's status. What can i do?
if (loading) return <Spinner />;
return (
<div className="card-container">
{data.length ? (
data.map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{error.length
? error.map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>
```
Theres a few things wrong here and one or more probably fixes it:
You keep a ref around to track the first fetch but theres no need as you can do that by virtue of using [] in an effects deps array, which you already have.
The loading state does not wait until all requests in flight finished.
The 15 second interval does not wait until all requests launched are finished.
You dont clear down the timer if the component unmounts and remounts.
The data is not keyed against the endpoint which could land you in trouble if using React strictmode that runs affects twice in dev mode.
Your code, by design it seems, does append data each time one of the requests comes back -- but I think that was intentional?
const [data, setData] = useState<Record<string, Response>>({});
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Record<string, string | null>>({});
const fetching = async () => {
setLoading(true);
await Promise.all(
endpoints.map((endpoint) => {
return axios
.get(endpoint)
.then((res) => {
setData((prev) => ({...prev, [endpoint]: res.data}));
})
.catch((err) => {
setError((prev) => ({...prev, [endpoint]: err.message}));
});
})
);
setLoading(false);
};
useEffect(() => {
let timer: number | null = null;
const intervalFetch = async () => {
await fetching();
timer = setTimeout(async () => {
setError({});
setData({});
intervalFetch();
}, 15000);
};
intervalFetch();
return () => timer !== null && clearTimeout(timer);
}, []);
if (loading) return <Spinner />;
return (
<div className="card-container">
{Object.values(data).length ? (
Object.values(data).map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{Object.values(error).length
? Object.values(error).map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>)
I think this piece of code might be adding additional data instead of overwriting the existing one. Is that what you're trying to do?
setData((prev) => [...prev, res.data]);
Related
I'm trying to learn ReactJS..
Today I was trying to create an array of objects with fetch results and after that create the cards, but I just can update the state but the cards are not re-render.. can you help me?
App.js
const teamsForLoop = [
Team1,
Team2,
Team3
];
const [allPlayers, setAllPlayers] = useState([]);
const [team, setTeam] = useState([]);
const [allTeams] = useState(teamsForLoop);
const [loading, setLoading] = useState(true);
useEffect(() => {
const playerInfo = async() => {
setLoading(true)
allTeams.map(async(teamArray) => {
setTeam([])
teamArray.map(async (player) => {
let playerName = player.split(" ");
const result = await axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${playerName[0]}%20${playerName[1]}`
);
if (result.data.player === null) {
setTeam((state) => {
return [...state];
});
} else {
setTeam((state) => {
return [...state, result.data.player[0]];
});
}
});
setAllPlayers(team);
});
setLoading(false);
};
playerInfo();
},[]);
if (loading) return "...Loading...";
return (
<>
<PlayerList allPlayers={allPlayers} />
</>
);
}
export default App;
PlayerList.js
function PlayerList({ allPlayers }) {
const myData = []
.concat(allPlayers)
.sort((a, b) => (a.idTeam !== b.idTeam ? 1 : -1))
return (
<div>
{myData.map((player,index) =>
(
<div key={index}>
...........
</div>
)
)}
</div>
);
}
I think my problem was on the useEffect hook or maybe on my fetch function..
I already have done it using just arrays but without state.
Issue
The issue I see now is that you are attempting to cache the fetched players in the team state in the loops and then use the team state to update the players state. The problem here is that React state updates are asynchronously processed, so team hasn't updated when setAllPlayers(team); is called.
Solution
It would be simpler to map the allTeams arrays to the GET requests, wait for them to resolve, and enqueue a single allPlayers state update. Flatten the arrays of team's players and map these to the axios GET Promise. Wait for these to resolve and map the results to the array of players.
Example:
function App() {
const [allPlayers, setAllPlayers] = useState([]);
const [allTeams] = useState(teamsForLoop);
const [loading, setLoading] = useState(true);
const playerInfo = async () => {
setLoading(true);
const response = await Promise.all(
allTeams
.flat()
.map((player) =>
axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${player}`
)
)
);
const players = response.map((result) => result.data.player[0]);
setAllPlayers(players);
setLoading(false);
};
useEffect(() => {
playerInfo();
}, []);
if (loading) return "...Loading...";
return <PlayerList allPlayers={allPlayers} />;
}
I am trying to call a component that shows the details of a notification when the notification is clicked. However, I kept on getting an error of too many re-renders.
This is my Notifications code
This component calls the database to get the list of notifications and then sets the first notification as the default notification clicked.
const Notification = (hospital) => {
const [users, setUsers] = useState([]);
const [search, setSearch] = useState(null);
const [status, setStatus] = useState(null);
const [notifDetails, setNotification] = useState();
useEffect(async () => {
await axios
.get("/notifications")
.then((res) => {
const result = res.data;
setUsers(result);
setNotification(result[0]);
})
.catch((err) => {
console.error(err);
});
}, []);
return(
<div className="hospital-notif-container">
{filteredList(users, status, search).map((details, index) => {
for (var i = 0; i < details.receiver.length; i++) {
if (
(details.receiver[i].id === hospital.PK ||
details.receiver[i].id === "others") &&
details.sender.id !== hospital.PK
) {
return (
<div
className="hospital-notif-row"
key={index}
onClick={() => setNotification(details)}
>
<div className="hospital-notif-row">
{details.name}
</div>
</div>
);
}
}
return null;
})}
</div>
<NotificationDetails details={notifDetails} />
);
}
For NotificationDetails:
This function is triggered when a notification is clicked from Notifications. The error is said to be coming from this component.
const NotificationDetails = ({ details }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
if (Object.keys(details).length != 0) {
setLoading(false);
}
}, [details]);
if (!loading) {
return (
<>
<div className="hospital-details-container">
<h2>{details.sender.name}</h2>
</div>
</>
);
} else {return (<div>Loading</div>);}
};
What should I do to limit the re-render? Should I change the second argument of the useEffects call? Or am I missing something in my component?
I tried calling console.log from NotificationDetails and it shows that it is infinitely rendering with the data I set in axios which is result[0]. How is this happening?
Your problem should be in NotificationDetails rendering. You should write something like:
const NotificationDetails = ({ details }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
if (details.length != 0) {
setLoading(false);
}
}, [details]);
return (
<div>
{loading &&
<div className="hospital-details-container">
<div className="hospital-details-header">
<h2>{details.sender.name}</h2>
</div>
</div>
}
{!loading &&
<div>
<ReactBootStrap.Spinner animation="border" />
</div>
}
</div>
);
}
With return outside the condition statement.
EDIT
Now I noted that you have an async useEffect that is an antipattern. You should modify your useEffect in this way:
useEffect(() => {
(async () => {
await axios
.get("/notifications")
.then((res) => {
const result = res.data;
setUsers(result);
setNotification(result[0]);
})
.catch((err) => {
console.error(err);
});
})()
}, []);
Alternating between the 2 buttons will display first names or last names, but pressing them together really fast will chain requests and will combine the two. How can I make create a check, and only display the names from the button that was pressed last
export default function App() {
const [name, setName] = useState();
return (
<div className="App">
<button onClick={() => setName("first_name")}>1</button>
<button onClick={() => setName("last_name")}>2</button>
<Users name={name} />
</div>
);
}
export default function Users({ name }) {
const [users, setUsers] = useState([]);
useEffect(() => {
setUsers([]);
axios({
method: "GET",
url: `https://reqres.in/api/users?delay=1`
})
.then((res) => {
const allUsers = res.data.data.map((user) => <p>{user[name]}</p>);
setUsers((prev) => [...prev, ...allUsers]);
})
.catch((e) => {
console.log(e);
});
}, [name]);
return <div className="Users">{users}</div>;
}
Here is a great article by Dan Abramov about the useEffect hook in which he also talks about how to handle race cases- https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions
To solve your issue, create a variable like let didCancel = false at the start of useEffect. Then, you have to return a function from useEffect, which automatically runs at the time when the name changes next time. In that function set didCancel to true. Now, you have to handle fetch response only if didCancel is false. This way, you are discarding all fetch responses received from second-last, third-last, etc. button presses, and handling fetch response only from the last button press.
Here is updated useEffect code:-
useEffect(() => {
let didCancel = false;
setUsers([]);
axios({
method: "GET",
url: `https://reqres.in/api/users?delay=1`
})
.then((res) => {
if (!didCancel) {
const allUsers = res.data.data.map((user) => <p>{user[name]}</p>);
setUsers((prev) => [...prev, ...allUsers]);
}
})
.catch((e) => {
console.log(e);
});
return () => {
didCancel = true;
};
}, [name]);
return <div className="Users">{users}</div>;
}
you have to create a loading state, and the user should not be able to send a new request until the data is received... you can create a hook for this or use SWR:
let me give you an example:
function Users(usersList) {
return (
<ul>
{usersList.map((user, key) => (
<li key={key}>{user}</li>
))}
</ul>
);
}
const useFetchUsers = (name) => {
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [data, setData] = React.useState([]);
React.useEffect(() => {
setIsLoading(true);
setError(null);
fetch('https://blahblahblah.com/api/users')
.then((res) => res.json())
.then((response) => setData(response))
.catch((err) => setError(err))
.finally(() => setIsLoading(false));
}, [name]);
return {
isLoading,
error,
data,
};
};
function App() {
const [name, setName] = React.useState('Tom');
const { isLoading, error, data } = useFetchUsers(name);
const handleSubmitName = (name) => {
if (isLoading) alert('wait!');
else setName(name);
};
if (error) return <>an error occured</>;
if (data)
return (
<>
<button onClick={() => handleSubmitName('first_name')}>1</button>
<button onClick={() => handleSubmitName('last_name')}>2</button>
<Users name={name} />
</>
);
}
hint/note: it's just pseudocode and there are some tools to do data fetching + caching.
The problem is in this line setUsers((prev) => [...prev, ...allUsers]);. You are assuming that prev is [], but when the second request is resolve prev has data, that is why you see the request are combined:
I recommend to change your useEffect block to avoid the problem you are facing:
useEffect(() => {
axios({
method: "GET",
url: `https://reqres.in/api/users?delay=1`
})
.then((res) => {
const allUsers = res.data.data.map((user) => <p>{user[name]}</p>);
setUsers(...allUsers); //--> with the last name's value
})
.catch((e) => {
console.log(e);
});
}, [name]);
My Functional component is as follows:
const Scratch = () => {
const [isLoaded, setIsLoaded] = useState(false);
const colorSelectItems=[];
const [selectedColor, setSelectedColor] = useState("fffff");
useEffect(() => {
fetch(
`http://localhost:8765/fetchData?userId=1`
)
.then((response) => response.json())
.then((data) => {
createDropDown(data));
setIsLoaded(true);
});
}, []);
const createDropDown= (data) => {
data.map((color) => {
colorSelectItems.push({
label: color.colorName,
value: color.hexValue,
});
});
return (
<div className="commonMargin">
{!isLoaded&& <p>Loading..</p>}
{isLoaded&& (
<Dropdown
value={selectedColor}
optionLabel="label"
options={colorSelectItems}
onChange={(e) => setSelectedColor(e.target.value);}
/>
)}
</div>
);
};
export default Scratch;
The problem is, it is displaying Loading... until the API call is complete, and it is rendering DropDown after that. But even after the completion of API call, the DropDown is still empty!
What am I missing here?
PS: This DropDown works perfectly if I replace fetching data from API to fetching data from local json file
Try this .In case any problem plz reply
const Scratch = () => {
const [isLoaded, setIsLoaded] = useState(false);
const colorSelectItems=[];
const [selectedColor, setSelectedColor] = useState("fffff");
useEffect(() => {
fetch(
`http://localhost:8765/fetchData?userId=1`
)
.then((response) => response.json())
.then((data) => {
var temp=data?.map((item)=>({label: item?.colorName,
value: item?.hexValue }));
colorSelectItems=temp;
setIsLoaded(true);
});
}, []);
return (
<div className="commonMargin">
{!isLoaded&& <p>Loading..</p>}
{isLoaded&& (
<Dropdown
value={selectedColor}
optionLabel="label"
options={colorSelectItems}
onChange={(e) => setSelectedColor(e.target.value);}
/>
)}
</div>
);
};
export default Scratch;
I honestly have no idea what is going on here. I have this code, on first render it should fetch popular repos and set them to the repos state, which should cause a re-render and paint the new repos on the DOM. The reason I use Map/obj is because I'm caching the repos to avoid re-fetch.
The code doesn't work as expected, it's not setting any new state, and I can verify it in the react dev tools. For some reason if I click around on Components in the devtools, the state updates(?!), but the DOM is still not painted (stuck on Loading), which is a very strange behavior.
export default () => {
const [selectedLanguage, setSelectedLanguage] = useState('All');
const [error, setError] = useState(null);
const [repos, setRepos] = useState(() => new Map());
useEffect(() => {
if (repos.has(selectedLanguage)) return;
(async () => {
try {
const data = await fetchPopularRepos(selectedLanguage);
setRepos(repos.set(selectedLanguage, data));
} catch (err) {
console.warn('Error fetching... ', err);
setError(err.message);
}
})();
}, [selectedLanguage, repos]);
const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);
const isLoading = () => !repos.has(selectedLanguage) && !error;
return (
<>
<LanguagesNav
selected={selectedLanguage}
updateLanguage={updateLanguage}
/>
{isLoading() && <Loading text="Fetching repos" />}
{error && <p className="center-text error">{error}</p>}
{repos.has(selectedLanguage)
&& <ReposGrid repos={repos.get(selectedLanguage)} />}
</>
);
};
However, if I change up the code to use object instead of a Map, it works as expected. What am I missing here? For example, this works (using obj instead of a Map)
const Popular = () => {
const [selectedLanguage, setSelectedLanguage] = useState('All');
const [error, setError] = useState(null);
const [repos, setRepos] = useState({});
useEffect(() => {
if (repos[selectedLanguage]) return;
(async () => {
try {
const data = await fetchPopularRepos(selectedLanguage);
setRepos(prev => ({ ...prev, [selectedLanguage]: data }));
} catch (err) {
console.warn('Error fetching... ', err);
setError(err.message);
}
})();
}, [selectedLanguage, repos]);
const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);
const isLoading = () => !repos[selectedLanguage] && !error;
return (
<>
<LanguagesNav
selected={selectedLanguage}
updateLanguage={updateLanguage}
/>
{isLoading() && <Loading text="Fetching repos" />}
{error && <p className="center-text error">{error}</p>}
{repos[selectedLanguage]
&& <ReposGrid repos={repos[selectedLanguage]} />}
</>
);
};
repos.set() mutates the current instance and returns it. Since setRepos() sees the same reference, it doesn't trigger a re-render.
Instead of
setRepos(repos.set(selectedLanguage, data));
you can use:
setRepos(prev => new Map([...prev, [selectedLanguage, data]]));