What's wrong with my custom hook in React? - reactjs

Hi Stack Overflow Community!
I am learning react and now I am practicing custom hooks. I get the example data from here:
https://jsonplaceholder.typicode.com/posts
I've got two components.
App.js
import React from "react";
import useComments from "./hooks/useComments"
const App = () => {
const Comments = useComments();
const renderedItems = Comments.map((comment) => {
return <li key={comment.id}>{comment.title}</li>;
});
return (
<div>
<h1>Comments</h1>
<ul>{renderedItems}</ul>
</div>
);
}
export default App;
useComments.js
import {useState, useEffect} from "react";
const useComments = () => {
const [Comments, setComments] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts", {
"mode": "cors",
"credentials": "omit"
}).then(res => res.json()).then(data => setComments(data))
}, []);
return [Comments];
};
export default useComments;
My output looks like this and i don't know why. There are no warnings or errors.

This line makes Comments be an array:
const [Comments, setComments] = useState([]);
...and then you're wrapping it in an additional array:
return [Comments];
But when you use it, you're treating it as a single dimensional array.
const Comments = useComments();
const renderedItems = Comments.map...
So you'll just need to line those two up. If you want two levels of array-ness (perhaps because you plan to add more to your hook, so that it's returning more things than just Comments), then the component will need to remove one of them. This can be done with destructuring, as in:
const [Comments] = useComments();
Alternatively, if you don't need that complexity, you can change your hook to not add the extra array, and return this:
return Comments;

Related

async fetch pushing data twice into array

I'm trying to make a Rick & Morty API call with fetch and an async arrow function, but I found that the function is pushing the elements received twice into my array.
I already tried to make the call with and without useEffect (I'm using React with TypeScript) but I got no results and I don't understand why the function is being called twice.
Anyone available to explain to me why this is happening?
data.ts:
import { PlanetInterface, ResidentsInterface } from "./data-interfaces";
export const planetsList: PlanetInterface[] = [];
export const residentsList: ResidentsInterface[] = [];
export const getPlanetById = async (planets: number[]) => {
for (let planet of planets) {
const response = await fetch(
`https://rickandmortyapi.com/api/location/${planet}`
);
const planetData: PlanetInterface = await response.json();
planetsList.push(planetData);
}
console.log(planetsList);
};
// export const getResidentsByPlanet = async (residents: string[]) => {
// for (let resident of residents) {
// const response = await fetch(resident);
// const residentData = await response.json();
// residentsList.push(residentData);
// }
// console.log(residentsList);
// };
app.tsx:
import { useEffect } from "react";
import { getPlanetById } from "./api/data";
import "./App.css";
function App() {
useEffect(() => {
getPlanetById([1, 2]);
}, []);
// getPlanetById([1, 2]);
return <main className="container"></main>;
}
export default App;
Expected output: Array of 2 objects (planets with ID 1 and 2)
Received output: Array of 4 objects (planet with ID 1 twice and planet with ID 2 also twice)
If anyone can help me understand why this is happening and how I can fix it, I would be very grateful.
The design of that getPlanetById might be not suit for React since the call of it create a side effect and there is no way to clean it up, you should wrap it into a hook or do a manually clean up, here is an example:
useEffect(() => {
getPlanetById([1, 2]);
return () => { planetsList.length = 0 }
}, []);
I guess you are using <React.StrictMode />
If you remove that, the function is called once as you expect.
Here is the document about strict mode
https://en.reactjs.org/docs/strict-mode.html

Execute Function when a State Variable Changes inside of a useEffect() Hook

so I am trying to create a graph visualization front-end using Antv's G6 and React. I have this useState() variable and function as shown below:
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
The function is in charge of hiding the selected node. However, the problem with running this function as it is, is that it will raise the error TypeError: Cannot read properties of null (reading 'findById') because graph is assigned inside of the useEffect() hook, as shown below:
useEffect(() => {
if (!graph) {
graph = new G6.Graph();
graph.data(data);
graph.render();
hideN();
}
}, []);
It only works as intended if I call the function hideN() inside of the useEffect() hook, otherwise outside of the useEffect() if I console.log(graph) the result would be undefined.
So I wanted to ask, is there a way I could have this function run when the state changes while inside of the useEffect(), or is there a better way to go about this. I'm sorry I am super new to React so still learning the best way to go about doing something. I'd appreciate any help you guys can provide.
Full code:
import G6 from "#antv/g6";
import React, { useEffect, useState, useRef } from "react";
import { data } from "./Data";
import { NodeContextMenu } from "./NodeContextMenu";
const maxWidth = 1300;
const maxHeight = 600;
export default function G1() {
let graph = null;
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graph) {
graph = new G6.Graph(cfg);
graph.data(data);
graph.render();
hideN();
}
}, []);
return (
<div>
<div ref={ref}>
{showNodeContextMenu && (
<NodeContextMenu
x={nodeContextMenuX}
y={nodeContextMenuY}
node={nodeInfo}
setShowNodeContextMenu={setShowNodeContextMenu}
sethideNode={sethideNode}
/>
)}
</div>
</div>
);
}
export { G1 };
Store graph in a React ref so it persists through rerenders. In hideN use an Optional Chaining operator on graphRef.current to call the findById function.
Add hideNode state as a dependency to the useEffect hook and move the hideN call out of the conditional block that is only instantiating a graph value to store in the ref.
const graphRef = useRef(null);
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graphRef.current?.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graphRef.current) {
graphRef.current = new G6.Graph(cfg);
graphRef.current.data(data);
graphRef.current.render();
}
hideN();
}, [hideNode]);

Setting state without re-rendering with useEffect not working

I simply need my state value to change based on screen sizing live time. Even though my screen size changes, my count value stays the same unless I reload the page. this needs to update live for better responsive design. Here is my code. Thanks!
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive'
const MyCarousel = () => {
useEffect(() => {
loadMediaQuery();
}, []);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
if (phone)
setCount(2)
if (desktop)
setCount(3)
}
const tablet = useMediaQuery({
query: '(max-width: 876px)'
})
const phone = useMediaQuery({
query: '(max-width: 576px)'
})
const desktop = useMediaQuery({
query: '(min-width: 876px)'
})
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}
That's because your useEffect has no dependencies so it loads one time only after the component has been mounted.
To fix that you should have the following code:
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive';
const MyCarousel = () => {
const tablet = useMediaQuery({
query: '(max-width: 876px)'
});
const phone = useMediaQuery({
query: '(max-width: 576px)'
});
const desktop = useMediaQuery({
query: '(min-width: 876px)'
});
useEffect(() => {
loadMediaQuery();
}, [tablet, phone, desktop]);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
else if (phone)
setCount(2)
else if (desktop)
setCount(3)
}
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}
useEffect means: runs the callback function that's passed to useEffect after this component is rendered, and since you passed an empty array as the 2nd argument, it means useEffect is executed once, only the first time when this component gets rendered (and not when the state changes) , since your function call is inside useEffect, it will work only when you reload the page, you can either add the variables to the empty array like Mohamed Magdy did, or simply call loadMediaQuery again outside useEffect
Expanding on the custom hook provided in this answer, you can do something like
const [width, height] = useWindowSize();
useEffect(() => {
loadMediaQuery();
}, [width]);
That useEffect function says: Execute loadMediaQuery() every time the value of width changes.

How do I update an array using the useContext hook?

I've set a Context, using createContext, and I want it to update an array that will be used in different components. This array will receive the data fetched from an API (via Axios).
Here is the code:
Context.js
import React, { useState } from "react";
const HeroContext = React.createContext({});
const HeroProvider = props => {
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
const [heroesContext, setHeroesContext] = useState(heroInformation);
return (
<HeroContext.Provider value={heroesContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };
See above that I created the context, but set nothing? Is it right? I've tried setting the same name for the array and function too (heroesContex and feedHeroes, respectively).
Component.js
import React, { useContext, useEffect } from "react";
import { HeroContext } from "../../context/HeroContext";
import defaultSearch from "../../services/api";
const HeroesList = () => {
const context = useContext(HeroContext);
console.log("Just the context", context);
useEffect(() => {
defaultSearch
.get()
.then(response => context.feedHeroes(response.data.data.results))
.then(console.log("Updated heroesContext: ", context.heroesContext));
}, []);
return (
//will return something
)
In the Component.js, I'm importing the defaultSearch, that is a call to the API that fetches the data I want to push to the array.
If you run the code right now, you'll see that it will console the context of one register in the Just the context. I didn't want it... My intention here was the fetch more registers. I have no idea why it is bringing just one register.
Anyway, doing all of this things I did above, it's not populating the array, and hence I can't use the array data in another component.
Does anyone know how to solve this? Where are my errors?
The issue is that you are declaring a piece of state to store an entire context object, but you are then setting that state equal to a single destructured array.
So you're initializing heroesContext to
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
But then replacing it with ...arrayFromAPI.
Also, you are not spreading the array properly. You need to spread it into a new array or else it will return the values separately: setHeroesContext([...arrayFromAPI]);
I would do something like this:
const HeroContext = React.createContext({});
const HeroProvider = props => {
const [heroes, setHeroes] = useState([]);
const heroContext = {
heroesContext: heroes,
feedHeroes: arrayFromAPI => {
setHeroes([...arrayFromAPI]);
}
};
return (
<HeroContext.Provider value={heroContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };

Skipping Effects does not work for Array of dynamic URL's

I have a React.SFC / react stateless / functional component which is unfortunately rendering a little too frequent due to some excess data coming in from redux in a parent component. Nothing I can do about that for now, so I'm just accepting the extra rerenders, and using useEffect to make sure data is only fetched whenever a certain property changes. In this case its called "urls" and it is an array of URL's (TypeScript URL Type).
Here's some example code illustrating the issue:
import React from "react";
import { useState, useEffect, useMemo } from "react";
import { render } from "react-dom";
import "./styles.css";
const useCustomHook = urls => {
const [onlyChangeWhenUrlsChange, setOnlyChangeWhenUrlsChange] = useState(
null
);
useEffect(
() => {
setOnlyChangeWhenUrlsChange(Math.random());
},
[urls]
);
return onlyChangeWhenUrlsChange;
};
const dynamicUrls = (pageRouteParamId, someDynamicUrlParam) => {
return [
{
pageRouteParamId: 1337,
urls: [new URL(`https://someurl.com/api?id=${someDynamicUrlParam}`)]
}
];
};
const SomePage: React.SFC<any> = ({
simulateFrequentUpdatingData,
pageRouteParamId
}) => {
const someOtherId = 1;
// As suggested in SO answer, using useMemo seems to work, but will that not create a memory leak?
// Is there any good alternative?
// const urls = useMemo(() => dynamicUrls(pageRouteParamId, someOtherId).find(url => url.pageRouteParamId === pageRouteParamId).urls, [pageRouteParamId, someOtherId]);
const urls = dynamicUrls(pageRouteParamId, 1).find(
url => url.pageRouteParamId === 1337
).urls;
return (
<div>
<p>parent</p>
<p>{simulateFrequentUpdatingData}</p>
<p>
Page route param id (in real app this would come from react-router route
param): {pageRouteParamId}
</p>
{urls && urls.length && <MyStateLessFunctionalComponent {...{ urls }} />}
<p>
Page route param id (in real app this would come from react-router route
param): {pageRouteParamId}
</p>
{urls && urls.length && (
<MyStateLessFunctionalComponentWithHook {...{ urls }} />
)}
</div>
);
};
const MyStateLessFunctionalComponent: React.SFC<any> = ({ urls }) => {
const [onlyChangeWhenUrlsChange, setOnlyChangeWhenUrlsChange] = useState(
null
);
useEffect(
() => {
setOnlyChangeWhenUrlsChange(Math.random());
},
[urls]
);
return (
<div>
<p>MyStateLessFunctionalComponent</p>
<p>{JSON.stringify(urls)}</p>
<p>This should only change when urls change {onlyChangeWhenUrlsChange}</p>
</div>
);
};
const MyStateLessFunctionalComponentWithHook: React.SFC<any> = ({ urls }) => {
const onlyChangeWhenUrlsChange = useCustomHook(urls);
return (
<div>
<p>MyStateLessFunctionalComponentWithHook</p>
<p>{JSON.stringify(urls)}</p>
<p>This should only change when urls change {onlyChangeWhenUrlsChange}</p>
</div>
);
};
function App() {
const [
simulateFrequentUpdatingData,
setSimulateFrequentUpdatingData
] = useState(null);
const [pageRouteParamId, setPageRouteParamId] = useState(1337);
useEffect(() => {
setInterval(() => setSimulateFrequentUpdatingData(Math.random()), 1000);
}, []);
return (
<div className="App">
<SomePage {...{ simulateFrequentUpdatingData, pageRouteParamId }} />
</div>
);
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Edit:
I had to change the title and question, since while reproducing it with the example code I realized the problem was not about "Skipping Effects inside a custom hook". Before I though I saw a difference when using useEffect directly vs inside a custom hook, and as the comments rightfully mentioned, there should not be any difference - and I came to the same conclusion while reproducing my issue with this sample code:
You can check out a live example here.
As it was suggested in the answer below, it seems like useMemo solves the issue (see line 36)
My guess is that urls is being declared inside a render higher up the tree, and thus getting a new identity every time. You can either useMemo on the place where it is being declared, JSON.stringify the urls in the deps-array, or a useRef which works as an additional guard against re-runs.
Edit: This is being discussed by smarter people than me: https://github.com/facebook/react/issues/14476#issuecomment-471199055.
If urls is an array of strings you can pass that as the second argument to useEffect
useEffect(() => {
loadData();
}, urls);
that way it will check the string values instead of the array reference.

Resources