React parent component state updates but child does not re-render - javascript

I have a react app with many entries, each entry can have many tags.
It is a moderation app, so the entries are listed on a page and a user can click on an entry to moderate it (for example, to add or remove tags). Once clicked, the entry will show up in a modal.
Once the modal is open, a user can chain the entries with a 'next' button, so that the modal does not close. When the user clicks 'next', the next entry gets loaded into the modal.
In the modal, I have a react CreatableSelect component that takes the tag list of that loaded entry.
The issue is that when the user clicks 'next', the tags in the CreatableSelect don't update, it is still showing the tags of the first loaded entry.
Here is the code, transformed to make my issue hopefully clearer.
first, the component is loaded with an empty array of codes
second, the useEffect is triggered and populates the state with 2 dummy codes
Although when I console.log the state, it is correctly updated with the 2 dummy codes, the CreatableSelect still shows empty.
What I would like to understand is why the CreatableSelect does not rerender with the new state?
Thank you!
const SelectTags = ({ nextEntry, entry, topicId, updateEntry }) => {
const projectCodes = useSelector(state => state.project.codes);
const formatedCodes = projectCodes.map(code => ({value: code, label: code, isFixed: true}) );
const [selectedTags, setSelectedTags] = useState([]);
useEffect(() => {
const newTags = [{value: 'hello', label: 'hello'}, {value: 'world', label: 'world'}];
setSelectedTags([...newTags]);
}, [entry]);
const handleChange = newValue => setSelectedTags([...newValue]);
const setSubmittingFalse = () => setSubmitting(false);
return (
<CreatableSelect isMulti onChange={handleChange} options={formatedCodes} defaultValue={selectedTags} />
)
};
export default SelectTags;

Alright, switching the CreatableSelect props from defaultValue to value apparently solved that issue!
<CreatableSelect key={entry.id} isMulti onChange={handleChange} options={formatedCodes} value={tags} />

Related

Updating material ui select options from input

I'm new to Reactjs, here i have material ui select element, as you can see i have default values for select element, and also by clicking 'ADD USER' button and submitting, i can add new values to select element, and from select element i can also delete options, my question here is how can i edit specific option from select element, i have added EditUser component for that when option is clicked, but dont know how to update it, any advice ?
my code:
https://codesandbox.io/s/material-ui-multiple-select-with-select-all-option-forked-ysglz8?file=/src/AddUser.js
App.js:
import React, { useState } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import InputLabel from "#material-ui/core/InputLabel";
import ListItemIcon from "#material-ui/core/ListItemIcon";
import ListItemText from "#material-ui/core/ListItemText";
import MenuItem from "#material-ui/core/MenuItem";
import FormControl from "#material-ui/core/FormControl";
import Select from "#material-ui/core/Select";
import DeleteIcon from "#material-ui/icons/Delete";
import CreateIcon from "#material-ui/icons/Create";
import { MenuProps, useStyles } from "./utils";
import AddUser from "./AddUser";
import {
Button,
List,
ListItem,
Dialog,
DialogTitle,
DialogContent
} from "#material-ui/core";
import EditUser from "./EditUser";
function App() {
const rawOptions = [
"Oliver Hansen",
"Van Henry",
"April Tucker",
"Ralph Hubbard",
"Omar Alexander",
"Carlos Abbott",
"Miriam Wagner",
"Bradley Wilkerson",
"Virginia Andrews",
"Kelly Snyder"
];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [options, setOptions] = useState(rawOptions);
const [openAddModal, setOpenAddModal] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const handleChange = (event) => {
console.log("vals", event.target);
const value = event.target.value;
setSelected(value);
console.log("values", selected);
};
function addUser(newArray) {
setOptions(newArray);
}
const openAddUser = () => {
setOpenAddModal(true);
};
const openUpdateUser = (e) => {
e.stopPropagation();
setOpenUpdateModal(true);
};
const closeAddModal = () => {
setOpenAddModal(false);
};
const closeUpdateModal = () => {
setOpenUpdateModal(false);
};
const updateUser = (updateUser) => {
setOptions(updateUser);
};
return (
<FormControl className={classes.formControl}>
<div>
<InputLabel id="mutiple-select-label">Multiple Select</InputLabel>
<Select
labelId="mutiple-select-label"
multiple
variant="outlined"
value={selected || []}
onChange={handleChange}
renderValue={(selected) => selected}
MenuProps={MenuProps}
>
{options.map((option, index) => (
<MenuItem key={option.id} value={option}>
<ListItemIcon>
<Checkbox checked={selected?.includes(option)} />
</ListItemIcon>
<ListItemText primary={option.title}>{option}</ListItemText>
<DeleteIcon
onClick={(e) => {
e.stopPropagation();
setOptions(options.filter((o) => o !== option));
console.log("run");
}}
/>
<ListItemIcon>
<CreateIcon onClick={openUpdateUser} />
</ListItemIcon>
</MenuItem>
))}
</Select>
<Button onClick={openAddUser} style={{ backgroundColor: "#287B7A" }}>
Add User
</Button>
</div>
<p>{selected}</p>
<AddUser
openAddModal={openAddModal}
handleClose={closeAddModal}
array={options}
addUser={addUser}
/>
<EditUser
openUpdateModal={openUpdateModal}
handleClose={closeUpdateModal}
array={options}
updateUser={updateUser}
/>
</FormControl>
);
}
export default App;
Currently your users haven't any ids or something that they can be identified with.
Try to make users array of objects like this
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
Make your inputs and etc. accept array of objects.
After this in your Edit component, you should pass there selected user object and set default state for your input value (so your can really edit it and not input a new value)
const [value, setValue] = useState(props.user.name);
And in your someFunction, that acts like handleSubmit function, pass your user object, or user id and new value. It will look like
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.hanldeSubmit(props.user.id, value)
props.handleClose() // Might be better to put it into your handleSubmit in parent component
}
};
And finally in your App.js, create handleSubmit function that accepts user id and value, and modify your state in it. Find user by ID and put a new value.
Do not forget to pass this function into your EditUser component.
Hope that helped you!
UPDATE
Ooookay, so, you might also want to start with less hard examples, but lets stick to what we have. I'll note here some problems that I found, and explain how to make it work.
First of all, always name functions and variables correctly, you should understand what function or variable do only by its name (ideally), I understand that this is just an example code and etc, but this makes this point only more important, because when you learn something new, its good not to make it harder for yourself.
Second thing, just for some case, I don't know if this mistake or not, so I mention this:
<CreateIcon
onClick={() =>
openUpdateUser({ id: option.id, name: option.name })
}
/>
Here you pass object, and in openUpdateUser you accept e (event) as a first parameter. Just for you to know, you will get event in your anonymous function and it wouldn't be passed further in openUpdateUser, to pass it, you should write it like this:
<CreateIcon
onClick={(e) =>
openUpdateUser(e, { id: option.id, name: option.name })
}
/>
Okay, let's get back to business.
The first real problem here: You have your options in one state, and selected options in other, so when you add some user, you will see users from selected. What problem does it cause? When you will try to update user in your options, it might be updated, but you wouldn't see any changes in selected, because it two different states.
We will solve it by making one source of information. Now we will store in selected not users, but users ids and in render we will get users from our options by ids.
// before
<p>{selected}</p>
// after
{selected.map(selectedUserId => (
<div>{options.filter(option => option.id === selectedUserId)[0].name}</div>
))}
Now, any changes to options will affect your selected users. Also, update your code to add\remove ids and not user objects.
Let's go further, now you have your selected user and method for updating in edit component, let's go edit:
// EditUser.js
const [value, setValue] = useState(props.edit.name); // set user name as default value to edit it
function changeValue(e) {
setValue(e.target.value);
}
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.updateUser({id: props.edit.id, name: value}) // Pass user id and new value to our update function
props.handleClose();
}
};
So, now we have our new value in update function, the only one thing left is to save those updates. We'll do it in easy way:
// Normally here would be some api call for user update
const updateUser = (updateUser) => {
const temp = [...options] // Not deep copy of our options
temp[temp.findIndex(user => user.id === updateUser.id)].name = updateUser.name;
setOptions(temp)
};
And thats all, now it should work as expected.
Also as improve, you can restructure your options array of objects to make it easier to modify data.
(yep, I know that it was my suggestion, but anyway :) )
Currently it looks like this:
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
We can make it object of objects, where key will be id of user:
const rawOptions = {
0: {
id: 0,
name: "Oliver Hansen"
},
};
//Now to get user you can just do
options[userId]
// To get users array
Object.values(options)
// To modify user
const updateUser = (updateUser) => {
setOptions({...options}, [updateUser.id]: updateUser)
};
Just like previously, I wouldn't make those changes into codesandbox, the best way to learn programming is to write some code by yourself :)
If you will find any other issues or questions, feel free to ask, hope it helps :)

modal update has delay to set new State in react hook

I have some problem with updating the editModal (react bootstrap modal).
after dispatching the action it works and I'm able to fetch single pet data as an object.
and redux dev tools confirms that
But modal doesn't open at first attempt and I need to click the (edit button: doesn't matter which one) again to show filled input data ,and each time I click the edit button for first pet or second pet it doesn't update modal and it shows data related to the previously clicked edit
and as you see I clicked the second edit and data has fetched as redux dev tools shows correctly but Modal still shows first pet details
thanks for your help :)
here's my code
const PetScreen = () => {
const dispatch = useDispatch()
//part of state that comes from store.js
const petDetailByUser = useSelector((state) => state.petDetailByUser)
//when action dispatch petDetails fills with single pet Details ex:_id,editPetName,...
const { petDetailLoading, petDetails } = petDetailByUser
const [editPetName, setEditPetName] = useState('')
//edit button dispatch getPetDetail and get pet data as an object
const editButtonHandler = (id) => {
dispatch(getPetDetails(id))
if (petDetails) {
setEditPetName(petDetails.editPetName)
//shows edit modal
handleEditShow()
}
}
//this field is inside react modal body
<Form.Control
type='text'
placeholder='Pet Name'
name='editPetName'
value={editPetName}
onChange={(e) => setEditPetName(e.target.value)}
>
</Form.Control>
Just after dispatch(getPetDetails(id)), the editPetName variable has not been refreshed yet so it contains the name of the previous pet.
You can fix it by using an effect to update editPetName when petDetails is updated.
const petDetailByUser = useSelector((state) => state.petDetailByUser)
const { petDetailLoading, petDetails } = petDetailByUser;
const [editPetName, setEditPetName] = useState('');
useEffect(() => {
if (petDetails) {
setEditPetName(petDetails.editPetName);
handleEditShow();
}
}, [petDetails]);
const editButtonHandler = (id) => dispatch(getPetDetails(id));

How to use same page anchor tags to hide/show content in Next.js

The problem
The current project is using Next.js and this situation occurred: the content needs to be hidden or replaced, matching the current category selected. I want to do it without reloading or using another route to do so. And when the user presses F5 or reloads the page the content remains unchanged.
The attempts
Next.js' showcase page apparently is able to do so. In the docs, there's a feature called 'Shallow routing', which basically gives the possibility to update the URL without realoading the page. That's what i figured out for now. Any clues on how the content is changed to match the category?
Thanks!
You can load the content on the client based on the category passed in the URL fragment (# value) using window.location.hash.
Here's a minimal example of how to achieve this.
import React, { useState, useEffect } from 'react'
const data = {
'#news': 'News Data',
'#marketing': 'Marketing Data',
default: "Default Data"
}
const ShowCasePage = () => {
const router = useRouter()
const [categoryData, setCategoryData] = useState()
const changeCategory = (category) => {
// Trigger fragment change to fetch the new data
router.push(`/#${category}`, undefined, { shallow: true });
}
useEffect(() => {
const someData = data[window.location.hash] ?? data.default // Retrieve data based on URL fragment
setCategoryData(someData);
}, [router])
return (
<>
<div>Showcase Page</div>
<button onClick={() => changeCategory('news')}>News</button>
<button onClick={() => changeCategory('marketing')}>Marketing</button>
<div>{categoryData}</div>
</>
)
}
export default ShowCasePage

how to fix form submission with useEffect hook (as is: need to click submit twice)

App takes user options and creates an array objects randomly, and based on user options. (it's a gamer tag generator, writing to learn react.js). As is, App is a functional component and I use useState to store array of objects (gamertags) and the current selected options.
I use formik for my simple form. It takes two clicks to get a new item with updated options. I know why, options in state of App doesn't not update until it rerenders as the function for form submission is async. Therefore, all of my options are updated, after the first click, and are correct with the second because they were updated with the rerendering and after I needed them.
I know the solution is to use a useEffect hook, but despite reading over other posts and tuts, I don't understand how to apply it. It's my first instance of needing that hook and I'm still learning.
I wrote a simplified App to isolate the problem as much as possible and debug. https://codesandbox.io/s/morning-waterfall-impg3?file=/src/App.js
export default function App() {
const [itemInventory, setItemInventory] = useState([
{ options: "apples", timeStamp: 123412 },
{ options: "oranges", timeStamp: 123413 }
]);
const [options, setOptions] = useState("apples");
const addItem = (item) => {
setItemInventory([item, ...itemInventory]);
};
const createItem = () => {
return { options: options, timeStamp: Date.now() };
};
class DisplayItem extends React.Component {
render() { // redacted for brevity}
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
addItem(createItem());
};
const UserForm = (props) => {
return (
<div>
<Formik
initialValues={{
options: props.options
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(values);
props.onUpdate(values);
}}
>
{({ values }) => (
<Form> //redacted for brevity
</Form>
)}
</Formik>
</div>
);
};
return (
<div className="App">
<div className="App-left">
<UserForm options={options} onUpdate={onFormUpdate} />
</div>
<div className="App-right">
{itemInventory.map((item) => (
<DisplayItem item={item} key={item.timeStamp} />
))}
</div>
</div>
);
}
This is probably a "layup" for you all, can you help me dunk this one? Thx!
Solved problem by implementing the useEffect hook.
Solution: The functions that create and add an item to the list, addItem(createItem()), become the first argument for the useEffect hook. The second argument is the option stored in state, [options]. The callback for the form, onFormUpdate only updates the option in state and no longer tries to alter state, i.e. create and add an item to the list. The useEffect 'triggers' the creation and addition of a new item, this time based on the updated option because the updated option is the second argument of the hook.
Relevant new code:
useEffect( () => {
addItem(createItem());
}, [options]);
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
//addItem used to be here
};

How do I subscribe to state changes only when I am on a particular page?

Overview:
I am trying to subscribe to changes in state once I have rendered a particular component (AddFoodForm), if I navigate away, there should be no state available when I come back, however, when I am on the page and I change state then I want those state changes to be available to my other component (AddedList). Is this possible?
Or another approach. When I render my AddedList component, I don't want any initial state to show up, but if anything changes while this component is viewable then the new state change should show up. Again, not sure if this is possible.
The components AddFoodForm and AddedList are below:
AddFoodForm
class AddFoodForm extends Component {
state = {
date: moment(),
mealCategory: 'breakfast',
calendarFocused: false,
}
onDateChange = date => {
if (date) this.setState({ date })
}
onSubmit = e => {
e.preventDefault()
const food = {
date: this.state.date,
mealCategory: this.state.mealCategory
}
this.props.addFoodToMeal(food)
}
onFocusChange = ({ focused }) => this.setState({ calendarFocused: focused })
onMealChange = e => this.setState({ mealCategory: e.target.value })
render() {
return (
<div>
<form onSubmit={this.onSubmit}>
<select value={this.state.mealCategory}
onChange={this.onMealChange}>
<option value='breakfast'>Breakfast</option>
<option value='lunch'>Lunch</option>
<option value='dinner'>Dinner</option>
<option value='snack'>Snack</option>
</select>
<SingleDatePicker
date={this.state.date}
onDateChange={this.onDateChange}
focused={this.state.calendarFocused}
onFocusChange={this.onFocusChange}
numberOfMonths={1}
isOutsideRange={() => false}
id="caloriEat-addFood-form" />
<button type="submit">Add Food</button>
</form>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
addFoodToMeal: food => dispatch(addFoodToMeal(food))
})
const mapStateToProps = state => ({
food: state.food
})
export default connect(mapStateToProps, mapDispatchToProps)(AddFoodForm)
AddedList
const AddedList = (props) => {
console.log(props);
return (
<div>
{props.meals.map(meal => (
<li key={meal.uuid}>{`${meal.foodItem} added, calories:${meal.calories}`}</li>
))}
</div>
);
}
const mapStateToProps = (state) => ({
meals: state.meals
})
export default connect(mapStateToProps)(AddedList)
The components hierarchy I have is:
SearchFood renders InputForm, DisplayFood, and AddedList
DisplayFood renders two form components ServingForm and AddFoodForm
AddFoodForm will update state via redux
AddedList should be based on the changes made by AddFoodForm.
The GitHub repo if needed is https://github.com/altafmquadri/caloriEat
Update to clarify question
All my states update correctly. When someone is on the search page. I want them to search for a food item, if they decide to add that item then the form clears and there is no redirect since I want the functionality to be that someone can add more food items.
At the same time, if an item is added, I want the user to know that they added something. Hence, the creation of the AddedList component. It simply renders a list of what is added. I want that list to be just the items added when a user adds new items per visit to the page.
Since I am grabbing the Meal state, I am getting all the meals that is not what I want. So let's say on the search a user adds an apple and an orange, a list showing apple and orange should be rendered by the AddedList component. If the user navigates away from the page and Later returns, the list should be empty. Now the user adds a banana. Only the banana should be rendered by the AddedList component, apple and orange should not show up since they are meals that were searched for separately in a different page visit. Hope that makes sense.
You can lift state up or use a state management library such as redux to make state of one component available to another.
I think your issue may be in the addFoodToMeal action
return dispatch(addMeal(meal))
I believe you simply want to call the dispatch to send it to the reducer.
dispatch(addMeal(meal))
I'm not sure returning the function is giving the functionality you want to update state.

Categories

Resources