I have the following component called PrivateReview, that has event handlers for updating it's content.
export default function PrivateReview(props) {
const classes = useStyles();
const removeReviewAndReload = async () => {
await fetch(`/rest/review/${props.movieId}`, deleteHeaders);
props.onChange();
};
const updateRating = async (event, newRating) => {
event.preventDefault();
const data = {
rating: newRating,
};
await fetch(`/rest/rating/${props.movieId}`, {...updateHeaders, body: JSON.stringify(data)});
props.onChange();
};
const updateReview = async (event, newComment) => {
event.preventDefault();
const data = {
comment: newComment,
};
await fetch(`/rest/review/${props.movieId}`, {...updateHeaders, body: JSON.stringify(data)});
props.onChange();
};
return (
<Grid item xs={12}>
<Card style={{width: 1150, margin: 10}}>
<CardHeader
title={<Link href={`/Movie/${props.movieId}`} className={classes.title} style={{ fontSize: '30px' }}>{props.title}</Link>}
action={
<Box component="fieldset" mb={-1} borderColor="transparent" marginTop={5}>
<Rating name="read-only" precision={0.5} value={props.rating} onChange={updateRating} />
</Box>
}
/>
<CardContent>
{props.text}
</CardContent>
<CardActions className={classes.right}>
<EditReviewButton movieId={props.movieId} oldReview={props.text} onSubmit={updateReview}/>
<IconButton color="primary" component="span" className={classes.control} onClick={removeReviewAndReload}>
<DeleteIcon />
</IconButton>
<Button disabled color="secondary">
{props.postDate}
</Button>
</CardActions>
</Card>
</Grid>
);
}
I map the review data that I receive from a parent component and try to generate a list of rendered components.
const Reviews = reviews.map(({ movieName, movieId, comment, rating, post_date, user }) => {
return <PrivateReview
key={movieId}
title={movieName}
text={comment}
rating={rating}
postDate={post_date}
user={user.userId}
movieId={movieId}
onChange={props.reloadDashboardData}
/>;
});
My problem is that when updateRating gets called, props.movieId is set to the movieId of the first item in the list inside updateRating, and hence the first item in the list gets modified instead of the item that I want.
Outside of this particular function however, when I console.log(props.movieId) in the function body, props.movieId is set correctly. This is the case for the other update methods as well, such as removeReviewAndReload and updateReview.
I suspect that this has something to do with either closing over the wrong value, or the bare asynchronous fetch calls. Could anyone point out the incorrectness or what I'm doing wrong here?
Thanks
EDIT:
As suggested in the comments below, I've changed the key property used in the list, and created a somewhat minimal running version here:
https://codesandbox.io/s/purple-framework-382hq
There are 3 event handlers in PrivateReview.js, removeReview, updateReview, and updateRating. removeReview and updateReview correctly log the corresponding movieId of the review that they are associated with, but updateRating logs the movieId of the review at the top of the rendered list.
I've removed the async/await things in the linked sandbox, but the bug is still there, so I think can rule out problems with fetch and async stuff. I've updated the tags accordingly.
Thanks very much for your help so far!
EDIT 2:
Since I've super simplified the event handlers in the sandbox to the point where they are only console logging, I think it might have something to do with material-ui, or my component layout structure (the DOM thing but for components). I've added the material-ui tag to this question in case it has something to do with that.
Cheers
Thanks very much for your help and comments guys!
After minifying my example into the sandbox, and removing all the extraneous stuff, I found out the problem.
In my example, the name was set as such:
<Rating name="read-only" precision={0.5} value={props.rating} onChange={updateRating} />
However as stated here: https://material-ui.com/pt/api/rating/
The name attribute of the radio input elements. If readOnly is false,
the prop is required, this input name`should be unique within the
parent form.
Changing that to this:
<Rating name={`${props.movieId}`} precision={0.5} value={props.rating} onChange={updateRating} />
And everything works! Therefore I just needed to set the rating "name" attribute to a uniqueId associated with the review much in the same way as we need to set the key for components that are part of a list.
Cheers for your feedback!
Related
I'm stuck at a problem - I'm building a receipes app with Firebase Realtime. I've a working prototype, but I'm stuck with an issue where useEffect won't trigger a reload after editing the array [presentIngredients].
This how my presentIngredients is defined (note that presentIngredient is used to store the current user input before the user adds the ingredient. After that, the presentIngredient get's added to the presentIngredients!):
const [ presentIngredients, setPresentIngredients ] = useState([]);
const [ presentIngredient, setPresentIngredient ] = useState('');
My useEffect hook looks like that:
useEffect(() => {
console.log('useEffect called!')
return onValue(ref(db, databasePath), querySnapshot => {
let data = querySnapshot.val() || {};
let receipeItems = {...data};
setReceipes(receipeItems);
setPresentIngredients(presentIngredients);
})
}, [])
Here's my code to render the UI for adding/removing existing ingredients:
{ /* adding a to-do list for the ingredients */ }
<View style={styles.ingredientsWrapper}>
{presentIngredients.length > 0 ? (
presentIngredients.map((key, value) => (
<View style={styles.addIngredient} key={value}>
<Text>{key} + {value}</Text>
<TouchableOpacity onPress={() => removeIngredient(key)}>
<Feather name="x" size={24} color="black" />
</TouchableOpacity>
</View>
))
) : (
<Text style={styles.text}>No ingredients, add your first one.</Text>
)}
<View style={styles.addIngredientWrapper}>
<TextInput
placeholder='Add ingredients...'
value={presentIngredient}
style={styles.text}
onChangeText={text => {setPresentIngredient(text)}}
onSubmitEditing={addIngredient} />
<TouchableOpacity onPress={() => addIngredient()}>
<Feather name="plus" size={20} color="black" />
</TouchableOpacity>
</View>
</View>
And this is my function to delete the selected entry from my presentIngredients array or add one:
// update the ingredients array after each input
function addIngredient() {
Keyboard.dismiss();
setPresentIngredients(presentIngredients => [...presentIngredients, presentIngredient]);
setPresentIngredient('');
}
// remove items by their key
function removeIngredient(id) {
Keyboard.dismiss();
// splice (remove) the 1st element after the id
presentIngredients.splice(id, 1);
setPresentIngredients(presentIngredients);
}
The useEffect hook isn't triggered when adding an ingredient, however the change is instantly rendered on the screen. If I delete an item, the change isn't noticeable until I reload the screen - what am I doing wrong?
Note that all this is happening before data is send to Firebase.
Issues
There are a few overt issues I see with the code:
The code uses the array index as the React key, so if you mutate the array, i.e. add, remove, reorder, etc... the index values won't be the same as they were on a previous render cycle per array element. In other words, the React key is the same regardless what value is now at any given index and React likely bails on rerendering.
The removeIngredient callback handler is mutating the existing state instead of creating a new array reference.
Solution
Using the array index as a React key is bad if you are actively mutating the array. You want to use React keys that are intrinsically related to the data so it's "sticky" and remains with the data, not the position in the array being mapped. GUIDs and other object properties that provide sufficient uniqueness within the data set are great candidates.
presentIngredients.map((el, index) => (
<View style={styles.addIngredient} key={el.id}>
<Text>{el.key} + {index}</Text>
<TouchableOpacity onPress={() => removeIngredient(el.id)}>
<Feather name="x" size={24} color="black" />
</TouchableOpacity>
</View>
));
Use Array.prototype.filter to remove an element from an array and return a new array reference.
function removeIngredient(id) {
Keyboard.dismiss();
setPresentIngredients(presentIngredients => presentIngredients.filter(
el => el.id !== id
));
}
Note above that I'm assuming the presentIngredients data element objects have a GUID named id.
Listening for state updates
The useEffect hook isn't triggered when adding an ingredient, however
the change is instantly rendered on the screen.
The single useEffect only exists to run once when the component mounts to fetch the data and populate the local state. If you want to then issue side-effects when the state updates later you'll need a second useEffect hook with a dependency on the state to issue the side-effect.
Example:
useEffect(() => {
console.log("presentIngredients updated", { presentIngredients });
// handle side-effect like updating the fire store
}, [presentIngredients]);
you need to create another useEffect, because the first useEffect need to be called only once (because you are setting a watcher).
useEffect(() => {
// this effect will be executed when presentIngredients change
}, [presentIngredients]);
if you don't remember how the array of dependencies work
you can check the explanation here
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
I'm using axios to return a map to my main app where it will be distributed to other values in the program. I am having an issue though. When I use 'onClick' on a drop down select, I want it to call that external function to return the JSON string and save it to a variable but it won't do it. I have console logged it and it says my variable is use undefined. Here is my axios code
import axios from "axios";
// ** when you launch server. Make sure 'express stuff' server is working and that it is posting to 5000/loadCycle
function Parent() {
let data = null;
console.log("called");
const url = "http://localhost:5000/";
axios
.get(`${url}loadCycle`)
.then((response) => {
data = response.data.workflow;
data = JSON.stringify(data);
data = JSON.parse(data);
//console.log(data);
const map = new Map(Object.entries(data));
console.log(map);
return map;
})
.catch((error) => console.error(`Error: ${error}`));
}
export default Parent;
and here is the code I want to format
function App() {
let dataCollection = null;
return (
<div>
<Box
sx={{ display: "flex", width: "40%", justifyContent: "space-between" }}
>
<Box sx={{ display: "flex" }}>
{/* <Typography sx={{ paddingTop: "6%" }}>Cycle</Typography> */}
{/* Cycle dropdown menu */}
{/* // MAKE CHANGES ON BRANCH // */}
<FormControl
sx={{ m: 1, minWidth: 200 }}
size="small"
variant="standard"
>
<InputLabel>Cycles</InputLabel>
<Select>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10} onClick={dataCollection=Parent()}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
{/* cycle dropdown menu end */}
</Box>
</div>
)
Why won't selecting 'one' from my select update dataCollection from 'null' to the map I am trying to return to it. Console logging it shows that the 'map' data in Parent is correct but the log for dataCollection is 'undefined'
<MenuItem value={10} onClick={dataCollection=Parent()}>Ten</MenuItem>
First of all you didn't define function (you are tried to do it like in a vainilla js, but react don't work in this way)
So, let's define separate function:
const handleSave = () => {
dataCollection=Parent()
}
// ...
<MenuItem value={10} onClick={handleSave}>Ten</MenuItem>
Next problem that's what your Parent function isn't synchronous, you should return your axios promise and after that use this function like that:
Parent().then(data => {
dataCollection = data;
})
That's not all, we can't save data at dataCollection, because your functional component this is like render function and you will lose your data on next render, so you shoud save your data to ref or state (depending on the purpose of use), let's use state:
const [dataCollection, setDataCollection] = React.useState();
// ...
const handleSave = () => {
Parent().then(data => {
setDataCollection(data);
})
}
Besides this I can see some style issues. And looks like you haven't read react doc attentively, please read againg "state and props" and "lifecycle" subjects from docs.
You have a couple of issues with your approach. I'm not sure what the other components: Box, FormControl, InputLabel, Select, and MenuItem are doing, so it makes it harder to discern if they are functioning correctly. I would simplify the code and just use regular HTML select and option tags. The select tag receives a change event and with React all events can be prefixed with "on", so it would be onChange on the select tag.
Create a prototype, a simpler project, that just focuses on that functionality until you understand it for your needs. Also, practice naming constructs a bit better, as Parent doesn't convey what it is doing. Aim to be succinct and general.
I have a problem where useState updates the state but does not show the changes until I refresh the app. First, I declare an array called sampleFriends, made up of objects with fields "name", "location", and "picture" (each element of the array looks similar to: {name: 'John', location: 'Boston', picture: TestImage}).
Then, I have the following useState:
const [selectedFriends, setSelectedFriends] = useState([])
At some point, I successfully render
sampleFriends.map(({ name, location, image }, index) => (
<NewMsgTableRow
name={name}
index={index}
location={location}
image={image}
onPress={() => selectFriend(name)}
/>
))
And I also have this function right above
const selectFriend = name => {
// if the friend is not already selected
if (!selectedFriends.find(e => e === name)) {
const newFriends = selectedFriends
newFriends.push(name)
setSelectedFriends(newFriends)
}
}
The component NewMsgTableRow has a button that uses onPress
<TouchableOpacity
onPress={onPress}
>
So, I want to render selectedFriends as soon as they are selected (the TouchableOpacity is touched and thus the state updates). However, when I click the button, nothing shows up until I edit and save my code and it refreshes automatically. It was my understanding that useState rerendered the components as soon as it was updated, but it is not happening in this case and I can't figure out why. I've been reading that it is async and that it does not change it instantly, but I don't know how to make it work. Hope it makes sense and thanks for your help!!
You can use array spread or Array.concat() to make a shallow clone, and add new items as well) so change the line below:
const newFriends = selectedFriends
to this line :
const newFriends = [...selectedFriends]
I have an Autocomplete component that is required to load a massive data list (up to 6000 elements) and show suggestions accordingly to the user's input.
As the data options have so many elements, whenever the user starts typing in a slow computer, it slows down and requires some time to load everything. I have to prevent it, so I came with an idea to show the user suggestions after they typed the third character. It's even giving me this error whenever the user clicks on the input box:
Warning: React instrumentation encountered an error: RangeError: Maximum call stack size exceeded console.
I need to show the suggestions after the third character input. I have tried to use the getOptionDisabled suggestion and the limitTags, but they did not work.
Here is the code:
const NameSelect = (props) => {
return (
<Dialog>
<React.Fragment>
<DialogTitle id="search-name-dialog-title">
Search name
</DialogTitle>
<DialogContent>
<Autocomplete
id="combo-box-client-select"
options={props.NameList}
value={props.preSelectedName}
getOptionLabel={(option) =>
option.Name_name +
", " +
option.country +
", " +
option.city
}
onChange={(object, value) => {
props.preSelectedNameSet(value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Search name"
variant="outlined"
fullWidth
/>
)}
/>
.
.
.
</Dialog>
);
};
Can someone please help me with that approach, or suggest a better one? Thanks!
Try something like this:
<Autocomplete
inputValue={inputValue}
onInputChange={(e) => setinputValue(event.target.value)}
id="combo-box-demo"
options={values}
getOptionLabel={(option) => option}
style={{ width: 300 }}
renderInput={(params) => (
<TextField {...params} label="Combo box" variant="outlined" />
)}
open={inputValue.length > 2}
/>
Use InputValue prop to trigger the auto complete drop down.
Example : auto complete dropdown
My idea is to add a state for Autocomplete current value to watch for its autoComplete property. That state will look something like this:
const [currentValue, useCurrentValue] = useState(props.preSelectedName);
so that your component will look something like this:
<Autocomplete
autoComplete={currentValue.length >= 3 ? true : false}
onChange={useCurrentValue}
...your other properties
/>
Another idea: you might wanna use setTimeout in your onChange method to slow down the re-render. But don't forget to clearTimeout before you set them.
The feature that you require is known as "Debouncing" and it is used whenever time consuming tasks occur frequently. In your case it, everytime you type the key, the suggestions are computed and this will definetely lead to lagging.
Lodash's debounce function will implement this for you.
As far as my knowledge, I am not sure whether you can implement this with MUI Autocomplete, but a custom solution you can do something like this:-
import React, { useState, useCallback } from "react";
import { _ } from "lodash";
function AutoComplete() {
const [input, setInput] = useState("");
const [suggestions, setSuggestions] = useState([]);
const updateInput = (input) => {
setInput(input);
/*_.debounce will fire the setSuggestions
and fetchSuggestions only after a gap of 3000ms */
_.debounce((input) => setSuggestions(fetchSuggestions(input), 3000));
};
return (
<div>
<input
value={input}
class="input"
onChange={(event) => updateInput(event.target.value)}
/>
<div class="suggestions">
<ul>
{suggestions?.length > 0 &&
suggestions?.map((val, idx) => (
<li class="suggestion" key={idx}>
{val}
</li>
))}
</ul>
</div>
</div>
);
}
export default AutoComplete;
You can style the components using the appropriate styles and materialize.css so that you get a functional replica of the Autocomplete component of MUI.
I'm trying to create a subtable of the main React Material-Table.
Everything is working properly as it should work, details panel (subtable) is showing on toggle icon press.
Are there any ways to show it opened by default? I mean to remove the toggle icon and show the detailPanel right from the component render?
Here is how my mat-table looks like (I didn't want to insert the whole component code, cause it will be too much code, full code is in the sandbox):
<MaterialTable
icons={tableIcons}
tableRef={tableRef}
columns={tableColumns}
data={tableData}
onRowClick={(evt, selectedRow) =>
setSelectedRow(selectedRow.tableData.id)
}
title="Remote Data Example"
detailPanel={detailSubtable}
options={{
rowStyle: rowData => ({
backgroundColor:
selectedRow === rowData.tableData.id ? "#EEE" : "#FFF"
})
}}
/>
And a link to the Codesandbox
As per knowledge, there is not any proper way or props to achieve this but you can do native DoM manipulation.
Provide custom Icon in DetailPanel icon with some unique ID like this:
DetailPanel: forwardRef((props, ref) => (
<div id="my-id" style={{ display: "none" }}>
<ChevronRight {...props} ref={ref} />
</div>
)),
Now, On componentDidMount find this element and trigger a click event on it and hide parent node like this
useEffect(() => {
const myToggler = document.getElementById("my-id");
if (!!myToggler) {
myToggler.click();
myToggler.parentNode.style.display = "none";
}
}, []);
here is the working sandbox link forked from yours, let me know if I am missing something.
If you see the source code There is a props called defaultExpanded which should work but there is an open issue which is causing the issue of not opening the panel by default.
To make it work (until the issue is fixed), you can imperatively modify the material-table's component the state in the useEffect
Like this
useEffect(() => {
tableRef.current.state.data = tableRef.current.state.data.map(data => {
console.log(data);
data.tableData.showDetailPanel = tableRef.current.props.detailPanel;
return data;
});
}, []);
Working demo of your code
This solution works for any number of rows/detailsPanel.