How to use Hooks inside useEffect? - javascript

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.

Related

Call react hooks inside event handler

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.

calling method on sub component in React functional component via ref

I'm using syncfusion react controls to add some functionality to my app. I want to call a method on the control in my functional component, but I haven't been able to get the ref set properly:
import React, {createRef, useEffect, useState} from "react";
import {AutoCompleteComponent} from "#syncfusion/ej2-react-dropdowns";
import "#syncfusion/ej2-base/styles/bootstrap.css";
import "#syncfusion/ej2-react-inputs/styles/bootstrap.css";
import "#syncfusion/ej2-react-dropdowns/styles/bootstrap.css";
const UserLookup = ({userSelected}) => {
const [searchString, setSearchString] = useState('');
const [items, setItems] = useState([]);
const helper = new QueryHelper();
let listObj = createRef();
const searchStringChanged = (args) => {
console.log(args.text);
if (args.text.length > 3) {
setSearchString(args.text);
}
}
const optionSelected = (event) => {
memberSelected(event.item.id);
}
useEffect(() => {
fetch('http://example.com/myendpoint')
.then(response.map((result) => {
listObj.current.showPopup(); // <-- this method should be called on the autocomplete component
return {
id: result.contactId,
label: result.firstName + ' ' + result.lastName
}
}))
.then(data => console.log(data));
}, [searchString]);
return (
<AutoCompleteComponent
id="user_search"
autofill={true}
dataSource={items}
fields={
{
value: 'label'
}
}
filtering={searchStringChanged}
select={optionSelected}
popupHeight="250px"
popupWidth="300px"
placeholder="Find a contact (optional)"
ref={listObj}
/>
);
};
export default UserLookup;
this always throws an error that Cannot read property 'showPopup' of null -- how do you set the ref for the instance of the AutoCompleteComponent so that you can call methods on it?
We can get the reference for the AutoComplete when it's rendered as a functional component with help of using useRef method instead of createRef method. Please find the modified sample from below.
Sample Link: https://codesandbox.io/s/throbbing-shadow-ddsmf

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.

Destructured object as function parameter

I don’t understand the parameter of const Posts below. I’m fairly new to node/React. Is it a destructured parameter object? Or is it just an object being passed as a parameter?
getPosts and post are showing as undefined. But I don’t understand where the parameter object is being passed from into the function...
Full code here: https://github.com/bradtraversy/devconnector_2.0/blob/master/client/src/components/posts/Posts.js
Thanks in advance!!
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import PostForm from './PostForm';
import { getPosts } from '../../redux/actions/post';
const Posts = ({ getPosts, post: { posts, loading } }) => {
useEffect(() => {
getPosts();
}, [getPosts]); ```
So Posts is a React Function component.
All Function components will receive a props object as its first argument.
const Posts = (props) => { /* ... */ }
props will always be an object containing the props that were passed into it when the component was rendered, for example:
import Posts from './path/to/Posts'
function SomeParentComponent() {
return <Posts limit={10} categories={{news:true, sports:false}} />
}
In this case props will be an object that looks like this:
{
limit : 10,
categories : {
news : true,
sports : false,
}
}
You can of course destructure the props object in your component:
const Posts = (props) => {
const {
limit,
categories
} = props
// ... other stuff
}
But you can go even further and do what's called "unpacking" in order to destructure nested properties
const Posts = (props) => {
const {
limit,
categories : {
sports,
news
}
} = props
// ... other stuff
}
Lastly, instead of doing that in the function body, you can destructure and unpack objects in-line where the arguments are for the same result.
const Posts = ({limit, categories:{news,sports}}) => {
// ... other stuff
}
Which is what your code sample is doing.
It appears it's expecting the parent component to pass in a function as the getPosts prop, which when called will first set posts.loading to true, load the posts, then set posts.loading to false. Ex:
function SomeParentComponent() {
const [loading, setLoading] = useState(false)
const [posts, setPosts] = useState([])
const loadPosts = useCallback(async () => {
setLoading(true)
const loadedPosts = await loadPostsSomehow()
setPosts([posts, ...loadedPosts])
setLoading(false)
}, [])
return <Posts getPosts={loadPosts} post={{posts, loading}} />
}
Make sure to use useCallback to get a memoized callback here or you will get stuck in an infinite loop
**EDIT**
After actually looking at the link provided, it's slightly different actually. Instead of the post object being provided by the parent component, it's actually provided by redux, but the logic is essentially the same. The difference is that instead of the parent component changing the loading and post state, it's done via redux state management.
Yes, it is de-structured function parameter object.
In your case parameters to Posts pass through Redux connect() function.
const mapStateToProps = state => ({
post: state.post
});
export default connect(
mapStateToProps,
{ getPosts }
)(Posts);
Check your getPosts import and make sure it is not undefined.
import { getPosts } from '../../actions/post';
Also check your redux state and make sure it has state.post.

Categories