Updating State in React Component causing it to get unmounted - javascript

I have a component where-in I need to fetch some data and render it. The component gets rendered initially. The problem I'm facing is when the handler function switchDocumentType is called after clicking the button for a particular type, the whole component gets unmounted/un-rendered.
While debugging on my own I found this happens after setDocumentType is run inside event handler function.
What is wrong in the below code snippet that could possibly cause this issue? I can see the useEffect is not going in infinite-loop as well.
Code snippet:
import * as React from 'react';
const MyComponent = (props) => {
const [documentType, setDocumentType] = React.useState('alpha');
const [documentData, setDocumentData] = React.useState('');
const types = ['alpha', 'beta', 'gamma'];
React.useEffect(() => {
myDataFetch('https://example.com/foo/?bar=123').then(async (response) => {
const data = await response.json();
setDocumentData(data.terms); // html string
const myDiv = document.getElementById('spacial-div');
myDiv.innerHTML = data; // need to render raw HTML inside a div
});
}, [documentType]);
const switchDocumentType = (type) => {
setDocumentType(type);
// send some analytics events
};
const convertToPDF = () => {
// uses documentData to generate PDF
};
return (
<div className="container-div">
{types.map((type) => {
return (
<button key={type} onClick={(type) => switchDocumentType(type)}>
{type}
</button>
);
})}
<div id="special-div" />
</div>
);
};
export default MyComponent;

You shouldn't edit the DOM directly. React has two DOMs, a virtual DOM and a real DOM. Rendering can be a bit finicky if you decide to edit the real DOM.
You can parse html safely, by using html-react-parser. This is the best way to do it, because it becomes part of the react tree whereas dangerouslySetInnerHTML will replace the entire HTML to flush changes to the DOM. With reconciliation, it can create exponential load times.
It will also sanitize your inputs, you know.. for safety. :)
import parse from 'html-react-parser';
const SpecialDiv = ({html}) => {
const reactElement = parse(html);
return reactElement
}
If you decide that you must use dangerouslySetInnerHTML you can do it as so:
const [someHTML, setSomeHTML] = useState(null)
const someFunction = async() => {
const response = await getData();
const data = await response.json();
setSomeHTML(data);
}
return(
<div>
{someHTML && <div dangerouslySetInnerHTML={{__html: someHTML}} id="special-div"/>}
</div>
)
That being said, I would say that by allowing this, you open yourself up to the possibility of a XSS attack, without properly parsing and purifying your inputs.

Do not use useEffect as handler, use useEffect hooks for initializations.
Instead of using/setting innerHtml, let react do it for you.
I suppose you have myDataFetch defined somewhere and I don't see your data fetch using the type.
Anyways, try to use the modified code below.
import * as React from 'react';
const MyComponent = (props) => {
const [documentType, setDocumentType] = React.useState('alpha');
const [documentData, setDocumentData] = React.useState('');
const types = ['alpha', 'beta', 'gamma'];
const fetchData = async () => {
const response = await myDataFetch('https://example.com/foo/?bar=123')
const data = await response.json();
setDocumentData(data);
}
React.useEffect(() => {
fetchData();
}, []);
const switchDocumentType = async (e, type) => {
e.preventDefault();
setDocumentType(type);
await fetchData();
// send some analytics events
};
return (
<div className="container-div">
{types.map((type) => {
return (
<button key={type} onClick={(e) => switchDocumentType(e, type)}>
{type}
</button>
);
})}
<div id="special-div">{documentData}</div>
</div>
);
};
export default MyComponent;

Not sure why but placing debuggers before state update causes this issue, not only for this component, but for all the other components I tried with. Seems to be an issue either with debugger or React. Removing debuggers solved the issue.
Also, now I'm returning a cleanup function inside useEffect as pointed out in some stack-overflow posts. I also refactored the code as suggested by #iaq and #sheepiiHD to follow React best practices.
Updated code:
import * as React from 'react';
const MyComponent = (props) => {
const [documentType, setDocumentType] = React.useState('alpha');
const [documentData, setDocumentData] = React.useState('');
const types = ['alpha', 'beta', 'gamma'];
const fetchData = async () => {
const response = await myDataFetch('https://example.com/foo/?bar=123')
const data = await response.json();
setDocumentData(data);
}
React.useEffect(() => {
fetchData();
return () => {
setDocumentType('');
setDocumentData('');
};
}, []);
const switchDocumentType = async (e, type) => {
e.preventDefault();
setDocumentType(type);
await fetchData();
// send some analytics events
};
return (
<div className="container-div">
{types.map((type) => {
return (
<button key={type} onClick={(e) => switchDocumentType(e, type)}>
{type}
</button>
);
})}
<div id="special-div" dangerouslySetInnerHTML={{__html: documentData.terms}} />
</div>
);
};
export default MyComponent;

Related

React useState array empty on initial load but after editing code while app is running array fills?

This is going to be really hard to explain, but here goes. I am building a React card grid with a filter. The data is pulled from an MySQL AWS API I built. The .tags property is JSON with an array that stores each tag associated with the card. I have written Javascript in App.jsx to turn this JSON into an object, and then store every unique tag in a piece of state. See code below:
//App.jsx
import { useEffect, useState } from 'react';
import '../assets/css/App.css';
import Card from './Card';
import Filter from './Filter'
import {motion, AnimatePresence} from 'framer-motion'
function App() {
const [cards, setCards] = useState([]);
const [filter, setFilter] = useState([]);
const [activeFilter, setActiveFilter] = useState("all");
const [tags,setTags] = useState([]);
useEffect(() => {
fetchData();
}, []);
/*useEffect(() => {
console.log(tags);
console.log(activeFilter);
}, [activeFilter,tags]);
*/
const getTags = () => {
let tags = [];
cards.forEach((card) => {
let obj = JSON.parse(card.tags);
obj.forEach((tag) => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
setTags(tags);
}
const fetchData = async () => {
const data = await fetch("<<api>>");
const cards = await data.json();
setCards(cards);
setFilter((cards));
getTags();
}
return (
<div className="App">
<Filter
cards={cards}
setFilter={setFilter}
activeFilter={activeFilter}
setActiveFilter={setActiveFilter}
/>
<motion.div layout className="Cards">
<AnimatePresence>
{filter.map((card) => {
return <Card key={card.id} card={card}/>;
})}
</AnimatePresence>
</motion.div>
</div>
);
}
export default App;
The problem that I am having is that when I run the app initially, the tags state is empty when inspecting from React Dev tools. However, when I keep the app running, and then add something like a console.log(tags); before setTags(tags) is called in the getTags() function, the data suddenly appears in the state. If someone could explain why the state seems to be empty even though I am updating it on the initial render that would be really appreciated.
You are running getTags on empty array. setCards doesn't set the const variable instantly. New values will be present in the next render cycle.
Try adding cards param
const getTags = (cards) => {
let tags = [];
cards.forEach((card) => {
let obj = JSON.parse(card.tags);
obj.forEach((tag) => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
setTags(tags);
}
And use it like this:
const fetchData = async () => {
const data = await fetch("API url");
const cards = await data.json();
setCards(cards);
setFilter((cards));
getTags(cards);
}

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.

retrieve parent component ref for a hook

I've found myself needing to retrieve the element ref for every parent component that my hook, useExample, is used in. However, I'm stumped as to how I might be able to retrieve something like this or how to even check if there is an element to target?
Usually I would just do something a little "hacky" in a functional component like so:
const Example = WrappedComponent => {
const ref = createRef();
return <WrappedComponent ref={ref} />;
};
However, due to it being a hook and returning information and not a component, I can't target any component, and thus I'm very stumped.
My current code:
const useExample = () => {
const [stateValue, setStateValue] = useState("example");
useEffect(() => {
// Run some code...
}, []);
return stateValue;
};
const Component = () => {
const data = useExample();
return (
<div> /* <--- How do I gain access to this element */
<span>{ data }</span>
</div>
);
};
I could probably pass a created ref which has been attached to the parent div as a parameter to useExample, however this feels cheap and hacky, and I feel there should be a much easier solution.
In the ideal world something like this would be amazing:
const ref = React.getParentRef();
Apologies if there is an obvious answer in the documentation, I'm very new to React and am unsure of the correct question to be asking or what to be looking for in order to find it in the docs.
You can return the ref from the hook
const useExample = () => {
const myRef = React.useRef(null)
const [stateValue, setStateValue] = useState("example");
useEffect(() => {
// Run some code...
}, []);
return [myRef , stateValue];
};
const Component = () => {
const [myRef , data] = useExample();
return (
<div ref={myRef}> /* <--- How do I gain access to this element */
<span>{ data }</span>
</div>
);
};
If data can be a component:
const useExample = () => {
const myRef = React.useRef(null);
const [stateValue, setStateValue] = React.useState("example");
React.useEffect(() => {
const parent = myRef?.current?.parentNode;
console.log(parent);
}, []);
return <div ref={myRef}>{stateValue}</div>;
};
const Component = () => {
const data = useExample();
return (
<div>
<span>{data}</span>
</div>
);
};
export default function App() {
return <Component />;
}
But then you have to access the parent node from the ref, I believe this may cause problems as a component is being returned, and its anti pattern

API based search using React

I am new to React and I want to run API based search. I have written a sample code with the search functionality but it is not working as per requirement. I want to search from the list but it is always giving the same array of as the whole list when I am writing anything in the search box.
Please help me out and let me know where the code in wrong.
Here is my code:
TestEntry.js
import React , {useState,useEffect} from 'react'
import {Table} from 'reactstrap'
import {Navbar,Nav,NavDropdown,Form,FormControl,Button} from 'react-bootstrap'
//import axios from 'axios'
import Loading from './loading.gif';
const CoinGecko = require('coingecko-api');
const CoinGeckoClient = new CoinGecko();
function TestEntry(){
const[item,SearchData]=useState([]);
const[cryptos,setCryptos]=useState([]);
useEffect(()=>{
fetchItems()
},[])
const fetchItems=async()=>{
const url="https://api.coingecko.com/api/v3/coins/list";
const response= await fetch(url);
const info=await response.json();
console.log(info);
setCryptos(info);
}
const Search=(key)=>{
console.log(key);
fetch("https://api.coingecko.com/api/v3/coins/list?q="+key)
.then((data)=>{
data.json().then((resp)=>{
console.warn("resp:",resp)
SearchData(resp)
})
})
}
const cryptoJsx=cryptos.map(crypto=>(
<div key={crypto.id}>
{crypto.id}
</div>
));
return(
<div>
Search:
<input type="text" onChange={(event)=>Search(event.target.value)}/>
<div>
{
{item} ?
<div>
{
item.map((items)=>
<div key={items.id}>{items.name}</div>
)
}
</div>
: ""
}
</div>
{cryptoJsx}
</div>
)
}
export default TestEntry
The search api seems not working. When I tried api-search for a text separately in browser, it returned full results.
Anyway....
You can do search locally i.e. filter the cryptos array. It will cause re-render and only filtered results are shown.
Note:
Maintain a copy of the cryptos and always filter based on original and only mutate the cryptos. This way search works (both typing a char and deleting a char) and search results are re-rendered automatically
The downside of filtering state data is that new data from server is only obtained in client upon page refresh
If you really want to use api docs and use correct endpoint. Also consider using debouncing. Perform debounce in React.js
I have checked this and search is working fine.
import React, { useState, useEffect } from "react";
function TestEntry() {
const [item, SearchData] = useState([]);
const [cryptos, setCryptos] = useState([]);
const [origCryptosCount, setOrigCryptosCount] = useState([]);
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
const url = "https://api.coingecko.com/api/v3/coins/list";
const response = await fetch(url);
const info = await response.json();
setCryptos(info);
setOrigCryptosCount(info);
};
// const Search_Old = key => {
// console.log(key);
// fetch("https://api.coingecko.com/api/v3/coins/list?q=" + key).then(data => {
// data.json().then(resp => {
// SearchData(resp);
// });
// });
// };
//
const Search = key => {
const newResults = origCryptosCount.filter(crypto => crypto.name.includes(key));
console.log('newResults', newResults);
setCryptos(newResults);
};
const cryptoJsx = cryptos.map(crypto => (
<div key={crypto.id}>{crypto.id}</div>
));
return (
<div>
Search:
<input type="text" onChange={event => Search(event.target.value)} />
{cryptoJsx}
</div>
);
}
export default TestEntry;

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