I'm trying to make a search component with Material-UI Autocomplete.
The issue I am facing is that the data from the API doesn't reload. So when I load, it fetch results, but then, if I change the input, it doesn't make another request.
I tried calling the api function in the onChange function, but it still doesn't do anything.
I am still new to React Hooks, any feedback will be greatly appreciated.
import fetch from "cross-fetch";
import React from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import CircularProgress from "#material-ui/core/CircularProgress";
function sleep(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
export default function Asynchronous() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState([]);
const [input_var, setInput] = React.useState("");
const loading = open && options.length === 0;
const api = React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
var response = await fetch(`${baseUrl}/users?q=${input_var}`, {
method: "get",
headers: {
"Content-Type": "application/json",
Authorization: auth
}
});
await sleep(1e3); // For demo purposes.
const countries = await response.json();
console.log(countries);
countries.items.map((item) => console.log(item.name));
if (active) {
setOptions(countries.items);
}
})();
return () => {
active = false;
};
}, [loading, input_var]);
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.name === value.name}
getOptionLabel={(option) => option.name}
options={options}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
onChange={async (newValue) => {
await api;
await setInput(newValue);
}}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
);
}
Related
Now I have created this custom hook to perform lazy loading,which takes redux slice action as input and
import { useState, useEffect, useCallback, useRef } from "react";
import { useDispatch } from "react-redux";
function useLazyFetch(fetchAction) {
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const loadMoreRef = useRef(null);
const handleObserver = useCallback(async(entries) => {
const [target] = entries;
console.log(target.isIntersecting);
if (target.isIntersecting) {
console.log("INTERSECTING.....");
await new Promise((r) => setTimeout(r, 2000));
setPage((prev) => prev + 1);
}
}, []);
useEffect(() => {
const option = {
root: null,
rootMargin: "0px",
threshold: 1.0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
}, [handleObserver]);
const fetchApi = useCallback(async () => {
try {
setLoading(true);
await new Promise((r) => setTimeout(r, 2000));
dispatch(fetchAction(page))
setLoading(false);
} catch (err) {
console.error(err);
}
}, [page,fetchAction,dispatch]);
useEffect(() => {
fetchApi();
}, [fetchApi]);
return { loading, loadMoreRef };
}
export default useLazyFetch;
I am using this in my component like this, here you can see I am tracking div in the bottom using loadMoreRef from useLazyFetch, Now when I am commenting out the fetchApi(); from custom hook its working as expected, on scroll its logging INTERSECTING... in the console but the moment I try to execute the action through fetchApi() my whole app goes into loop,the div tracker with ref comes to top and it fetches the posts but after immediately that action repeats the tracker comes to top and page becomes empty & it fetches next set of posts,I can see that my list is getting appended new set of posts to state in redux dev tool instead of completely setting new state, but in UI it's rendering all posts again and again whic is causing the loop,how can I avoid this ?
import { CircularProgress, Grid, IconButton, Typography } from "#mui/material";
import { Box } from "#mui/system";
import React, { useEffect,useRef,useState } from "react";
import AssistantIcon from "#mui/icons-material/Assistant";
import Post from "../components/Post";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../redux/postSlice";
import AddPost from "../components/AddPost";
import useLazyFetch from "../hooks/useLazyFetch";
export default function Home() {
const dispatch = useDispatch();
// const api = `https://picsum.photos/v2/list`
const { status, posts } = useSelector((state) => state.post);
const {loading,loadMoreRef} = useLazyFetch(getPosts)
useEffect(() => {
dispatch(getPosts());
}, []);
return (
<Box>
<Box borderBottom="1px solid #ccc" padding="8px 20px">
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<Typography variant="h6">Home</Typography>
</Grid>
<Grid item>
<IconButton>
<AssistantIcon />
</IconButton>
</Grid>
</Grid>
</Box>
<Box height="92vh" sx={{ overflowY: "scroll" }}>
<AddPost />
<Box textAlign="center" marginTop="1rem">
{status === "loading" && (
<CircularProgress size={20} color="primary" />
)}
</Box>
{status === "success" &&
posts?.map((post) => <Post key={post._id} post={post} />)}
<div style={{height:"50px",width:"100px",backgroundColor:"red"}} ref={loadMoreRef}>{loading && <p>loading...</p>}</div>
</Box>
</Box>
);
}
And here is my redux action & state update part
const initialState = {
status: "idle",
posts: []
};
export const getPosts = createAsyncThunk("post/getPosts", async (page) => {
console.log(page);
console.log("calling api ...");
const { data } = await axios.get(`/api/posts?page=${page}`);
return data;
});
export const postSlice = createSlice({
name: "post",
initialState,
reducers: {},
extraReducers: {
[getPosts.pending]: (state, action) => {
state.status = "loading";
},
[getPosts.fulfilled]: (state, action) => {
state.status = "success";
state.posts = [...state.posts,...action.payload.response.posts] ;
},
[getPosts.rejected]: (state, action) => {
state.status = "failed";
},
}
this is the solution that is working
import { CircularProgress, Grid, IconButton, Typography } from "#mui/material";
import { Box } from "#mui/system";
import React, { useEffect,useMemo } from "react";
import AssistantIcon from "#mui/icons-material/Assistant";
import Post from "../components/Post";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../redux/postSlice";
import AddPost from "../components/AddPost";
import useLazyFetch from "../hooks/useLazyFetch";
export default function Home() {
const { status, posts } = useSelector((state) => state.post);
const {loading,loadMoreRef} = useLazyFetch(getPosts)
const renderedPostList = useMemo(() => (
posts.map((post) => {
return( <Post key={post._id.toString()} post={post} />)
})
), [posts])
return (
<Box>
<Box borderBottom="1px solid #ccc" padding="8px 20px">
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<Typography variant="h6">Home</Typography>
</Grid>
<Grid item>
<IconButton>
<AssistantIcon />
</IconButton>
</Grid>
</Grid>
</Box>
<Box height="92vh" sx={{ overflowY: "scroll" }}>
<AddPost />
<Box textAlign="center" marginTop="1rem">
{status === "loading" && (
<CircularProgress size={20} color="primary" />
)}
</Box>
{renderedPostList}
<div style={{height:"50px",width:"100px",backgroundColor:"red"}} ref={loadMoreRef}>{loading && <p>loading...</p>}</div>
</Box>
</Box>
);
}
}
I used useMemo hook to memoize and it works as expected
I am using ActivityIndicator for showing the loading screen while my dispatch function dispatches the action and fetches the products from the firebase and renders it on my app screens But this is not happening. My app is showing products that are in store as dummy data and if I refresh the screen then it shows the data from firebase but not the loading spinner to show that loading is true.
ProductOverviewScreen.js:
import React, { useState, useEffect, useCallback } from "react";
import {
FlatList,
View,
Button,
Text,
StyleSheet,
Platform,
ActivityIndicator,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import ProductItem from "../../components/shop/ProductItem";
import * as cartActions from "../../store/actions/cart";
import * as productActions from "../../store/actions/products";
import CustomHeaderButton from "../../components/UI/HeaderButton";
import Colors from "../../constants/Colors";
const ProductOverviewScreen = (props) => {
const [IsLoading, setIsLoading] = useState();
const [IsRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState();
const products = useSelector((state) => state.products.availableProducts);
const dispatch = useDispatch();
const loadedProducts = useCallback(() => {
setError(null);
setIsRefreshing(true);
dispatch(productActions.fetchProducts())
.then(setIsLoading(false))
.catch((err) => {
setError(err.message);
});
setIsRefreshing(false);
}, [dispatch, setIsLoading, setError]);
useEffect(() => {
const willFocusSub = props.navigation.addListener(
"willFocus",
loadedProducts
);
return () => {
willFocusSub.remove();
};
}, [loadedProducts]);
useEffect(() => {
const loading = async () => {
setIsLoading(true);
await loadedProducts();
setIsLoading(false);
};
}, [dispatch, loadedProducts]);
const selectItemHandler = (id, title) => {
props.navigation.navigate("ProductDetail", {
productId: id,
productTitle: title,
});
};
const addToCartHandler = async (itemData) => {
setIsLoading(true);
await dispatch(cartActions.addToCart(itemData.item));
setIsLoading(false);
};
if (error) {
return (
<View style={styles.loadingSpiner}>
<Text>An Error occurred! </Text>
<Button
title="Try Again"
onPress={loadedProducts}
color={Colors.primary}
/>
</View>
);
}
if (IsLoading) {
return (
<View style={styles.loadingSpiner}>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
);
}
if (!IsLoading && products.length === 0) {
return (
<View style={styles.loadingSpiner}>
<Text>No Product Found!</Text>
</View>
);
}
return (
<FlatList
data={products}
onRefresh={loadedProducts}
refreshing={IsRefreshing}
renderItem={(itemData) => (
<ProductItem
image={itemData.item.imageUrl}
title={itemData.item.title}
price={itemData.item.price}
onSelect={() => {
selectItemHandler(itemData.item.id, itemData.item.title);
}}
>
<Button
color={Colors.primary}
title="View Details"
onPress={() => {
selectItemHandler(itemData.item.id, itemData.item.title);
}}
/>
{IsLoading ? (
<ActivityIndicator size="small" color={Colors.primary} />
) : (
<Button
color={Colors.primary}
title="To Cart"
onPress={() => {
addToCartHandler(itemData);
}}
/>
)}
</ProductItem>
)}
/>
);
};
ProductOverviewScreen.navigationOptions = (navigationData) => {
return {
headerTitle: "All Products",
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="Menu"
iconName={Platform.OS === "android" ? "md-menu" : "ios-menu"}
color={Platform.OS === "android" ? Colors.primary : "white"}
onPress={() => {
navigationData.navigation.toggleDrawer();
}}
/>
</HeaderButtons>
),
headerRight: () => (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="Cart"
iconName={Platform.OS === "android" ? "md-cart" : "ios-cart"}
onPress={() => {
navigationData.navigation.navigate("Cart");
}}
/>
</HeaderButtons>
),
};
};
const styles = StyleSheet.create({
loadingSpiner: {
flex: 1,
justifyContent: "center",
alignItems: "center",
opacity: 1,
},
});
export default ProductOverviewScreen;
I have also checked on both emulators IOS and android also on my real device. If I open the app on my real device then instantly app renders the data from the firebase but doesn't show a loading spinner.
In useEffect If I try to add dependency loading which costs async code and a function which fetches the data from firebase then it shows an error saying Can't find variable: loading.
Please making loadedProducts from sync to async
I have created a common table component in which I am calling in the file that are needed under some table there is an field action which have an edit and delete icon whenever I click on edit icon it should trigger a dropdown that will allow to update the status of component. But when I click on edit icon the table component will refresh so the menu is not opening at suitable position. Please help me
TableComponent
import React, { useState } from 'react';
import { Table, TableHead, TableRow, TableCell, makeStyles, TablePagination, TableSortLabel } from '#material-ui/core';
import PropTypes from 'prop-types';
const useStyles = makeStyles(theme => ({
table: {
marginTop: theme.spacing(3),
'& thead th': {
fontWeight: '600',
color:'white',
backgroundColor: '#143174',
},
'& tbody td': {
fontWeight: '300',
},
},
}))
export default function TableComponent(records, headCells,filterFn) {
const classes = useStyles();
const pages = [10, 50, 100]
const [page, setPage] = useState(0)
const [rowsPerPage, setRowsPerPage] = useState(pages[page])
const [order, setOrder] = useState()
const [orderBy, setOrderBy] = useState()
const TblContainer = props => (
<Table className={classes.table}>
{props.children}
</Table>
)
TblContainer.propTypes = {
children:PropTypes.element
}
const TblHead = props => {
const handleSortRequest = cellId => {
const isAsc = orderBy === cellId && order === "asc";
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(cellId)
}
return (<TableHead>
<TableRow>
{
headCells.map(headCell => (
<TableCell key={headCell.id}
sortDirection={orderBy === headCell.id ? order : false}>
{headCell.disableSorting ? headCell.label :
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={() => { handleSortRequest(headCell.id) }}>
{headCell.label}
</TableSortLabel>
}
</TableCell>))
}
</TableRow>
</TableHead>)
}
const handleChangePage = (event, newPage) => {
setPage(newPage);
}
const handleChangeRowsPerPage = event => {
setRowsPerPage(parseInt(event.target.value, 10))
setPage(0);
}
const TblPagination = () => (<TablePagination
component="div"
page={page}
rowsPerPageOptions={pages}
rowsPerPage={rowsPerPage}
count={records.length}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>)
function stableSort(array, comparator) {
const stabilizedThis = array.map((el, index) => [el, index]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}
function getComparator(order, orderBy) {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
function descendingComparator(a, b, orderBy) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
const recordsAfterPagingAndSorting = () => {
return stableSort(filterFn.fn(records), getComparator(order, orderBy))
.slice(page * rowsPerPage, (page + 1) * rowsPerPage)
}
return {
TblContainer,
TblHead,
TblPagination,
recordsAfterPagingAndSorting
}
}
ParentCoponent
import React,{ useState, useEffect, useCallback } from 'react';
import { Button, Typography, Grid, TextField, MenuItem, Menu, IconButton, Link, TableBody, TableRow, TableCell, Paper, makeStyles } from '#material-ui/core';
import Modals from './Components/Modals';
import DropDownComponent from './Components/DropDownComponent';
import PropTypes from 'prop-types';
import { fetchImage,postImage, removeImage, updateImageStatus } from './app/ActionCreator';
import { connect } from 'react-redux';
import EditStatusComponent from './Components/EditStatusComponent';
import { Edit, Delete } from '#material-ui/icons';
import TableComponent from './Components/TableComponent';
const styles = makeStyles(theme => ({
root:{
marginTop:'60px',
},
typo:{
flexGrow:1,
textAlign:'center',
marginTop:'30px',
color:'black'
},
headingpaper:{
width:'100%',
height:40,
marginTop:'30px',
background:'white',
borderColor:'white',
},
buttonColor:{
color:'white',
background:'#143174'
},
formControl: {
margin:'8px',
minWidth: 120,
},
selectEmpty: {
marginTop: '16px',
},
formRoot:{
margin:'8px',
width: '25ch',
},
modalButton:{
color:'white',
background:'#143174',
marginRight:'10px'
}
}))
const headCells = [
{id:1,label:'SlNo'},
{id:2,label:'Virtual Machine Image '},
{id:3,label:'createdAt'},
{id:4,label:'createdBy'},
{id:5,label:'IpAddress'},
{id:6,label:'Status'},
{id:7,label:'Action',disableSorting:true}
]
function AdminImages(props){
const [imageModal,setImageModal] = useState(false)
const [name,setName] = useState('')
const [cpu,setCpu] = useState('')
const [ram,setRam] = useState('')
const [disk,setDisk] = useState('')
const [imageid,setImageid] = useState('')
const [anchorEl,setAnchorEl] = useState(null)
const [filterFn, setFilterFn] = useState({ fn: items => { return items; } })
const handlecpuChange = (event) => {
setCpu(event.target.value)
}
const handleramChange = (event) => {
setRam(event.target.value)
}
const handlediskChange = (event) => {
setDisk(event.target.value)
}
useEffect(() => {
props.fetchImageData()
},[])
const handleMenu = useCallback((event,id) => {
setAnchorEl(event.currentTarget)
setImageid(id)
},[])
const handleClose = (ev) => {
setAnchorEl(null)
if(ev.target.innerText !== ''){
const data = {
"status":ev.target.innerText
}
props.updateImageStatusData(imageid,data)
}
}
const newInstance = () => {
if(name !== '' && cpu !== '' && ram !== '' && disk !== ''){
props.postImageData(name,cpu,ram,disk)
}
else {
alert("Please Enter All Field")
}
setImageModal(false)
}
const labstatus = (status) => {
if(status === 'Resuming' || status === 'Running'){
return(
<EditStatusComponent
status={status}
statuscolor='#32CD32'
showDropDown={false}/>
)
}
else if(status === 'Suspending' || status === 'suspending' || status === 'error'){
return(
<EditStatusComponent
status={status}
statuscolor='red'
showDropDown={false}/>
)
}
else{
return(
<EditStatusComponent
status={status}
statuscolor='#143174'
showDropDown={false}/>
)
}
}
const handleSearch = e => {
let target = e.target;
setFilterFn({
fn: items => {
if (target.value == "")
return items;
else
return items.filter(x => x.instance_name.toLowerCase().includes(target.value))
}
})
}
const classes = styles()
const { TblContainer, TblHead, TblPagination, recordsAfterPagingAndSorting } = TableComponent(props.Images.images,headCells,filterFn)
return(
<div className={classes.root}>
<Grid container direction="row" justify="flex-end" alignItems="flex-start">
<Button variant="contained" className={classes.buttonColor} onClick={() => setImageModal(true)}>
Create Images
</Button>
</Grid>
<Typography variant="h2" noWrap className={classes.typo}>
Virtual Machine Image
</Typography>
<Paper>
<TblContainer>
<TblHead/>
<TableBody>
{
recordsAfterPagingAndSorting().map((v,i) => (
<TableRow key={v.id}>
<TableCell>{v.id}</TableCell>
<TableCell>{v.instance_name}</TableCell>
<TableCell>{v.created_at}</TableCell>
<TableCell>{v.created_by.first_name + v.created_by.last_name}</TableCell>
<TableCell><Link target={"_blank"} onClick={() => window.open(`http://${v.ip_address}/project`,'_blank')}>{v.ip_address}</Link></TableCell>
<TableCell>{labstatus(v.status)}</TableCell>
<TableCell>
<div style={{display:'flex'}}>
<IconButton
aria-controls="simple-menu"
aria-haspopup="true"
onClick={(e)=> handleMenu(e,v.id)}>
<Edit/>
</IconButton>
<IconButton onClick={() => props.removeImageData(v.id)}>
<Delete/>
</IconButton>
</div>
</TableCell>
</TableRow>
))
}
</TableBody>
</TblContainer>
<TblPagination/>
</Paper>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}>
<MenuItem value="Resume" onClick={handleClose}>Resume</MenuItem>
<MenuItem value="Suspend" onClick={handleClose}>Suspend</MenuItem>
<MenuItem value="Freeze" onClick={handleClose}>Freeze</MenuItem>
</Menu>
<div>
<Modals
show={imageModal}
title="Create Instance"
handleClose={() => setImageModal(true)}
showSubmit={true}
onPress={newInstance}
size="xs">
<TextField
id="standard-basic"
label="Name"
style={{width:'50ch',margin:10}}
value={name}
onChange={(e) => setName(e.target.value)}
InputLabelProps={{
shrink: true,
}}/>
<DropDownComponent
dropdownid="standard-select-cpu"
selectedValue={cpu}
handleChange={handlecpuChange}
dropdownLabel="CPU">
<MenuItem value="8">8</MenuItem>
<MenuItem value="10">10</MenuItem>
<MenuItem value="12">12</MenuItem>
<MenuItem value="14">14</MenuItem>
<MenuItem value="16">16</MenuItem>
<MenuItem value="18">18</MenuItem>
<MenuItem value="20">20</MenuItem>
<MenuItem value="22">22</MenuItem>
<MenuItem value="24">24</MenuItem>
<MenuItem value="26">26</MenuItem>
<MenuItem value="28">28</MenuItem>
<MenuItem value="30">30</MenuItem>
<MenuItem value="32">32</MenuItem>
</DropDownComponent>
<DropDownComponent
dropdownid="standard-select-ram"
selectedValue={ram}
handleChange={handleramChange}
dropdownLabel="RAM">
<MenuItem value="16">16</MenuItem>
<MenuItem value="20">20</MenuItem>
<MenuItem value="24">24</MenuItem>
<MenuItem value="28">28</MenuItem>
<MenuItem value="32">32</MenuItem>
<MenuItem value="36">36</MenuItem>
<MenuItem value="40">40</MenuItem>
<MenuItem value="44">44</MenuItem>
<MenuItem value="48">48</MenuItem>
<MenuItem value="52">52</MenuItem>
<MenuItem value="56">56</MenuItem>
<MenuItem value="60">60</MenuItem>
<MenuItem value="64">64</MenuItem>
</DropDownComponent>
<DropDownComponent
dropdownid="standard-select-disk"
selectedValue={disk}
handleChange={handlediskChange}
dropdownLabel="Disk">
<MenuItem value="50">50</MenuItem>
<MenuItem value="100">100</MenuItem>
<MenuItem value="150">150</MenuItem>
<MenuItem value="200">200</MenuItem>
<MenuItem value="250">250</MenuItem>
<MenuItem value="300">300</MenuItem>
<MenuItem value="350">350</MenuItem>
<MenuItem value="400">400</MenuItem>
<MenuItem value="450">450</MenuItem>
<MenuItem value="500">500</MenuItem>
</DropDownComponent>
</Modals>
</div>
</div>
)
}
const mapStateToProps = state => {
return {
Images:state.Images,
}
}
const mapDispatchToProps = dispatch => ({
fetchImageData:() => {
dispatch(fetchImage())
},
postImageData:(name,cpu,ram,storage) => {
dispatch(postImage(name,cpu,ram,storage))
},
removeImageData:(id) => {
dispatch(removeImage(id))
},
updateImageStatusData:(id,data) => {
dispatch(updateImageStatus(id,data))
}
})
AdminImages.propTypes = {
classes:PropTypes.object.isRequired,
Images:PropTypes.object,
fetchImageData:PropTypes.func,
postImageData:PropTypes.func,
removeImageData:PropTypes.func,
updateImageStatusData:PropTypes.func,
}
export default connect(mapStateToProps,mapDispatchToProps)(AdminImages)
You can prevent unnecessary re-renders of a child component in 2 ways
If your component is class based then you can extend React.PureComponent or implement shouldComponentUpdate lifecycle method by yourself.
class MyComponent extends React.PureComponent {
// your component logic
}
class MyComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
//compare nextProps and this.props
//compare nextState and this.state
//return true for re-render, otherwise false
}
// your component logic
}
If you have a functional component you can wrap it with memo function that will check only for prop changes or you can pass it a function in the second argument that will do the current props and next props comparison and return true/false if you want do manual comparison just like shouldComponentUpdate
const MyComponent = React.memo(function(props) {
/* render using props */
});
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
Here are some useful links from the docs:
class component
function component
I am a beginner in Reactjs. I want to create an Autocomplete component where on every input change the API is hit and the options are updated accordingly. I am using the Autocomplete component provided by Material UI. As I understand, the example given here hits an API once and filters it locally. I tried using the InputChange props provided by material component. Also I found this anser - https://stackoverflow.com/a/59751227/8090336. But can't figure out the right way.
import Autocomplete from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
import {CircularProgress} from "#material-ui/core";
import debounce from 'lodash/debounce';
const SelectField = ({inputLabel}) => {
const [ open, setOpen ] = React.useState(false);
const [ options, setOptions ] = React.useState([]);
const [ inputValue, setInputValue ] = React.useState("");
const loading = open && options.length === 0;
const onInputChange = debounce((event, value) => {
console.log("OnInputChange",value);
setInputValue(value);
(async() => {
const response = await fetch('https://api.tvmaze.com/search/shows?q='+inputValue);
console.log("API hit again")
let movies = await response.json();
if(movies !== undefined) {
setOptions(movies);
console.log(movies)
}
})();
}, 1500);
return (
<Autocomplete
style={{ width:300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionLabel={(option) => option.show.name}
onInputChange={onInputChange}
options={options}
loading={loading}
renderInput={(params) => (<TextField
{...params}
label={inputLabel}
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} />: null }
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
}
export default SelectField;
I had faced this problem I manually called the API when ever the user types in. find the link for the sandbox. Check the onChange prop for the textfield rendered inside the autocomplete
// *https://www.registers.service.gov.uk/registers/country/use-the-api*
import fetch from "cross-fetch";
import React from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import CircularProgress from "#material-ui/core/CircularProgress";
export default function Asynchronous() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState([]);
const loading = open && options.length === 0;
const onChangeHandle = async value => {
// this default api does not support searching but if you use google maps or some other use the value and post to get back you reslut and then set it using setOptions
console.log(value);
const response = await fetch(
"https://country.register.gov.uk/records.json?page-size=5000"
);
const countries = await response.json();
setOptions(Object.keys(countries).map(key => countries[key].item[0]));
};
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.name === value.name}
getOptionLabel={option => option.name}
options={options}
loading={loading}
renderInput={params => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
onChange={ev => {
// dont fire API if the user delete or not entered anything
if (ev.target.value !== "" || ev.target.value !== null) {
onChangeHandle(ev.target.value);
}
}}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
);
}
I want to use an Autocomplete field for my React JS Project. For the design of the UI I use Material UI. In the documentation you can see the following example:
<Autocomplete
required
id="combo-box-demo"
filterOptions={(x) => x}
value={this.state.departure}
options={top100Films}
getOptionLabel={(option) => option.title}
renderInput={(params) => <TextField {...params} label="Startpunkt" variant="outlined" />}
/>
The options objects have the following default value:
let top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
For my purpose I want to dynamically change the options since I use an Rest API where I get the results for the input. My question is therefore how I can change the options dynamically when the user is typing.
You can use onInputChange prop in your case:
<Autocomplete
required
id='combo-box-demo'
filterOptions={(x) => x}
value={this.state.departure}
options={top100Films}
getOptionLabel={(option) => option.title}
onInputChange={(event: object, value: string, reason: string) => {
if (reason === 'input') {
changeOptionBaseOnValue(value);
}
}}
renderInput={(params) => (
<TextField {...params} label='Startpunkt' variant='outlined' />
)}
/>
Then you can define changeOptionBaseOnValue to handle your options.
You can check this example:
import fetch from 'cross-fetch';
import React from 'react';
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import CircularProgress from '#material-ui/core/CircularProgress';
function sleep(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
export default function Asynchronous() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState([]);
const loading = open && options.length === 0;
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
const response = await fetch('https://country.register.gov.uk/records.json?page-size=5000');
await sleep(1e3); // For demo purposes.
const countries = await response.json();
if (active) {
setOptions(Object.keys(countries).map((key) => countries[key].item[0]));
}
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.name === value.name}
getOptionLabel={(option) => option.name}
options={options}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
}
Source
I'm doing this as part of an address search/verification by using the OnChange in the text field with a handleAddressChange function that calls a findAddresses function. findAddresses uses Axios to make a call to an API, and then saves those results and displays them as the options for the results in the autocomplete.
Here's a simplified version of my code:
import React, { useState, ChangeEvent } from 'react';
import {
TextField,
InputAdornment
} from "#material-ui/core";
import Autocomplete from '#material-ui/lab/Autocomplete';
import { Search } from "#material-ui/icons";
import axios from "axios";
const AddressSearch = (props) => {
const [addressesList, setAddressesList] = useState([]);
const [inputAddress, setInputAddress] = useState<string>("");
const handleAddressChange = (event: ChangeEvent<{ value: unknown }>) => {
setInputAddress(event.target.value as string);
findAddresses(event.target.value as string);
};
const baseUrl = 'https://api.website.com/';
const findAddresses = (text?: string) => {
let params = `Key=value`
if (!!text) {
params += (`&Text=` + text);
let addressesResponse;
return (
axios.get(baseUrl + params)
.then(response => {
addressesResponse = response.data.Items
if (!Array.isArray(addressesResponse) || !addressesResponse.length) {
return;
}
setAddressesList(addressesResponse);
})
.catch(error => console.log(error))
)
}
}
return (
<div>
<Autocomplete
id="address-autocomplete"
freeSolo
options={addressesList}
getOptionLabel={(option) => option.Text}
popupIcon={<Search />}
renderInput={(params) => <TextField
id="address-input"
{...params}
onChange={handleAddressChange}
placeholder="Quickly find your address"
InputProps={{ ...params.InputProps,
startAdornment: (
<InputAdornment position="start"><Search /></InputAdornment>
)
}}
/> }
/>
</div>
);
}
export default AddressSearch;