in my form i have a few dropdown components. Whenever first dropdown option changes i want to update props for the second dropdown and rerender it. My code looks like this
handleProjectChange(option) {
//this.setState({ selectedProject: option })
this.refs.phase.props = option.phases;
//this.refs.forceUpdate()
this.refs.phase.render()
}
render() {
var projectOptions = this.projectOptions
var defaultProjectOption = this.state.selectedProject
var phaseOptions = defaultProjectOption.phaseOptions
var defaultPhaseOption = phaseOptions[0]
var workTypeOptions = api.workTypes().map(x => { return { value: x, label: x } })
var defaultWorkTypeOption = workTypeOptions[0]
return (
<div>
<Dropdown ref='project' options={projectOptions} value={defaultProjectOption} onChange={this.handleProjectChange.bind(this)} />
<Dropdown ref='phase' options={phaseOptions} value={defaultPhaseOption} />
<Dropdown options={workTypeOptions} value={defaultWorkTypeOption} />
<button className="btn btn-primary" onClick={this.handleAddClick.bind(this)}>Add</button>
</div>
)
}
But props are not changed, so it rerenders the same options. At the moment im just rerendering entire form by setting new state on it. Is there any way to rerender only one child/Dropdown with new props?
The way to do this is to put the selected option in first dropdown selectedProject in state.
And inside your render function, fetch/ populate the options in the second dropdown, dependent on the selected project.
Flow will then be:
User selects an option in the first dropdown.
This triggers handleProjectChange()
Inside handleProjectChange(), the newly selected option is put in state, by a this.setState() call
Because state changed, react re-runs the entire render() function.
Under the hood, react figures out that only the second dropdown has changed, so react will only re-render the second drop-down on your screen/ in the DOM.
Although React does have a reconciliation algorithm that dynamically checks whether each component should be rerenader or not in every rendering of its parent, it doesn't always work as we intended.
https://reactjs.org/docs/reconciliation.html
For this kind of issues, you have two options. You can use either React.pureComponent or React.useMemo().
Related
This question requires some background to understand:
My app uses various lists of items with Boolean state that the user toggles by clicking.
I implemented this logic with two reusable elements:
A custom hook, useSelections(), which maintains an array of objects of the form { id, name, isSelected }, where isSelected is Boolean and the only mutable element. It returns the array as current state, and a dispatch function that takes an input of the form { id, newValue } and updates the isSelected member of the object with the given id
A function component Selector, which takes as props a single item and the dispatch from useSelections and returns a <li> element whose CSS class depends on isSelected.
Normally, they are used together in the following way, which works fine (i.e., internal state and the color of the list item are synchronized, and toggle when clicked):
function localComponent(props) {
const [items, dispatch] = useSelections(props.data);
return (
<ul>
{items.map(item => <Selector key={item.id} item={item} onChange={dispatch} />)}
</ul>
);
}
It works equally well when useSelections() is elevated to a parent component, and items and dispatch are passed as props.
The trouble started when the array became larger, and I decided to page it:
<ul>
{items.slice(start, end).map(item => <Selector key={item.id} item={item} onChange={dispatch} />)}
</ul>
(start and end are part of component state.)
When my component is first rendered, it works normally. However, once start and end are changed (to move to the next page), clicking an item changes internal state, but no longer triggers a re-render. In UX terms this means that after the first 'next' click, when I click on an item nothing appears to happen, but if I hit 'next' again, and then 'back', the item I just clicked on has now changed.
Any suggestions would be appreciated.
I solved the problem by removing the logic from the reducer function to ignore calls where there is no change:
const hasChanged = modifiedItem.isSelected !== newValue;
modifiedItem.isSelected = newValue;
return hasChanged ? { data } : state;
is now simply:
modifiedItem.isSelected = newValue;
return { data };
There's still something going on with React here that I do not understand, but my immediate problem is solved.
I want to reload the UserPlaylist tag after running the onChangeTracks() function in order to update it's contents but I'm not sure how to re-execute a specific tag if possible.
Parent Component:
render(){
return(
<li><UserPlaylist onChange={this.onChangeTracks}/></li>
);
}
UserPlaylist Component (Child Component):
render() {
return(
window.addEventListener('DOMContentLoaded', (event) => {
this.getPlaylists() //Have tracks load immediately
}),
<select value={"DEFAULT"} onChange={this.props.onChange}>
<option value="DEFAULT"> Add to Playlist </option>
<option value="new"> New Playlist </option>
{
this.state.users_playlists.map((playlist, index) => (
<option key={index} value={playlist.id}> { playlist.name }</option>
))
}
</select>
);
}
Components will rerender every time their state is updated. So you should update the state of your Parent component whenever the this.onChangeTracks function is invoked. Since this.state.currentTrack is being passed in as a prop to UserPlaylist, once it is updated via a call to setState the UserPlaylist component will receive new props and should rerender those new props accordingly.
Example:
onChangeTracks(val) {
...
this.setState({currentTrack: val});
...
}
EDIT
Here's a Codesandbox that updates the "New Playlist" select element whenever the onChange event is called.
Here's how it works: First, the useEffect hook fires and loads in data from the examplePlaylists variable. In your app, you should populate this with the data you have saved in your database or localStorage, etc. This data is passed to the playlists state which uses the useState hook. Whenever a person selects an option from the dropdown menu, the onChange event is fired and calls the handleUpdatePlaylists function. This function first prompts the user to enter the name of their new playlist. It then updates the playlists state through the updatePlaylistsfunction. Now, since the state has changed, the component will rerender and map over all the playlists, displaying their names in the dropdown menu. All without needing to refresh the page.
Of course, since I don't have a DB to save these to, the new playlists will disappear on page refresh. But you can write logic to save them however you wish.
And while this works, I think that you should look to using buttons and the onClick event instead of a select element to handle this logic. Since a new playlist is created whenever an option is selected, then clicking on the name of any playlist will also create them. This doesn't appear to be intended functionality.
So to summarize: handle updates to your components by changing their state. Once state changes, the components will rerender with their new state available for you to work with.
Here's the code:
class MenuContainerComponent extends Component {
onInputWidgetMenuChange(event, data) {
console.log(data);
}
render() {
var inputWidgets = [];
for (var i = 0; i < this.props.cdata.widgets.inputWidgets.length; i++) {
var componentName = getComponentNameFromType(this.props.cdata.widgets.inputWidgets[i]);
var key = "inputWidget" + i;
inputWidgets.push(<Dropdown.Item key={key}>{componentName}</Dropdown.Item>);
}
return (
<Dropdown style={childStyle} text='Input widgets' icon='keyboard' floating labeled button className='icon' onChange={this.onInputWidgetMenuChange}>
<Dropdown.Menu>
<Dropdown.Header icon='tags' content='Select a widget to add to canvas' />
<Dropdown.Divider />
{inputWidgets}
</Dropdown.Menu>
</Dropdown>
)
}
I am trying to get an event on menu selection. 'onClick' is working in similar fashion but there is no event on menu selection.
AFAIK, since you're using Dropdown.Menu inside this Dropdown, the onChange won't work. It's for normal Drodowns (like selecting a value etc). Try creating a generic onClick and assign it to <Dropdown.Item />
Giri's answer is correct. Change this line
inputWidgets.push(<Dropdown.Item key={key}>{componentName}</Dropdown.Item>);
to
inputWidgets.push(<Dropdown.Item key={key} value={componentId} onClick={this.onInputWidgetMenuChange}>{componentName}</Dropdown.Item>);
Where componentId is the actual value of the Dropdown.Item, (as opposed to the text displayed). Given the right circumstances componentId can be the same as the componentName too.
Another thing is that since you're using Dropdown.Menu inside the Dropdown, clicking the items on the menu won't automatically change the value of the Dropdown. (which is why the onChange of event the Dropdown component isn't fired). You need to save the current value of the Dropdown in the react state and manually set the trigger prop of Dropdown to make it look like the selected item.
so i have 3 react component 1 is the whole dialog component, tabs component and tabsContent component.And for example in tab content i have a field that when you write something it change the tab name on what you write.. I have done that with .forceUpdate everytime when you change that field it call function from dialog component and their it have .forceUpdate.. and that field is bind with the same object like the tab name. The problem is that if i have for example like 10 fields whenever i change a field it will call that forceUpdate and i don't need to call it everytime only for 1 field.So can you give me some way to do it that ?
window.DialogComponent = react.createClass(
componentDidMount(){
this.props.obj...;
}
onChange:function(){
this.forceUpdate()
}
render:function(){
return(
<dialogTab obj={this.props.obj} />
<dialogContent obj={this.props.obj} onChange={this.onChange} />
)
}
)
dialogTabComponent =react.createClass(
render:function(){
return(
<span>this.props.obj.name</span>
)
}
)
dialogTabContent = react.createClass(
onChange(){
this.props.obj.name = value;
this.props.onChange();
}
render:function(){
return(
<input text.... onChange = {this.onChange}>
)
}
)
If I understood it right, you have a DialogComponent that contains two child components (DialogTab and DialogContent).
DialogContent contains an input that, whenever is changed, will update some value on its sibling component, DialogTab. So basically you have a need for communication between sibiling components.
You should make use of the state in your parent component and pass props from your parent (DialogComponent) to its children, here is a fiddle: https://jsfiddle.net/69z2wepo/62767/
Good luck.
Say I have the following component structure:
App
DivContainer1
Child1
Button1
Child2
Button2
DivContainer2
AComponentHere
When I press Button1, I want to replace the contents of DivContainer2 with another component, e.g. replace AComponentHere with AnotherComponent that will render data based on which Child's button was clicked.
Should there be an AppStore and AppAction that tells it to change to another component, or should I pass down props that change the state here? Basically, I would like to know if Stores are used for only holding data state, or also holding ui state.
You can definitely have a store for UI state. In this case, it sounds like you are looking for a way to implement navigation. If you don't want to use something large like react-router, I would just create a simple UIStore or NavigationStore with an identifier indicating which component is currently shown:
// store setup...
var flags = {
currentPage: PAGES.home // page names should be in shared file somewhere...
};
// rest of store impl to manage access to these UI flags...
To change the contents, fire off an action from the onClick event (for example) that updates the store property to match the component you want to show.
handleClick: function(e) {
// this should trigger store to change the currentPage
AppActions.navigateTo(PAGES.about);
}
Then, in render(), you just have a conditional or switch statement that renders the correct output for each given value:
//...
render: function() {
return (
<div class="DivContainer2">
{switch (this.state.currentPage) {
case PAGES.home:
<HomePage />
break;
case PAGES.about:
<AboutPage someAttr={true} />
break;
default:
<NotFoundPage />
}}
</div>
);
}