I have created the following component:
type ToggleButtonProps = { title: string, selected: boolean }
export default class ToggleButton extends Component<ToggleButtonProps>{
render(){
return (
<TouchableWithoutFeedback {...this.props}>
<View style={[style.button, this.props.selected ? style.buttonSelected : style.buttonDeselected]}>
<Text style={[style.buttonText, this.props.selected ? style.buttonTextSelected : style.buttonTextDeselected]}>{this.props.title}</Text>
</View>
</TouchableWithoutFeedback>
);
}
}
The styles are simple color definitions that would visually indicate whether a button is selected or not. From the parent component I call (item is my object):
item.props.selected = true;
I've put a breakpoint and I verify that it gets hit, item.props is indeed my item's props with a selected property, and it really changes from false to true.
However, nothing changes visually, neither do I get render() or componentDidUpdate called on the child.
What should I do to make the child render when its props change? (I am on React Native 0.59.3)
You can't update the child component by literally assigning to props like this:
item.props.selected = true;
However, there are many ways to re-render the child components. But I think the solution below would be the easiest one.
You want to have a container or smart component which will keep the states or data of each toggle buttons in one place. Because mostly likely, this component will potentially need to call an api to send or process that data.
If the number of toggle buttons is fixed you can simply have the state like so:
state = {
buttonOne: {
id: `buttonOneId`,
selected: false,
title: 'title1'
},
buttonTwo: {
id: `buttonTwoId`,
selected: false,
title: 'title2'
},
}
Then create a method in the parent which will be called by each child components action onPress:
onButtonPress = (buttonId) => {
this.setState({
[buttonId]: !this.state[buttonId].selected // toggles the value
}); // calls re-render of each child
}
pass the corresponding values to each child as their props in the render method:
render() {
return (
<View>
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonOne} />
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonTwo} />
...
finally each child can use the props:
...
<TouchableWithoutFeedback onPress={() => this.props.onPressFromParent(this.props.dataFromParent.id)}>
<View style={[style.button, this.props.dataFromParent.selected ? style.buttonSelected : style.buttonDeselected]}>
...
I left the title field intentionally for you to try and implement.
P.S: You should be able to follow the code as these are just JS or JSX.
I hope this helps :)
Because children do not rerender if the props of the parent change, but if its STATE changes :)
Update child to have attribute 'key' equal to "selected" (example based on reactjs tho')
Child {
render() {
return <div key={this.props.selected}></div>
}
}
Related
I'm learning React. Say that I have a ListContainer which renders several ListItems. I want to keep track of the currently selected ListItem, render it in another color, and be able to navigate up and down.
One way would be to store selectedItem as state in ListContainer, and send it down as a prop to ListItem. But if I do it this way, then every time I change selectedItem I will rerender all ListItems because they are dependent on selectedItem. (I should only have to re-render two ListItems, the one that gets deselected, and the one that gets selected).
Is there a way to implement next and previous function without re-rendering all items?
Note: I know that React doesn't re-render unnecessarily in the DOM, but I'm trying to optimize operations on virtual DOM also.
Edit: Here is my example in code. It renders a list, and when the user click one item it gets selected. We also see that "ListItem update" gets printed 100 times, each time we change selection, which happens regardless of PureComponent or React.memo.
let mylist = []
for (let i = 0; i < 100; i++) {
mylist.push({ text: "node:" + i, id: i })
}
window.mylist = mylist
const ListItem = React.memo (class extends Component {
componentDidUpdate() {
console.log('ListItem update')
}
render() {
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
return (
<li
style={{ backgroundColor }}
onMouseDown={() => this.props.setSelected(this.props.item.id)}
>
{this.props.item.text}
</li>
)
}
})
class ListContainer extends Component {
constructor(props) {
super(props)
this.state = {
selectedItem: 10
}
this.setSelected = this.setSelected.bind(this)
}
setSelected(id) {
this.setState({ selectedItem: id })
this.forceUpdate()
}
render() {
return (
<ul>
{this.props.list.map(item =>
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
</ul>
)
}
}
function App() {
return (
<ListContainer list={mylist} />
);
}
The state you suggesting is the right way to implement it...
The other problem with unnecessary renders of the list item can easily be soved by wrapping the export statement like this:
export default React.memo(ListItem)
This way the only elements that has changed their props will rerender.. be aware that overuse of This can cause memory leaks when using it unnecessarily...
UPDATE
according to your example in addition to the React.memo you can update the way you transfer props to avoid senfing the selected item in each item...
istead of:
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
do :
let backgroundColor = this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={item.id === this.state.selectedItem}
setSelected={this.setSelected}
/>)}
this way the react memo will prevent rerenders when it is possible...
I have created a signup module with three steps in my react native application where I have put all of the fields in the state of the parent component, and that gave me a very overloaded JSX file, So I am trying to move all of the fields validation functions to the related step and catch the returned value 'valid/ not valid' in the parent screen so I can move to the next step by clicking on the next button which will navigate to the next viewpager page.
So I have tried to use the React.createRef to reference the child step in the parent and call the methods within the child in the future:
SignupScreen.jsx (Parent):
constructor(props) {
super(props);
this.state = {
...fields,
}
this.signupStepOneRef = React.createRef();
}
render(){
return (
<ViewPager
initialPage={0}
style={styles.fieldsContainer}
scrollEnabled={false}
ref={(vp) => (this.viewPager = vp)}
>
<SignupStepOne
fieldsContainerStyle={styles.fieldsContainer}
scrollViewStyle={styles.scrollView}
key="1"
onInputChanged={this.handleChange}
stepOneErrors={this.state.stepOneErrors}
ref={this.signupStepOneRef}
/>
...OtherSteps
</ViewPage>
)
}
But this code gives me an Object : {current: null} when I console.log(this.signupStepOneRef).
The official documentation says that this won't work if the child is a function, but changing it to a class component didn't help either.
I have also tried the:
ref={(obj) => this.signupStepOneRef = obj}
syntax but also gives me the same null current object.
I finally found a solution, since viewpager children cannot be referenced directly I have wrapped up the child page in a View component and that solved my issue.
The solution code:
<ViewPager
initialPage={0}
style={styles.fieldsContainer}
scrollEnabled={false}
ref={(vp) => (this.viewPager = vp)}
>
<View key="1">
<SignupStepOne
fieldsContainerStyle={styles.fieldsContainer}
scrollViewStyle={styles.scrollView}
onInputChanged={this.handleChange}
stepOneErrors={this.state.stepOneErrors}
ref={this.signupStepOneRef}
/>
</View>
...OtherSteps
</ViewPage>
The key attribute needs to be moved to the wrapper view.
Currently, I am facing a problem with JS/React/React-Native. I am pulling categories from an API, and I am making buttons out of the results (they change often based on different variables in the URL). The code I am using to do this is as follows:
const cats = singles.map((d) => {
return (
<TouchableOpacity key={d} style={styles.Settingcats}><Text style={{color: '#f55f44'}}>{d}</Text></TouchableOpacity>
)}
With the dynamically generated buttons I want them to be able to be toggled in the application. When I tried to utilize the states with the following code:
const cats = singles.map((d) => {
return (
<TouchableOpacity key={d} onPress={ _ => this.changeStyle} style={this.state.style === 0 ? styles.Settingcats : styles.SelSettingcats}><Text style={{color: '#f55f44'}}>{d}</Text></TouchableOpacity>
)}
And realized that, since this was referring to one state for all of the buttons, it would change all of the buttons styles. So I tried to think otuside the box a bit and thought of using the ID as a state name, creating the state, and utilizing the state through an external function to change it's value.
const cats = singles.map((d) => {
return (
<TouchableOpacity key={d} onPress={ _ => this.changeStyle(d)} style={this.state([d]) === 0 ? styles.Settingcats : styles.SelSettingcats}><Text style={{color: '#f55f44'}}>{d}</Text></TouchableOpacity>
)}
changeStyle(d){
this.setState({
[d] : 1
})}
Which throws an error because the state is being utilized as if it is a function.
What practices can I use to make dynamically created buttons have their own separate toggle events?
Other things I have tried: Custom Switches, material-UI but I get stuck at the same problem when it comes to having unique functions for the toggled buttons.
Instead of trying to manage a list of states at the top level, move TouchableOpacity into a component that handles the toggle state internally.
class MyButton extends React.Component {
constructor(props) {
super(props)
this.state = {
toggle: false
}
this.setToggle = this.setToggle.bind(this);
}
setToggle(){
this.setState({
toggle: !this.state.toggle
})
}
render(){
return <TouchableOpacity onClick={this.setToggle} className={this.state.toggle ? 'red' : 'blue'}>{this.props.name}</TouchableOpacity>
}
}
You can render a list of these and each one manages it own toggle state independent of the others.
Here is a fiddle example: https://jsfiddle.net/n5u2wwjg/220149/
The method that I used to achieve this, with many thanks from #simbathesailor
I utilized the variable in the state to dynamically create states that I could utilize in telling whether the buttons were "on" or "off" by using
this.state[d]
Where d was the dynamic variable that I created with my TouchableOpaque component.
It is to my knowledge that if a parent component rerenders, then all its children will rerender UNLESS they implement shouldComponentUpdate(). I made an example where this doesn't seem to be the true.
I have 3 components: <DynamicParent/>, <StaticParent/> and <Child/>. The <Parent/> components are responsible for rendering the <Child/> but do so in different ways.
<StaticParent/>'s render function statically declares the <Child/> before runtime, like so:
<StaticParent>
<Child />
</StaticParent>
While the <DynamicParent/> handles receiving and rendering the <Child/> dynamically at runtime, like so:
<DynamicParent>
{ this.props.children }
</DynamicParent>
Both <DynamicParent/> and <StaticParent/> have onClick listeners to change their state and rerender when clicked. I noticed that when clicking <StaticParent/> both it and the <Child/> are rerendered. But when I click <DynamicParent/>, then only the parent and NOT <Child/> are rerendered.
<Child/> is a functional component without shouldComponentUpdate() so I don't understand why it doesn't rerender. Can someone explain why this is to be the case? I can't find anything in the docs related to this use case.
I'll post your actual code for context:
class Application extends React.Component {
render() {
return (
<div>
{/*
Clicking this component only logs
the parents render function
*/}
<DynamicParent>
<Child />
</DynamicParent>
{/*
Clicking this component logs both the
parents and child render functions
*/}
<StaticParent />
</div>
);
}
}
class DynamicParent extends React.Component {
state = { x: false };
render() {
console.log("DynamicParent");
return (
<div onClick={() => this.setState({ x: !this.state.x })}>
{this.props.children}
</div>
);
}
}
class StaticParent extends React.Component {
state = { x: false };
render() {
console.log("StaticParent");
return (
<div onClick={() => this.setState({ x: !this.state.x })}>
<Child />
</div>
);
}
}
function Child(props) {
console.log("child");
return <div>Child Text</div>;
}
When you write this code in your Application render:
<StaticParent />
What's rendered is this:
<div onClick={() => this.setState({ x: !this.state.x })}>
<Child />
</div>
And in reality, what happens (roughly) is this:
function StaticParent(props) {
return React.createElement(
"div",
{ onClick: () => this.setState({ x: !this.state.x }) },
React.createElement(Child, null)
);
}
React.createElement(StaticParent, null);
When you render your DynamicParent like this:
<DynamicParent>
<Child />
</DynamicParent>
This is what actually happens (again, roughly speaking)
function DynamicParent(props) {
return React.createElement(
"div",
{
onClick: () => this.setState({ x: !this.state.x }),
children: props.children
}
);
}
React.createElement(
DynamicParent,
{ children: React.createElement(Child, null) },
);
And this is the Child in both cases:
function Child(props) {
return React.createElement("div", props, "Child Text");
}
What does this mean? Well, in your StaticParent component you're calling React.createElement(Child, null) every time the render method of StaticParent is called. In the DynamicParent case, the Child gets created once and passed as a prop. And since React.createElement is a pure function, then it's probably memoized somewhere for performance.
What would make Child's render run again in the DynamicParent case is a change in Child's props. If the parent's state was used as a prop to the Child, for example, that would trigger a re-render in both cases.
I really hope Dan Abramov doesn't show up on the comments to trash this answer, it was a pain to write (but entertaining)
It's mainly cause of you have 2 different "children".
this.props.children
<Child/>
They're not the same thing, first one is a prop passed down from Application -> DynamicParent, while the second one is a Component rendered in StaticParent, they have separate rendering/life cycles.
Your included example
class Application extends React.Component {
render() {
return (
<div>
{/*
Clicking this component only logs
the parents render function
*/}
<DynamicParent>
<Child />
</DynamicParent>
{/*
Clicking this component logs both the
parents and child render functions
*/}
<StaticParent />
</div>
);
}
}
Is literally the same as:
class Application extends React.Component {
render() {
// If you want <Child/> to re-render here
// you need to `setState` for this Application component.
const childEl = <Child />;
return (
<div>
{/*
Clicking this component only logs
the parents render function
*/}
<DynamicParent>
{childEl}
</DynamicParent>
{/*
Clicking this component logs both the
parents and child render functions
*/}
<StaticParent />
</div>
);
}
}
As a comment to SrThompsons answer: "What would make Child's render run again in the DynamicParent case is a change in Child's props. If the parent's state was used as a prop to the Child, for example, that would trigger a re-render in both cases."
So props or not props passed to child component however it may look will cause a rerender if parent rerenders (so use React.memo for a child without props :) )
"Whether you’re implementing your component as a class component that extends React.Component, or as a functional component, the render function is called again whenever the parent container renders again." Please read here for more info, because great article. https://medium.com/free-code-camp/yeah-hooks-are-good-but-have-you-tried-faster-react-components-e698a8db468c"
It will only re-render components that have had a change. If nothing on the child component has changed, it will not be re-rendered.
I am creating a Quiz app for the sake of learning React Native.
I want that when a user presses an answer, all buttons should be disabled. I have no idea how to do this, I have tried all different approaches, like changing props of the buttons from the parent, setting state from the parent etc. I just can't figure it out. I can make the clicked button disabled, but that doesn't help since the other buttons are still clickable.
Parent
class Container extends Component {
state = { currentQuestion: questions[0] }
buttons = new Array();
componentWillMount() {
this.makeButtons();
}
makeButtons() {
for (let i = 0; i < 4; i++) {
const isCorrect = (i === 0); //the first answer is correct, this is how I keep track
const btn = (
<Button
key={i}
title={this.state.currentQuestion[i]}
isCorrect={isCorrect}
/>
);
this.buttons.push(btn);
}
shuffle(this.buttons);
}
render() {
return (
<View style={containerStyle}>
<Text style={textStyle}>
{this.state.currentQuestion.title}
</Text>
{this.buttons}
</View>
);
}
}
Button
class Button extends Component {
state = { color: "rgb(0,208,196)" };
handleEvent() {
const newColor = (this.props.isCorrect) ? "green" : "red";
this.setState({ color: newColor });
this.props.onPress();
}
renderButton() {
return (
<TouchableOpacity
style={buttonStyle}
onPress={this.handleEvent.bind(this)}
disabled={this.props.disabled}
>
<Text style={textStyle}>
{this.props.title}
</Text>
</TouchableOpacity>
);
}
render() {
return this.renderButton();
}
}
You are creating your button components within an instance variable once when the parent component loads, but never re-rendering them. This is an anti-pattern of React. Ideally, your components should all be rendered within render(), and their props should be computed from state, so you only need to worry about updating the state correctly and all your components render properly.
In this case, you should construct the data for your buttons at component load, save your button data within state, and render your buttons within render(). Add a "disabled" state to your Button component, and when a user presses one of the buttons, use a callback to set "disabled" state in the parent component, and all your buttons will re-render to be properly disabled.