Call react hooks inside event handler - javascript

I need to re-fetching data if i click some button, but when i call hook inside click handler i get following error
const Menus = ({ menus, title }) => {
const handleClick = () => {
const { data: cartItems } = useFetch(API_URL + 'cart');
}
}
src\components\Menus.js | Line 26:13: React Hook "useFetch" is called in function "handleMenu" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter react-hooks/rules-of-hooks

React hooks can't be used inside a pure JavaScript function. It will break the rules of hooks. Hooks can only be used in React function components. A function returning ReactElement will be treated as a React function component instead of a normal function in JS.
You should return the data and a data fetch function in the useFetch hook. So that you can use the data fetch function later.
E.g.
import React from 'react';
import { useCallback, useEffect, useState } from 'react';
const API_URL = 'http://localhost:8080/api/';
const api = {
async getCartItems() {
return ['apple', 'banana'];
},
};
function useFetch(url: string) {
const [cartItems, setCartItems] = useState<string[]>([]);
// fetch data later use this function.
const getCartItems = useCallback(() => {
return api.getCartItems().then((res) => {
setCartItems(res);
});
}, [url]);
// fetch data when component mount
useEffect(() => {
getCartItems();
}, [url]);
return { data: cartItems, getCartItems };
}
const Menus = () => {
const { data: cartItems, getCartItems } = useFetch(API_URL + 'cart');
const handleClick = () => {
getCartItems();
};
return (
<div onClick={handleClick}>
<ul>
{cartItems.map((item, i) => {
return <li key={i}>{item}</li>;
})}
</ul>
</div>
);
};

As the error mentions, the issue violates the rules of hooks (react-hooks/rules-of-hooks)
More information can be found here:
https://reactjs.org/docs/hooks-rules.html
You can only use hooks in the top level of functional components but the handleClick() function would put the hook at the second level rather than the top level.

Related

How to re-render a custom hook after initial render

I have custom hook named useIsUserSubscribed that checks to see a specific user is subscribed. It returns true if the user is subscribed and false if the user is not subscribed...
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { checkSubscription } from "../services";
// this hook checks if the current user is subscribed to a particular user(publisherId)
function useIsUserSubscribed(publisherId) {
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
const currentUserId = useSelector((state) => state.auth.user?.id);
useEffect(() => {
if (!currentUserId || !publisherId) return;
async function fetchCheckSubscriptionData() {
try {
const res = await checkSubscription(publisherId);
setUserIsSubscribed(true);
} catch (err) {
setUserIsSubscribed(false);
}
}
fetchCheckSubscriptionData();
}, [publisherId, currentUserId]);
return userIsSubscribed;
}
export default useIsUserSubscribed;
...I have a button using this hook that renders text conditionally based on the boolean returned from useIsUserSubscribed...
import React, { useEffect, useState } from "react";
import { add, remove } from "../../services";
import useIsUserSubscribed from "../../hooks/useIsUserSubscribed";
const SubscribeUnsubscribeBtn = ({profilePageUserId}) => {
const userIsSubscribed = useIsUserSubscribed(profilePageUserId);
const onClick = async () => {
if (userIsSubscribed) {
// this is an API Call to the backend
await removeSubscription(profilePageUserId);
} else {
// this is an API Call to the backend
await addSubscription(profilePageUserId);
}
// HOW CAN I RERENDER THE HOOK HERE!!!!?
}
return (
<button type="button" className="sub-edit-unsub-btn bsc-button" onClick={onClick}>
{userIsSubscribed ? 'Subscribed' : 'Unsubscribed'}
</button>
);
}
After onClick I would like to rerender my the useIsUserSubscribed hook So that my button text toggles. Can this be done?
you can not use useEffect in your hook for that purpose try this :
hook :
function useIsUserSubscribed() {
const currentUserId = useSelector((state) => state.auth.user?.id);
const checkUser = useCallback(async (publisherId, setUserIsSubscribed) => {
if (!currentUserId || !publisherId) return;
try {
const res = await checkSubscription(publisherId);
setUserIsSubscribed(true);
} catch (err) {
setUserIsSubscribed(false);
}
}, [currentUserId]);
return {checkUser};
}
export default useIsUserSubscribed;
component :
const SubscribeUnsubscribeBtn = ({profilePageUserId}) => {
const [userIsSubscribed,setUserIsSubscribed]=useState(false);
const { checkUser } = useIsUserSubscribed();
useEffect(()=>{
checkUser(profilePageUserId,setUserIsSubscribed)
},[checkUser,profilePageUserId]);
const onClick = async () => {
if (userIsSubscribed) {
// this is an API Call to the backend
await removeSubscription(profilePageUserId);
} else {
// this is an API Call to the backend
await addSubscription(profilePageUserId);
}
// HOW CAN I RERENDER THE HOOK HERE!!!!?
checkUser(profilePageUserId,setUserIsSubscribed)
}
return (
<button type="button" className="sub-edit-unsub-btn bsc-button" onClick={onClick}>
{userIsSubscribed ? 'Subscribed' : 'Unsubscribed'}
</button>
);
}
you can also add some loading state in your hook and return them too so you can check if process is already done or not
Add a dependece on useIsUserSubscribed's useEffect.
hook :
function useIsUserSubscribed(publisherId) {
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
const currentUserId = useSelector((state) => state.auth.user?.id);
// add refresh dependece
const refresh = useSelector((state) => state.auth.refresh);
useEffect(() => {
...
}, [publisherId, currentUserId, refresh]);
...
}
component :
const onClick = async () => {
...
// HOW CAN I RERENDER THE HOOK HERE!!!!?
// when click, you can dispatch a refresh flag.
dispatch(refreshSubState([]))
}
Expose forceUpdate metheod.
hook :
function useIsUserSubscribed(publisherId) {
const [update, setUpdate] = useState({});
const forceUpdate = () => {
setUpdate({});
}
return {userIsSubscribed, forceUpdate};
}
component :
const {userIsSubscribed, forceUpdate} = useIsUserSubscribed(profilePageUserId);
const onClick = async () => {
...
forceUpdate();
}
Here is another solution by user #bitspook
SubscribeUnsubscribeBtn has a dependency on useIsUserSubscribed, but useIsUserSubscribed don't depend on anything from SubscribeUnsubscribeBtn.
Instead, useIsUserSubscribed is keeping a local state. You have a couple of choices here:
Move the state regarding whetehr user is subscribed or not one level up, since you are using Redux, perhaps in Redux.
Communicate to useIsUserSubscribed that you need to change its internal state.
For 1)
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
move this state to Redux store and use it with useSelector.
For 2), return an array of value and callback from the hook, instead of just the value. It will allow you to communicate from component back into the hook.
In useIsUserSubscribed,
return [userIsSubscribed, setUserIsSubscribed];
Then in onClick, you can call setUserIsSubscribed(false), changing the hook's internal state, and re-rendering your component.

conditional rendering with toast and usestate does not work with react

I have my state and I want to display the component if the value is true but in the console I receive the error message Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state my code
import React, { useState} from "react";
import { useToasts } from "react-toast-notifications";
const Index = () => {
const [test, setTest]= useState(true);
const { addToast } = useToasts();
function RenderToast() {
return (
<div>
{ addToast('message') }
</div>
)}
return (
<div>
{test && <RenderToast /> }
</div>
)
}
You cannot set state during a render. And I'm guessing that addToast internally sets some state.
And looking at the docs for that library, you don't explicitly render the toasts. You just call addToast and then the <ToastProvider/> farther up in the tree shows them.
So to make this simple example works where a toast is shown on mount, you should use an effect to add the toast after the first render, and make sure your component is wrapped by <ToastProvider>
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
addToast('message')
}, [])
return <>Some Content here</>
}
// Example app that includes the toast provider
const MyApp = () => {
<ToastProvider>
<Index />
</ToastProvider>
}
how i can display the toast based on a variable for exemple display toast after receive error on backend?
You simply call addToast where you are handling your server communication.
For example:
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
fetchDataFromApi()
.then(data => ...)
.catch(error => addToast(`error: ${error}`))
}, [])
//...
}

How to use Hooks inside useEffect?

I wrote a demo here:
import React, { useRef, useEffect, useState } from "react";
import "./style.css";
export default function App() {
// let arrRef = [useRef(), useRef()];
let _data = [
{
title: A,
ref: null
},
{
title: B,
ref: null
}
];
const [data, setData] = useState(null);
useEffect(() => {
getDataFromServer();
}, []);
const getDataFromServer = () => {
//assume we get data from server
let dataFromServer = _data;
dataFromServer.forEach((e, i) => {
e.ref = useRef(null)
});
};
return (
<div>
{
//will trigger some function in child component by ref
data.map((e)=>(<div title={e.title} ref={e.ref}/>))
}
</div>
);
}
I need to preprocess after I got some data from server, to give them a ref property. the error says 'Hooks can only be called inside of the body of a function component' . so I checked the document, it says I can't use hooks inside a handle or useEffect. so is there a way to achieve what I need?
update:
I need to create component base on DB data, so when I create a component I need to give them a ref , I need trigger some function written in child component from their parent component and I use ref to achieve that. that is why I need to pass a ref to child component.

React hooks component callback only has initial state

I am new to react hooks, however I have a problem that I would think is fairly straight forward. Here is my parent component:
import React, { useState, useEffect } from 'react';
import DragAndDrop from '../DragAndDrop';
import Attachment from './Attachment';
import API from '../../services/api';
import '../../styles/components/attachments.scss';
const api = API.create();
const Attachments = ({attachments, type, typeId}) => {
const [attachmentData, setAttachmentData] = useState([]);
useEffect(() => {
setAttachmentData(attachments);
}, [attachments])
function onUpload(files) {
if (typeId) {
api.AddAttachment(type, typeId, files).then(response => {
let newAttachments = response.data.data;
let newAttachmentData = attachmentData;
newAttachmentData = newAttachmentData.concat(newAttachments);
setAttachmentData(newAttachmentData);
});
}
}
return (
<div className="attachments">
<h3 className="attachments-title">Attachments</h3>
<DragAndDrop onUpload={onUpload} />
{attachmentData.map((attachment, index) => (
<Attachment key={index} attachment={attachment} />
))}
</div>
);
}
export default Attachments;
attachments is passed in from the parent component async, which is why I'm using the useEffect function.
This all works fine, and the child Attachment components are rendered when the data is received.
I have a callback onUpload which is called from DragAndDrop component:
import React, { useCallback } from 'react';
import {useDropzone} from 'react-dropzone';
import '../styles/components/dragAndDrop.scss';
const DragAndDrop = ({onUpload}) => {
const onDrop = useCallback(acceptedFiles => {
onUpload(acceptedFiles);
}, [])
const {getRootProps, getInputProps} = useDropzone({onDrop});
return (
<div>
<div {...getRootProps({className: 'dropzone'})}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
</div>
);
};
export default DragAndDrop;
My problem is, when the callback onUpload in the Attachments component is called, attachmentData is the initial value which is an empty array instead of the being populated with the attachments. In my onUpload function, I'm posting the new uploads to my API which then returns them in the same format as the rest of the attachments. I then want to concat these new attachments to the attachmentData. I need attachmentData to have it's filled in value within the callback. Why is the attachmentData the initial state []? How do I get this to work?
The problem is that you're accessing attachmentData in onUpload which becomes stale by the time you use it, so to get the latest attachmentData you can pass a callback function to you updater function setAttachmentData like this:
function onUpload(files) {
if (typeId) {
api.AddAttachment(type, typeId, files).then(response => {
let newAttachments = response.data.data;
setAttachmentData(prevAttachmentData => ([...prevAttachmentData, ...newAttachments]));
});
}
}
If you want to access the attachmentsData inside onUpload, you can do so by creating a ref and then updating that ref whenever attachmentsData changes, that way you won't have to pass a function to setAttachmentsData also:
const [attachmentsData, setAttachmentsData] = React.useState([]);
const attachmentsDataRef = React.useRef(attachmentsData);
// Update ref whenever state changes
useEffect(() => {
attachmentsDataRef.current = attachmentsData;
}, [attachmentsData]);
// Now in onUpload
function onUpload(files) {
// Here you can access attachmentsDataRef.current and you'll get updated state everytime
if (typeId) {
api.AddAttachment(type, typeId, files).then(response => {
let newAttachments = response.data.data;
setAttachmentData([...attachmentsDataRef.current, ...newAttachments]);
});
}
}

React Context : Get Data from API and call API whenever some events happens in React Component

I am new to React Context.
I need to call the API in react context to use its data throughout my react application. Also the same API needs to be called on some CRUD operation on various component of react application.
For now I am storing API data in redux which I don't want to store.
Here is what I have tried..
context.js File
import React, { useState, createContext,useEffect } from 'react';
import {getData} from './actionMethods';
const NewContext = createContext();
function newContextProvider(props) {
useEffect(async () => {
const {dataValue} = await getData()
console.log("Data " , dataValue)
}, [])
return (
<NewContext.Provider
value={{
state: {
},
actions: {
}
}}
>
{props.children}
</NewContext.Provider>
);
}
const newContextConsumer = newContext.Consumer;
export { newContextProvider, newContextConsumer, newGridContext };
actionMethods.js
export function getData() {
let config = getInstance('GET', `${prefix}/xyz/list`)
return axios(config).then(res => res.data).catch(err => {
console.log(err)
})
}
when any CRUD operation performs , I need to call the API from the context.js file to get the data from API and store in the context.
Any help would be great.
Thank You.
First we create the Context and pass it an initial value.
In order to fetch data and keep track of the returned value, we create a state inside the component. This component will manage the fetched data and pass it in the Context Provider.
To call an async function inside useEffect we need to wrap it and call it inside useEffect callback.
export const NewContext = createContext({
my_data: {} // Initial value
});
export const NewContextProvider = props => {
const [my_data, setMyData] = useState({});
useEffect(() => {
const fetchMyData = async () => {
const { dataValue } = await getData();
if (dataValue) {
setMyData(dataValue);
} else {
// There was an error fetching the data
}
};
fetchMyData();
}, []);
return (
<NewContext.Provider
value={{
my_data
}}
>
{props.children}
</NewContext.Provider>
);
};
To use this Context in a component we use the useContext hook. Remember that this component needs to be wrapped by the Provider we just created.
import React, { useContext } from "react";
import { NewContext } from "./NewContext"; // The file where the Context was created
export const MyComponent = props => {
const { my_data } = useContext(NewContext);
return //...
};
Let me know if something is not clear.

Categories