Rect async calls outside useEffect - memory leak - reactjs

I have a simple useEffect hook in my Task:
const TaskAD = ({ match }: TaskADProps) => {
const { taskName } = match.params;
const [task, setTask] = useState<TaskData | null>(null);
const [loading, setLoading] = useState(true);
const authCommunicator = authRequest();
useEffect(() => {
const getTask = async () => {
const taskData = await authCommunicator
.get(`/task/${taskName}`)
.then((response) => response.data);
setTask(taskData);
setLoading(false);
};
getTask();
}, []);
if (loading || task == null) {
return <Spinner centered />;
}
const updateDescription = async (content: string): Promise<boolean> => {
const r = await authCommunicator
.patch(`/task/${task.name}/`, {
description: content,
})
.then((response) => {
console.log("Setting Task data!");
setTask(response.data);
return true;
})
.catch(() => false);
return r;
};
return (
<ProjectEntity name={taskName}>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<StatusRow status="Open" />
<TaskDetails task={task} />
<Description content={task.description} onSubmit={updateDescription} />
<Title level={2}>Subtasks:</Title>
<Table dataSource={dataSource} columns={columns} />
</Space>
</ProjectEntity>
);
};
Task object contains a description. The description is another component with a text area. The idea is: when a user changes the description in the child component, the child component has a function (passed via props) to update the description.
So I pass updateDescription to my child component (Description) via props. Both useEffect and updateDescription are in my Task component, the Description component is basically stateless. What happens:
user updates a description
child component calls the function, it updates a record in my DB
it gets the response from the API and calls setTask
task variable is passed to Description via props in Task's render, so they both get updated since the state of parent Task has changed
I see updated description
The only problem is that although it work, but when I do this, I can see this in console:
Setting Task data!
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
(i've added the console.log just to see when it happens).
So I wanted to ask if this is a problem of me having async calls outside useEffect or maybe something else?
#Edit Description code (I removed all the unnecessary junk):
interface DescriptionProps {
content: string;
onSubmit?: (content: string) => Promise<boolean>;
title?: string;
rows?: number;
}
const Description = (props: DescriptionProps) => {
const { content, onSubmit, title, rows } = props;
const [descriptionContent, setDescriptionContent] = useState(content);
const [expanded, setExpanded] = useState(true);
const [editMode, setEditMode] = useState(false);
const [descriptionChanged, setDescriptionChanged] = useState(false);
const editable = onSubmit !== undefined;
const resetDescription = () => {
setDescriptionContent(content);
setDescriptionChanged(false);
};
const changeDescription = (value: string) => {
setDescriptionContent(value);
setDescriptionChanged(true);
};
const descriptionTitle = (
<>
<S.DescriptionTitle>{title}</S.DescriptionTitle>
</>
);
return (
<Collapse
defaultActiveKey={["desc"]}
expandIcon={S.ExpandIcon}
onChange={() => setExpanded(!expanded)}
>
<S.DescriptionHeader header={descriptionTitle} key="desc">
<S.DescriptionContent
onChange={(event): void => changeDescription(event.target.value)}
/>
{descriptionChanged && onSubmit !== undefined ? (
<S.DescriptionEditActions>
<Space size="middle">
<S.SaveIcon
onClick={async () => {
setDescriptionChanged(!(await onSubmit(descriptionContent)));
}}
/>
<S.CancelIcon onClick={() => resetDescription()} />
</Space>
</S.DescriptionEditActions>
) : null}
</S.DescriptionHeader>
</Collapse>
);
};
#Edit2
Funny thing, adding this to my Description solves the issue:
useEffect(
() => () => {
setDescriptionContent("");
},
[content]
);
Can anyone explain why?

Related

How to refactor this call to refresh state

I want to know how improve this calls in order to not repeat always the same sentence to refresh the state...
I don't need any huge refactor, only inputs like: you need to put this call inside a function and call it when you want... something like this...
export const CategoriesPage = () => {
const [categories, setCategories] = useState<Category[]>([]);
const [showModal, setShowModal] = useState(false);
const handleCreateCategory = (newCategory: CategoryCreate, file: File) => {
createCategoryHelper(newCategory, file)
.then(() => {
getCategoriesHelper().then(setCategories);
})
.finally(() => handleClose());
};
const handleDeleteCategory = (categoryId: Id) => {
SwalHelper.delete().then(() => {
deleteCategoryHelper(categoryId).then(() =>
getCategoriesHelper().then(setCategories)
);
});
};
const handleClose = () => {
setShowModal(false);
};
const handleModal = () => {
setShowModal(true);
};
useEffect(() => {
getCategoriesHelper().then(setCategories);
}, []);
return (
<>
<PageTitle title="Categories" />
<FilterBar>
<Button type="button" background="green" onClick={handleModal}>
+ Add new
</Button>
</FilterBar>
{showModal && (
<ModalPortal onClose={handleClose}>
<CreateCategoryForm
createCategory={(category, file: File) => {
handleCreateCategory(category, file);
}}
/>
</ModalPortal>
)}
<ListGrid columns={3}>
{categories.map((category) => {
const { id: categoryId } = category;
return (
<CategoryCard
key={categoryId}
{...category}
onClick={() => handleDeleteCategory(categoryId)}
/>
);
})}
</ListGrid>
</>
);
};
When component is mounting, on useEffect, fills the state with response in order to create a list.
When a category is created, I call to setState again to refresh the list.
Same on delete, on then, refresh again to update the list.
Three times calling the same sentence
getCategoriesHelper().then(setCategories)
This is getCategoriesHelper:
export const getCategoriesHelper = async () => {
const service = new CategoryServiceImplementation(apiConfig);
const uploadImageService = new AmplifyS3Service();
const repository = new CategoryRepositoryImplementation(
service,
uploadImageService
);
const useCase = new GetCategoriesUseCaseImplementation(repository);
return await useCase.getCategories();
};
Is there any way to make this code much cleaner and reusable?
Thanks in advance!
Everything is write, and all calls are made as they are designed to do

persist state after page refresh in React using local storage

What I would like to happen is when displayBtn() is clicked for the items in localStorage to display.
In useEffect() there is localStorage.setItem("localValue", JSON.stringify(myLeads)) MyLeads is an array which holds leads const const [myLeads, setMyLeads] = useState([]); myLeads state is changed when the saveBtn() is clicked setMyLeads((prev) => [...prev, leadValue.inputVal]);
In DevTools > Applications, localStorage is being updated but when the page is refreshed localStorage is empty []. How do you make localStorage persist state after refresh? I came across this article and have applied the logic but it hasn't solved the issue. I know it is something I have done incorrectly.
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const localStoredValue = JSON.parse(localStorage.getItem("localValue")) ;
const [localItems] = useState(localStoredValue || []);
useEffect(() => {
localStorage.setItem("localValue", JSON.stringify(myLeads));
}, [myLeads]);
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
// setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true);
};
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
);
}
export default App;```
You've fallen into a classic React Hooks trap - because using useState() is so easy, you're actually overusing it.
If localStorage is your storage mechanism, then you don't need useState() for that AT ALL. You'll end up having a fight at some point between your two sources about what is "the right state".
All you need for your use-case is something to hold the text that feeds your controlled input component (I've called it leadText), and something to hold your display boolean:
const [leadText, setLeadText] = useState('')
const [display, setDisplay] = useState(false)
const localStoredValues = JSON.parse(window.localStorage.getItem('localValue') || '[]')
const handleChange = (event) => {
const { name, value } = event.target
setLeadText(value)
}
const saveBtn = () => {
const updatedArray = [...localStoredValues, leadText]
localStorage.setItem('localValue', JSON.stringify(updatedArray))
setDisplay(false)
}
const displayBtn = () => {
setDisplay(true)
}
const displayLocalItems = localStoredValues.map((item) => {
return <li key={item}>{item}</li>
})
return (
<main>
<input name="inputVal" value={leadText} type="text" onChange={handleChange} required />
<button onClick={saveBtn}> Save </button>
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
)

How do I avoid "Can't perform a React state update on an unmounted component" error on my application?

I'm trying to make upload file part and I got an issue like when I upload csv file and the first component has got error and when I upload file on another component it doesn't get error
and the error is like this :
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
my website is working fine, however I'm worrying the error would make it bad
info state is for uploading file .
and i need to upload file each components at Parent component
but i'm using it in Child component and it works fine except that error
I assume that info state is making the issue .
I'd like to know how to avoid that error
Thank you in advance
and my code is like this:
Parent Component :
const eachComponent = (index, id) => (
<DataSide id={id} key={index} onClick={chartItself}>
<SettingMenu
panelNum={index + 1}
show={show[index]}
chart={chart[index]}
changeLayout={changeLayout}
/>
{ChangeableType(index + 1).map(
(id, idx) =>
chart[index].key === id.key && ChangeableType(index + 1)[idx]
)}
{BarTypes(index).map(
(id, idx) => chart[index].key === id.key && BarTypes(index)[idx]
)}
{/* {LineTypes(index).map(
(id, idx) => chart[index].key === id.key && LineTypes(index)[idx]
)}
{GridTypes(index).map(
(id, idx) => chart[index].key === id.key && GridTypes(index)[idx]
)} */}
</DataSide>
);
const layout = [
eachComponent(0, "first"),
eachComponent(1, "second"),
eachComponent(2, "third"),
eachComponent(3, "fourth"),
and Child component :
const CsvFile = ({ match, location }) => {
const { panelNum, changeLayout } = location.state;
const chart = location.data;
const { Plugins, DataContextUseState } = useContext(DataContextApi);
const [plugins, setPlugins] = Plugins;
const [DataContext, setDataContext] = DataContextUseState;
const [info, setInfo] = useState([]);
///this info is the cause as i guess
const history = useHistory();
const [y, setY] = useState();
const [x, setX] = useState();
const [title, setTitle] = useState("");
This is the Child component of second one that I'm using info state :
const CsvFileReader = ({ setInfo }) => {
const handleOnDrop = data => {
const infos = data.map(item => item.data);
setTimeout(() => setInfo([...infos]), 1000);
};
const handleOnError = (err, file, inputElem, reason) => {
console.log(err);
};
const handleOnRemoveFile = data => {
console.log(data);
};
return (
<>
<MainReader>
<CSVReader
onDrop={handleOnDrop}
onError={handleOnError}
config={
(({ fastMode: true }, { chunk: "LocalChunkSize" }),
{ header: false })
}
addRemoveButton
onRemoveFile={handleOnRemoveFile}
>
You are using a timeout to update state, possibly after the component has unmounted. Use a react ref to store a reference to the current timeout and clear it when the component unmounts.
const CsvFileReader = ({ setInfo }) => {
const timerRef = React.useRef();
useEffect(() => {
return () => clearTimeout(timerRef.current); // clear any running timeouts
}, []);
const handleOnDrop = data => {
const infos = data.map(item => item.data);
timerRef.current = setTimeout(() => setInfo([...infos]), 1000); // save timeout ref
};
You can use a ref to check component is unmounted or not in CsvFileReader component
const ref = useRef()
const handleOnDrop = data => {
const infos = data.map(item => item.data);
setTimeout(() => ref.current && setInfo([...infos]), 1000);
};
return (
<div ref={ref}>
<MainReader>

How to update parent component with a state that has updated within a custom hook?

I'm not sure if this has been asked before. I couldn't find anything after googling.
I have a parent component which basically has a button which when clicked will open a modal.
Inside the modal, there is a form which makes a post-call to an API. If the post-call is successful, I need the modal to be closed. I'm experimenting custom hooks to achieve this.
Below is my code:
Custom Hook
type savedHook = {
saved: boolean,
loading: boolean,
error: string,
saveSearch: (search: any) => void,
showNewModal: boolean,
setShowNewModal: (boolean) => void
};
export const useSaveSearch = () : savedSearchHook => {
const [loading, setLoading] = useState(false);
const [saved, setSaved] = useState(false);
const [error, setError] = useState('');
const [showNewSaveSearch, setNewShowSearch] = useState(false);
const saveSearch = async (search: any) => {
setLoading(true);
fetch('my api', {
method: 'POST',
body: JSON.stringify(search),
headers: {
'Content-Type': 'application/json'
}
}).then((data) => {
setSaved(true);
setLoading(false);
setShowNewModal(false);
}).catch((error) => {
setError(error);
setLoading(false);
});
}
const setShowNewModal = (show: boolean) => {
setNewShowSearch(show);
}
return {
error,
loading,
saveSearch,
saved,
setShowNewModal,
showNewModal: showNewSaveSearch
}
}
Modal
export default function SaveSearch({isOpen, onDismiss}) {
const { state } = useSearch();
const [name, setName] = useState('');
const { loading, saved, error, saveSearch } = useSaveSearch();
const handleSave = () => {
saveSearch({
name,
query: state.query,
type: state.type
});
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss}>
<div>
<span>Save Search ({state.type})</span>
<IconButton styles={iconButtonStyles} iconProps={{iconName: 'Cancel'}} onClick={onDismiss} />
</div>
<div>
<TextField label="Name" autoFocus value={name} onChange={(e, value) => setName(value)} />
{loading && <Spinner size={SpinnerSize.small} />}
<DefaultButton text="Save" onClick={handleSave} iconProps={{iconName: 'Save'}} disabled={name.length === 0 || loading} />
</div>
</Modal>
)
}
Parent Component
export default function ParentComponent() {
const { showNewModal, setShowNewModal } = useSaveSearch();
return (
<div>
{<SaveSearch isOpen={showNewModal} onDismiss={() => setShowNewModal(false)} />}
<PrimaryButton text="Save Search" onClick={() => setShowNewModal(true)} iconProps={{iconName: 'Save'}} />
</div>
);
}
The problem I'm facing is, to open the modal, I'm calling setShowNewModal from the parent component which works fine. But after the save function, I'm calling setShowNewModal from the hook which doesn't get updated in the parent component.
It would be good if you could provide a working example.
Anyways, if I'm correct, the setShowNewModal(false); inside the saveSearch method of useSaveSearch custom hook should close the Modal, right?
Well, if that's the case, inside setShowNewModal you just call setNewShowSearch. Then, the value of setNewShowSearch is returned for a property named showNewModal, but inside the Modal, when you write the following line:
const { loading, saved, error, saveSearch } = useSaveSearch();
You don't consider the showNewModal property, in the deconstructor. Maybe I'm missing something in the flow: that's why I was asking for a working demo.
Anyways, going back to the issue: inside the Modal component you could just pass the onDismiss method:
const { loading, saved, error, saveSearch } = useSaveSearch(onDismiss);
And inside the useSaveSearch custom hook, just call the onDismiss parameter, which will call the callback () => setShowNewModal(false) defined in the Parent component.

react hooks issue when infinite paging filter applied

I have an infinite paging setup in a react redux project like this..
const ItemDashboard = () => {
const items= useSelector(state => state.items.items);
const dispatch = useDispatch();
const [loadedItems, setLoadedItems] = useState([]);
const [categories, setCategories] = useState([
'cycling',
'diy',
'electrical',
'food',
'motoring',
'travel'
]);
const initial = useRef(true);
const [loadingInitial, setLoadingInitial] = useState(true);
const [moreItems, setMoreItems] = useState([]);
const onChangeFilter = (category, show) => {
!show
? setCategories(categories.filter(c => c != category))
: setCategories([...categories, category]);
};
const loadItems = () => {
dispatch(getItems(categories, items && items[items.length - 1]))
.then(more => setMoreItems(more));
}
const getNextItems = () => {
loadItems();
};
useEffect(() => {
if(initial.current) {
loadItems();
setLoadingInitial(false);
initial.current = false;
}
}, [loadItems]);
useEffect(() => {
if(items) {
setLoadedItems(loadedItems => [...loadedItems, ...items]);
}
}, [items]);
useEffect(() => {
//this effect is fired on intial load which is a problem!
setLoadingInitial(true);
initial.current = true;
}, [categories]);
return (
<Container>
<Filter onFilter={onChangeFilter} categories={categories} />
{loadingInitial ? (
<Row>
<Col sm={8} className='mt-2'>
<LoadingComponent />
</Col>
</Row>
) : (
<ItemList
items={loadedItems}
getNextItems={getNextItems}
moreItems={moreItems}
/>
)}
</Container>
);
};
In the filter component, when the filter is changed the onChangeFilter handler method is fired which updates the array of categories in state. When this filter is changed I need to set the loadedItems in state to an empty array and call the load items callback again but I can't work out how to do it. If I add another effect hook with a dependency on categories state, it fires on the initial load also. I'm probably doing this all wrong as it feels a bit hacky the whole thing. Any advice much appreciated.

Resources