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.
Related
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
);
}
}
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.
I need to get data from DB depending on a search string value. Therefore I'm using an input field. The search string is stored as a state value.
The data for the component comes from a container (using npm meteor/react-meteor-data).
Now my problem is, how do I get the search string into the container to set the parameter for the publication?
container/example.js
export default createContainer((prop) => {
Meteor.subscribe('images', searchString) // How to get searchString?
return { files: Images.find({}).fetch() }
}, Example)
component/example.jsx
class Example extends Component {
constructor(props) {
super(props)
this.state = {
searchString: ''
}
}
searchImage(event) {
const searchString = event.target.value
this.setState({ searchString })
}
render() {
return (<Input onChange={ this.searchImage.bind(this) }/>)
}
}
export default Example
publication
Meteor.publish('images', function(search) {
return Images.find({ title: search }).cursor
})
Maybe you can create two different components: a parent and a child, and you can wrap child component with createContainer HOC like the following
childComponent.js
const Example = (props) => {
return <Input onChange={props.searchImage}/>
}
export default createContainer(({searchString}) => {
Meteor.subscribe('images', searchString)
return { files: Images.find({}).fetch() }
}, Example)
parentComponent.js
class ExampleWrapper extends Component {
constructor(props) {
super(props)
this.state = {
searchString: ''
}
}
searchImage = (event) => {
const searchString = event.target.value
this.setState({ searchString })
} // instead of binding this, you can also use arrow function that
// takes care of binding
render() {
return (<Example searchImage={this.searchImage} searchString={this.state.searchString} {...this.props} />)
}
}
export default ExampleWrapper
The idea is, since createContainer is a higher order component, it doesn't have access to the props of any component wrapped by it.
What we need to do is, passing the value of searchString from a parent component.
The way to do is the following:
ExampleWrapper has a state called searchString and Example component has a prop called searchString. We can set the value of searchString prop to state.searchString.
Since the default export corresponds to createContainer({..some logic…}, Example}), createContainer can make use of prop called searchString.
In order to change the value of state.searchString we also passed searchImage function as a prop to Example component. Whenever there is a change event, onChange triggers searchImage function that updates the value of state.searchString. And eventually, the minute the value of state.searchString changes searchString prop’s value changes thus your subscription result also changes
onChange={ (e)=> {this.setState({ searchString: $(e.target).val() }) } }
This is how we assign values to our internal state properties :)
EDIT: I appear to have misunderstood the question...
i have a very long form (75 input to be exact), since i'm using redux to manage my application state, whenever i want to edit this form i want to setState of form to the prop to allow editing.
Example Code:
class VisitCard extends Component {
constructor(props) {
super(props); //props.visit = {name:'randome name', data:'etc..'}
this.state = Object.assign({},props.visit);
this.bindInput = this.bindInput.bind(this);
}
//BindInput will return props for Inputs, to achive two way binding
bindInput(config){
const {name,...props} = config;
return {
value : this.state[name],
onChange: event => this.setState({[name]:event.target.value}),
...props
}
}
render(){
return <div>
<input {...this.bindInput({name:'name', type:'text'})} />
<input {...this.bindInput({name:'data', type:'text'})} />
</div>
}
}
above code works perfect, problem is when this component mounts, it give me error "Cannot update during an existing state transition"
also sometimes if the value is not predefined in the props, the value for input will be undefined, so after props load from server and updates component i get another error "trying to change input from uncontrolled to controled" that is because this.state[name] was undefined then i got a value
so what am'i doing wrong ? how can i link the state of component with props value and make sure that if props changed, state does change too, while at sametime, if state changes this does not affect props.
I hope modify your code to match the below logic will resolve your issues. Look for comments inside the code for explanations
class VisitCard extends Component {
constructor(props) {
super(props);
//set your state to have a key that holds your prop value.
this.state = { visit: props.visit };
this.bindInput = this.bindInput.bind(this);
}
componentWillReceiveProps(nextProps) {
//if your props is received after the component is mounted, then this function will update the state accordingly.
if(this.props.visit !== nextProps.visit) {
this.setState({visit: nextProps.visit});
}
}
bindInput(config){
const {name,...props} = config;
// return defaultValue which you get from the props.
// you can add `value: this.state.visit[name]` to the below object only if you want your input to be controlled, else it can be ignored.
return {
defaultValue : this.props.visit[name],
onChange: event => this.setState(
{visit: { ...this.state.visit,
[name]:event.target.value}
}),
...props
}
}
render(){
// render empty if your props has not yet arrived.
if(!this.props.visit) {
return (<div />);
}
// render after you have values in props
return (<div>
<input {...this.bindInput({name:'name', type:'text'})} />
<input {...this.bindInput({name:'data', type:'text'})} />
</div>);
}
}
I am trying to learn ReactJS and Redux, and have come across a problem that I cannot seem to get over.
I have a React component, that gets data from an asynchronous request.
export class MyPage extends React.Component {
constructor(props) {
super(props)
this.state = {
enableFeature: false,
}
this.handleEnableFeatureChange = this.handleEnableFeatureChange.bind(this)
}
componentWillMount () {
this.fetchData()
}
fetchData () {
let token = this.props.token
this.props.actions.fetchData(token)
}
handleEnableFeatureChange (event) {
this.setState({ enableFeature: event.target.checked })
}
render () {
if (this.props.isFetching) {
return (
<div>Loading...</div>
)
} else {
return (
<div>
<label>Enable Feature
<input type="checkbox"
className="form-control"
checked={this.props.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
</div>
)
}
}
}
So, my problem now is that, when I change the state of the checkbox, I want to update the state of my data. However, every time I update the state of my data, the react component method shouldComponentUpdate kicks in, and uses the current props to render the original data.
I would like to see how such cases are handled in general.
Thanks.
Try to do it like the following, i.e.
Use componentWillReceiveProps to assign props.enableFeature to state.enableFeature. From documentation
componentWillReceiveProps() is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render.
componentWillReceiveProps() is not invoked if you just call this.setState()
Use this state to load the value of checkbox
Manipulate this state (onchange) to update the value of checkbox
Following code can work in your case
export class MyPage extends React.Component {
static propTypes = {
isFetching: React.PropTypes.bool,
enableFeature: React.PropTypes.bool,
token: React.PropTypes.string,
actions: React.PropTypes.shape({
fetchData: React.PropTypes.func
})
};
state = {
enableFeature: false,
};
componentWillMount () {
this.fetchData();
}
/* Assign received prop to state, so that this state can be used in render */
componentWillReceiveProps(nextProps) {
if (this.props.isFetching && !nextProps.isFetching) {
this.state.enableFeature = nextProps.enableFeature;
}
}
fetchData () {
const { token } = this.props;
this.props.actions.fetchData(token)
}
handleEnableFeatureChange = (event) => {
this.setState({ enableFeature: event.target.checked })
};
render () {
return (<div>
{ this.props.isFetching && "Loading..." }
{
!this.props.isFetching && <label>
Enable Feature
<input
type="checkbox"
className="form-control"
checked={this.state.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
}
</div>);
}
}
Note: The above code was not executed, but should work (babel's stage-0 code)
Change it to checked={this.state.enableFeature}