Parent passes state to child - React js - javascript

ObjectList has an array of objects that get rendered as a list. When the user clicks a list item, that object is sent back to ObjectEditor so the user can view it and continue editing. The problem is that I'm not sure how to pass that object to ObjectEditor because the click event is taking place in ObjectList.
My initial solution was to pass it to ObjectEditor as props and use the componentWillReceiveProps method to update ObjectEditors state. However, that solution wasn't practical because I don't want it to update every time the props change. Is there a better way?
I'm new to React so I'd like to avoid using Redux for now until I've covered React.
I've heavily cut down the code for clarity.
ObjectList:
constructor(props){
super(props);
this.state = { objects: [
{title: '', items: [], anotherThing:''},
{title: '', items: [], anotherThing:''}
]}
}
viewObject = (index) => {
let object = {...this.state.object[index]};
// Then some code that passes the object to the ObjectEditor Component
}
render(){
return(
<div>
<li key={index} onClick={ () => this.viewObject(index) } >
// A list of titles from state
</li>
<ObjectEditor />
</div>
)
}
ObjectEditor:
constructor(props){
super(props);
this.state = {title:'', items: [], anotherThing:''}
}
// various event handlers that update the state based off form inputs
render(){
return(
<div>
// Various form fields which get pushed to state
<button>Save & Add New</button>
// function that maps through state and renders it to the page
</div>
)
}
}

My suggestion would be to have the parent component handle all the state and logic, and keep the ObjectEditor component a simple presentation component with no logic or state of its own. It would look a little something like this.
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
objects: [
{ title: '', items: [], anotherThing: '' },
{ title: '', items: [], anotherThing: '' }
],
editObject: {},
}
}
viewObject = (index) => {
let object = { ...this.state.object[index] };
this.setState({editObject: object}); // sets the state if the clicked item.
// Then some code that passes the object to the ObjectEditor Component
}
handleChange = (e) => {
// handle change
}
render() {
return (
<div>
<li key={index} onClick={() => this.viewObject(index)} >
// A list of titles from state
</li>
<ObjectEditor viewObject={this.state.viewObject} handleChange={this.handleChange} />
</div>
)
}
}
class ObjectEditor extends React.Component {
render() {
return (
// render some sort of editor
// display data based on the props passed down
// when user edits in the form, call up to the parent's change handler
);
}
}

Related

ReactJS Classes How to pass State from Child Component to its Parent?

I will call the LI tags like this (li) so they are not made into bullet points for his question
Hi I am trying to send a Child component to its Parent in ReactJS.
I tried many things I managed to send the child component state back up to its Parent props but when the page renders I have a couple of (li) tags which I want the update to update with it for example like:
(li) hard coded text (/li)
(li) old text (/li)
(li) update prop (/li)
(li) update prop etc (/li)
but instead the update deletes all previous code so it looks like:
(li) update prop deleted all previous li's (/li)
Hope that made sense here is my code
Parent Component
import React from 'react';
import { generateId, getNewExpirationTime } from '../../util/utilities';
import Thought from '../Thought/Thought.js'
import AddThoughtForm from '../AddThoughtForm/AddThoughtForm.js'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
thoughts: [{
id: generateId(),
text: 'This is a place for your passing thoughts.',
expiresAt: getNewExpirationTime()
},
{
id: generateId(),
text: "They'll be removed after 15 seconds.",
expiresAt: getNewExpirationTime()
}]
};
this.addThought = this.addThought.bind(this);
this.removeThought = this.removeThought.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.componentDidUpdate = this.componentDidUpdate.bind(this);
}
addThought(thought) {
console.log("LOOK")
console.log(thought)
console.log("DONE")
console.log(this.state.thoughts);
this.state.thoughts.push(thought);
this.setState(prevState => ({
...prevState,
thoughts: [thought]
}))
console.log("passed over")
console.log(this.state.thoughts);
}
removeThought(selected) {
//alert('selected');
let updatedThoughts = this.state.thoughts.filter((thought) => selected.id !== thought.id);
return this.setState({ thoughts: updatedThoughts })
}
render() {
return (
<div className="App">
<header>
<h1>Passing Thoughts</h1>
</header>
<main>
<AddThoughtForm
addThought={this.addThought}
thoughts={this.state.thoughts} />
<ul className="thoughts">
{(this.state.thoughts) && this.state.thoughts.map((thought, index) => (
<Thought
key={thought.id}
thought={thought}
removeThought={this.removeThought} />
))}
</ul>
</main>
</div>
);
}
}
Still on the Parent Component my .addThought(arg) is where the action is. This is where I'm sending the Child AddThoughtForm state object into it. By inside of .addThought() I am doing this:
addThought(thought) {
console.log("LOOK")
console.log(thought)
console.log("DONE")
console.log(this.state.thoughts);
this.state.thoughts.push(thought);
this.setState(prevState => ({
...prevState,
thoughts: [thought]
}))
console.log("passed over")
console.log(this.state.thoughts);
}
What happens is when I pass it over my previous State of my parent is deleted and replaced by this new information from my child component. How do I stop that? I want to only add this new Information to the previous info that the Parent state already have. here is the state from my parent:
constructor(props) {
super(props);
this.state = {
thoughts: [{
id: generateId(),
text: 'This is a place for your passing thoughts.',
expiresAt: getNewExpirationTime()
},
{
id: generateId(),
text: "They'll be removed after 15 seconds.",
expiresAt: getNewExpirationTime()
}]
};
this.addThought = this.addThought.bind(this);
this.removeThought = this.removeThought.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.componentDidUpdate = this.componentDidUpdate.bind(this);
}
Now over to the Child Component
AddThoughtForm.js
import React from 'react';
import { generateId, getNewExpirationTime } from '../../util/utilities';
class AddThoughtForm extends React.Component {
constructor(props) {
super(props);
this.state = {
ideas: [this.props.thoughts] // I can take off []
}
this.handleTextChange = this.handleTextChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleTextChange(event) {
const { value } = event.target
this.setState(prevState => {
let thoughts = Object.assign({}, prevState.ideas); // creating copy of state variable
thoughts.id = generateId();
thoughts.text = value; // update the name property, assign a new value
thoughts.expiresAt = getNewExpirationTime();
return { thoughts }; // return new object
})
console.log(this.state.ideas)
}
handleSubmit(event) {
event.preventDefault();
this.props.addThought(this.state.thoughts)
alert(this.state.ideas);
}
render() {
return (
<form className="AddThoughtForm" onSubmit={this.handleSubmit}>
<input
type="text"
aria-label="What's on your mind?"
placeholder="What's on your mind?"
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Add" />
</form>
)
}
}
export default AddThoughtForm;
On my .handleTextChange(event) is where I am linking it with my input tag in render so what I am doing what ever Is typed into it I want entered I want it be passed to my Parent Component. well It is passed over but it over-writes the old info every time the old (li) and it is then all just a new li being rendered. Any ideas on how I can fix all of this?
handleTextChange(event) {
const { value } = event.target
console.log(value)
this.setState(prevState => {
let thoughts = Object.assign({}, prevState.ideas); // creating copy of state variable
thoughts.id = generateId();
thoughts.text = value; // update the name property, assign a new value
thoughts.expiresAt = getNewExpirationTime();
return { thoughts }; // return new object
})
console.log(this.state.ideas)
}
I managed to fix it
just had to add on the Parent Component a thoughts: [...prevState, thought] as I was overwritting the old thoughts with the new incoming thought
Like this:
In the method, .addThought()
this.setState(prevState => ({
...prevState,
thoughts: [...prevState.thoughts, thought]
}))

How high should state be lifted in React?

Should intermediate components control parts of state and call props passed to them or should state be lifted higher? I've been going back and forth whether to have the child component utilize local state or have it handled by higher component and pass additional props down.
In this limited example, I have a Main component. I display some data in this component and pass functions to filter the data to a child component. Though, main component doesn't necessarily need to know about when the menuOpen property is changed. However, I need to update menuOpen when handleCancel(), handleSave(), and handleButtonClick() are called.
handleCancel() and handleSave() both modify the data that is displayed so I declare them in the Main component.
Should I be passing all these props through from Main component or use intermediate components to handle smaller portions of local state but also call props from a parent (grandparent etc) component?
Main Component
//Parent component
class Main extends React.Component {
constructor() {
super();
this.state = {
checkBoxes: {
1: {
name: 'Apple',
isChecked: true,
},
//...
},
fruit: {
1: {
name: 'Apple',
},
//...
},
checkedBoxes: [],
};
this.baseState = JSON.stringify(this.state.checkBoxes);
this.fruitFilter = this.fruitFilter.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleChange = this.handleChange.bind(this);
this.resetState = this.resetState.bind(this);
}
resetState() {
this.setState({checkBoxes: JSON.parse(this.baseState)});
}
//populates the checkedboxs array with name to filter by
handleSave() {
const checkedBoxes = Object.keys(this.state.checkBoxes)
.filter(key => {
//....some logic
});
this.baseState = JSON.stringify(this.state.checkBoxes);
this.setState({checkedBoxes: checkedBoxes});
}
//handles the checkbox toggle
handleChange(e) {
const checkBoxes = {...this.state.checkBoxes};
checkBoxes[e.target.id].isChecked = e.target.checked;
this.setState({checkBoxes: checkBoxes});
}
//filteres the fruit - if nothing is checked return them all
fruitFilter(fruit) {
return Object.keys(fruit)
.filter(key => {
//...filter logic
})
}
render() {
const visibleFruits = this.fruitFilter(this.state.fruit);
return (
<div>
<Filter
resetState={this.resetState}
checkBoxes={this.state.checkBoxes}
handleSave={this.handleSave}
handleChange={this.handleChange}
/>
<div>
<h2>Filtered Fruit</h2>
{Object.keys(visibleFruits).map(key => {
return (
//... renders list of fruit
);
})}
</div>
</div>
);
}
}
Child Component
class Filter extends React.Component {
constructor(props) {
super(props);
this.state = {
menoOpen: false,
};
this.handleCancel = this.handleCancel.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleSave() {
this.setState({menuOpen: false});
this.props.handleSave();
}
handleCancel() {
this.setState({menuOpen: false});
this.props.resetState();
}
handleButtonClick() {
this.setState({menuOpen: !this.state.menuOpen});
}
render() {
return (
<div>
<button onClick={this.handleButtonClick}>Choose Fruits</button>
{this.state.menuOpen && (
<FilterMenu
checkBoxes={this.props.checkBoxes}
handleSave={this.handleSave}
handleCancel={this.handleCancel}
handleChange={this.props.handleChange}
/>
)}
</div>
);
}
}
Grandchild Component
const FilterMenu = ({checkBoxes, handleChange, handleCancel, handleSave}) => {
return (
<div>
{Object.keys(checkBoxes).map(key => {
return (
//... renders dropdown menu
);
})}
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
);
};
Refine the separation of concerns and I think you'll like it better.
Define all checkbox event handlers in Filter.
Filter communications with Main via state only.
Don't force Main to evaluate UI components to set state.
Define Main state for Filter to use as needed to avoid the above.
Filter will construct the checkboxes.
Cancel and Save buttons seem like Filter level functions to me.
A FilterMenu component now seems pointless because it does not do anything. Perhaps in the larger architecture it is useful but you can always re-factor it out of Filter when needed
Filter component is the seam in the code that separates action from state.
State is not unnecessarily pushed further down.
Actual functionality is not unnecessarily pushed further up.
Coupling between Main and Filter is reduced. Filter has more reuse potential.

React with lists in state, how to set new state?

I ran into an issue with updating part of the state that is a list that's passed on to children of a component.
I pass in a list to a child, but then have trouble to update that list and have the child reflect the new state;
<ItemsClass items={this.state.items1} />
When I change the value of this.state.items1, the component doesn't render with the new value.
this.setState({items1: []}); // this has no effect
However, if I change the already existing array (not replacing it new a new empty one), the component renders as I wish;
this.setState(state => { clearArray(state.items1); return state; });
That means the state updating function isn't pure, which React states it should be.
The HTML;
<div id='app'></div>
The js;
class ItemsClass extends React.Component {
constructor(props){
super(props);
this.state = {items: props.items};
}
render() {
var items = this.state.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
function ItemsFunction(props) {
var items = props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
class App extends React.Component {
constructor(props){
super(props);
var items = [{id:1, text: 'item 1'}, {id: 2, text: 'item 2'}];
this.state = {
items1: items.slice(),
items2: items.slice(),
items3: items.slice()
};
this.clearLists = this.clearLists.bind(this);
}
clearLists() {
// for items1 and items2, clear the lists by assigning new empty arrays (pure).
this.setState({items1: [], items2: []});
// for items3, change the already existing array (non-pure).
this.setState(state => {
while (state.items3.length) {
state.items3.pop();
}
})
}
render() {
return (
<div>
<button onClick={this.clearLists}>Clear all lists</button>
<h2>Items rendered by class, set list to new empty array</h2>
<ItemsClass items={this.state.items1} />
<h2>Items rendered by class, empty the already existing array</h2>
<ItemsClass items={this.state.items3} />
<h2>Items rendered by function</h2>
<ItemsFunction items={this.state.items2} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Try it out on codepen.
It seems that the ItemsClass doesn't update even though it's created with <ItemsClass items={this.state.items1}/> and this.state.items1 in the parent changes.
Is this the expected behavior? How can I update the state in the ItemsClass child from the parent?
I'm I missing something? This behavior seems quite error prone, since it's easy to assume that the child should follow the new state, the way it was passed in when the child was created.
You're copying the props of ItemsClass into the state when the component gets initialized - you don't reset the state when the props change, so your component's updates don't get displayed. To quote the docs:
Beware of this pattern, as state won't be up-to-date with any props update. Instead of syncing props to state, you often want to lift the state up.
If your component has to do something when the props change, you can use the componentWillReceieveProps lifecycle hook to do so (note that it doesn't get run when the component initially mounts, only on subsequent prop updates).
That said, there's zero reason for you to be duplicating the props here (and honestly there's rarely a good reason to do so in general) - just use the props directly, as you're doing with ItemsFunction, and everything will stay in sync:
class ItemsClass extends React.Component {
render() {
var items = this.props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
Here's a working version of your Codepen: http://codepen.io/anon/pen/JNzBPV

Component with local state not updating on React Redux

I'm trying to create a standalone component MultiSelect (I know there are components out there but I want to learn how to do one).
I created it in a way it will store the selected values in its state.
I call it like this from my container:
<MultiSelect
items={ this.props.boards }
label="Boards"
value={ this.props.selectedBoards }
onChange={ e => this.onChange(e) } />
When I dispatch an action I get the correct values for the container's props. My MultiSelect even display the correct options. However for some reason it doesn't update the selected values. In the container this.props.selectedBoards has the correct value, but the component doesn't get this update.
export default class MultiSelect extends React.Component {
constructor(props) {
super(props); // props.items = all options to select
this.state = {
selectedItems: props.value || [], // selected options (problem)
search: '',
hidden: true,
};
}
render() {
return (<div>
...
this.state.selectedItems
The options to select is on the props (which works) and the selected options is on the state (which doesn't get updated).
How can I make the MultiSelect component update with a new value for selectedItems which is in the state? Or what would be the recommended way to create a component like this?
I end up moving selectedItems from the state to the props, and then on the onChange event where elements get selected I'm getting the value from the props, adding/removing the changed item and calling this.props.onChange with the modified array.
Then on the container I'm dispatching an action with the new array so I can get a new state.
I solved it by delegating the task to keep track of selected items to the container.
MultiSelect:
export default class MultiSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
search: '',
hidden: true,
};
}
onChange(item) {
const selectedItems = [ ...this.props.selectedItems ];
const index = selectedItems.findIndex(element => element.value === item.value);
if (index !== -1) {
selectedItems.splice(index, 1);
}
else {
selectedItems.push(item);
}
this.props.onChange(selectedItems); // container
}
render() {
return (<div>
...
this.props.selectedItems
Container:
onChange(data) {
this.props.dispatch({ type: 'SetSelectedBoards', data });
}
<MultiSelect
items={ this.props.boards }
label="Boards"
selectedItems={ this.props.selectedBoards }
onChange={ e => this.onChange(e) } />

Prepopulate controlled components

In my attempt to handle an update form, have written the code below. It is a controlled input component, with a corresponding state value. When a change happens on the input component the state value is updated. This means view will always reflect data changes and the other way around. My issue comes when trying to prepopulate the input component with data fetched from the database. My attempt was to define the initial state value in the constructor, to be equal to the passed props, but that did not work. When the component is first rendered it will not contain the passed spirit prop, since it has not yet been fetched. When the component is rendered the second time (because the data is ready) the constructor will not be called. How will I set the initial state when the data is ready and not before?
SpiritsEditContainer
export default createContainer(({params}) => {
const handle = Meteor.subscribe("spirit", params.id);
return {
loading: !handle.ready(),
spirit: Spirits.find(params.id).fetch()[0]
}
}, SpiritsEditPage);
SpiritsEditPage
export default class SpiritsEditPage extends Component {
constructor(props) {
super(props)
this.state = {name: this.props.spirit.name}
}
handleNameChange(event) {
this.setState({name: event.target.value});
}
handleUpdate(event) {
event.preventDefault();
}
render() {
const {name} = this.state;
if (this.props.loading) {
return <div>loading</div>
} else {
return (
<div>
<h1>SpiritsEditPage</h1>
<form onSubmit={this.handleUpdate.bind(this)}>
<Input type="text"
label="Name"
value={name}
onChange={this.handleNameChange.bind(this)}/>
<button>Update</button>
</form>
</div>
)
}
}
}
The constructor code may not work correctly:
constructor(props) {
super(props)
this.state = {name: this.props.spirit.name}
}
Instead check for props.spirit to be available.
this.state = { name: this.props.spirit && this.props.spirit.name }
Add a componentWillReceiveProps:
componentWillReceiveProps(nextProps) {
if (nextProps.spirit !== this.props.spirit) {
this.setState({ name: nextProps.spirit.name });
}
}
The rest of the code looks alright.

Categories

Resources