React - setTimeout triggers style change for wrong components - javascript

reproduced here: https://jsfiddle.net/69z2wepo/204131/
A parent component has two 'notifications' that it renders with different 'decay' rates.
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {
notifications: [
{ message: "I am the first component", code: 1, decay: 2000 },
{ message: "I am the second component", code: 2, decay: 5000 }
]
}
this.dismissNotification = this.dismissNotification.bind(this)
}
dismissNotification(code) {
this.setState({ notifications: this.state.notifications.filter(
n => n.code != code
)})
}
render() {
return (
<div>
{
this.state.notifications.map( (n, idx) => {
return (
<Notification
key={idx}
code={n.code}
decay={n.decay}
dismiss={this.dismissNotification}
>
{n.message}
</Notification>
)
})
}
</div>
)
}
}
The components set their own timeOut which will cause an animation and then send a message for them to be dismissed.
class Notification extends React.Component {
constructor(props) {
super(props)
this.state = {
style: { opacity: 1 }
}
this.makeRedFunction = this.makeRedFunction.bind(this)
}
componentDidMount = () => {
let timeout = parseInt(this.props.decay) || 2000
setTimeout(() => {
this.makeRedFunction();
setTimeout(() => {
this.dismiss();
}, 125)
}, timeout)
}
fadeOutFunction = () => {
let opacity = Math.floor(this.state.style.opacity * 10)
if (opacity > 0) {
opacity -= 1
setTimeout( () => { this.fadeOutFunction() }, 10)
}
let newState = Object.assign({}, this.state.style)
newState.opacity = opacity / 10
this.setState({ style: newState })
}
makeRedFunction = () => {
this.setState({ style: {color: 'red'} })
}
dismiss = () => {
this.props.dismiss(this.props.code)
}
render () {
return(
<div style={this.state.style}>{this.props.children}</div>
)
}
}
ReactDOM.render(
<Page/>,
document.getElementById('container')
);
Unforunately, the style seems to change for both notifications when the dismiss function has been called for only one of them.
In general there is strange behavior with the mounting lifecycle of the components with this approach.

tl;dr: Don't use array indexes as keys if elements in the list have state. Use something that is unique for each data point and does not depend on its position in the array. In your case that would be key={n.code}.
This is related to how React reconciles the component tree and is a good example for why using array index as keys doesn't always produce the expected outcome.
When you are mutating a list of elements, the key helps React to figure out which nodes it should reuse. In your case are going from
<Notification />
<Notification />
to
<Notification />
But how should React know whether to delete the first or second <Notification /> node? It does that by using keys. Assume we have
<Notification key="a">Foo</Notification>
<Notification key="b">Bar</Notification>
Now if it gets either
<Notification key="a">...</Notification>
or
<Notification key="b">...</Notification>
in the next render cycle it knows to remove the <Notification /> with key "b" (or "a").
However, your problem is that you base the key on the position of the data in the array. So on the first render you pass
<Notification key="0">First</Notification>
<Notification key="1">Second</Notification>
Then you are removing the first notification from the list, which changes the position of the second notification in the array, so React gets
<Notification key="0">Second</Notification>
which means
remove the element with key 1 and update the element with key 0 to show "Second"
But the element with key="0" already had its style changed to red text, so you see the text from the second notification in red.
Have a look at the documentation for more information.

Related

React toggle class on just one component

I'm creating, in componentDidMount, a lots of <div>'s.
constructor (props) {
super(props)
this.state = {
componentLoaded: false,
divs: []
}
}
componentDidMount () {
this.createDivs()
}
createDivs () {
// Actually, this divs are created dinamically and with infinite scroll
let divs = <div className='container'>
<div className='item' onClick={() => { /* Add class */ }}>...</div>
<div className='item' onClick={() => { /* Add class */ }}>...</div>
<div className='item' onClick={() => { /* Add class */ }}>...</div>
/* ... n divs ... */
</div>
let newDivs = this.state.divs
newDivs.push(divs)
this.setState({
componentLoaded: true,
divs: newDivs
})
}
render () {
return {this.state.componentLoaded ? this.state.divs : null }
/* In my return, if X event occurs, re-call this.createDivs() to add more divs */
}
What I'm trying to achieve, is to toggle a class into only one of the .item divs, and then if clicking another one, remove it from the before and add it to the one was clicked.
I've tried to add an attribute to the state, but it didn't add it. I also searched for some solutions, but I always find solutions which doesn't toggle, as they are "toggled individually" in separated components.
Hoping to find some help, maybe this thing is real simple, but for now, I cannot figure out how to make it.
PS: I'm adding the createDivs into the state because it's an infinite scroll that re-uses the function, so I just push them into the state and the scroll won't go to the top again when adding the previous ones + the new ones.
In problems like these it is always helpful to determine what goes into react's state. You want the state to be as lightweight as possible (so you store only the stuff which is necessary)
class Test extends React.Component {
state = {
selectedDiv: null,
};
handleClick = id => {
this.setState(prev => ({
// sets it to null if its already active else, sets it active
selectedDiv: prev.selectedDiv === id ? null : id,
}));
};
render() {
// Array to map over
const divs = [1, 2, 3, 4];
const { selectedDiv } = this.state;
return (
<div className="container">
{divs.map(div => {
return (
<div
key={div}
className={selectedDiv === div ? "item class_to_add" : "item"}
onClick={() => this.handleClick(div)}
>Item {div}</div>
);
})}
</div>
);
}
}
In the above examples we are only storing the unique Id of the div in the state and using that to determine if the selected div is active or not, if it is then we simply remove it from the state. The above solution does not require any complex lifecycle methods, my advice would be to keep the component as simple as possible.
PS. not part of the answer but I suggest you to look into the newer hooks API its more intuitive and most probably the future of react
First, note that you're breaking a React rule here:
this.state.divs.push(divs)
You must never directly modify state. The correct thing there is either:
this.setState({divs}); // Replaces any previous ones
or
this.setState(({divs: oldDivs}) => {divs: [...oldDivs, divs]}); // Adds to any previous ones
However, the "React way" to do this would probably be not to store those divs in state at all; instead, store the information related to them in state, and render them (in render) as needed, with the appropriate classes. The information about which one of them has the class would typically either be information on the items themselves, or some identifying information about the item (such as an id of some kind) held in your component's state.
Here's an example using items that have an id:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
// No items yet
items: null,
// No selected item yet
selectedId: null
};
}
componentDidMount() {
this.createDivs();
}
createDivs() {
// Simulate ajax or whatever
setTimeout(() => {
const items = [
{id: 42, label: "First item"},
{id: 12, label: "Second item"},
{id: 475, label: "third item"},
];
this.setState({items});
}, 800);
}
render () {
const {items, selectedId} = this.state;
if (!items) {
// Not loaded yet
return null;
}
return (
<div className='container'>
{items.map(({id, label}) => (
<div
key={id}
className={`item ${id === selectedId ? "selected" : ""}`}
onClick={() => this.setState({selectedId: id})}
>
{label}
</div>
))}
</div>
);
}
}
ReactDOM.render(<Example />, document.getElementById("root"));
.selected {
color: green;
}
.item {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Change React component visibility based on the state of another component

I have created the following React component. It uses an input box to accept a user's answer to a riddle. As soon as the user's input matches the desired answer, the input box become read-only (a bit of a strange way to use them). It also has an "isHidden" prop to determine whether the riddle is rendered.
class Riddle extends React.Component {
constructor(props) {
super(props);
this.answer = props.answer.toUpperCase();
this.state = {
text: "",
isAnswered: false
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
let userInput = event.target.value.toUpperCase();
if (userInput == this.answer) {
this.setState({
text: userInput,
isAnswered: true
});
} else {
this.setState({
text: userInput,
isAnswered: false
});
}
}
render() {
if (this.props.isHidden) {
return <div></div>;
} else {
return (
<div>
<p>{this.props.prompt}</p>
<input type="text" value={this.state.text}
readOnly={this.state.isAnswered}></input>
</div>
);
}
}
}
Here it is in practice:
function App() {
return (
<div>
<Riddle prompt='The first three letters in the alphabet.' answer="abc" isHidden="false"/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
What I would like to do is have a bunch of these riddles in sequence, but have riddles only be visible when the previous one was solved. The trouble is that I don't know how to cause the visibility update to happen.
I have read about lifting state from children to a parent component, and I've tried to see if I could create a RiddleSequence component with Riddles as its children, and have RiddleSequence manage visibility. My problem is that currently it is part of Riddle's state whether or not it's solved, and I don't know how RiddleSequence can read that information since child state should remain hidden. This seems like a reasonable way to encapsulate Riddle's functionality, but maybe I'm wrong given my goals.
I have also considered making Riddles be children of other riddles they depend on, since I can just pass state/props to children:
<Riddle prompt="first riddle"...>
<Riddle prompt="depends on first riddle"...>
<Riddle prompt="depends on second riddle"...>
</Riddle>
</Riddle>
</Riddle>
But if I have an app with 100 riddles, this seems to get ridiculous. This also reduces flexibility for a more expanded set of features (such as making one riddle depend on a group of 3 riddles).
How can I make the visibility of my Riddle components depend on the state of other riddles?
A simple solution would be to have a container component as you said:
class Riddle extends Component {
constructor(props) {
this.state = {
text: ''
}
this.answer = props.answer.toUpperCase()
}
handleChange = event => {
const userInput = event.target.value.toUpperCase()
const callback = userInput == this.answer ? this.props.onSolved : undefined
this.setState({ text: userInput }, callback)
}
render() {
const { text, isAnswered } = this.state
const { prompt } = this.props
if (this.props.isHidden) {
return null
}
return (
<div>
<p>{prompt}</p>
<input type="text" value={text} readOnly={isAnswered}></input>
</div>
)
}
}
and container should hold visibility like this:
class RiddleSequence extends Component {
state = {}
riddles = [
{
id: 1,
prompt: 'The first three letters in the alphabet.',
answer: 'abc',
prev: null
},
{
id: 2,
prompt: 'The last three letters in the alphabet.',
answer: 'xyz',
prev: 1
}
]
render() {
return (
<div>
{this.riddles.map(r => {
const { id, prev } = r
const visible = !prev || this.state[prev]
return (
<Riddle
key={id}
isHidden={!visible}
onSolved={() => this.setState({ [r.id]: true })}
{...r}
/>
)
})}
</div>
)
}
}

Component Doesn't Update once the Parent component's state is updated

import React, {Component} from 'react';
import "./DisplayCard.css";
class DisplayCard extends Component {
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
return <div>{array[i].task}</div>
}
}
renderElements = (savedTasks) =>{
if (savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(savedTasks)
}
}
render() {
return (
<div className="DisplayCardContainer">
{this.renderElements(this.props.saved)}
</div>
)
}
}
export default DisplayCard;
Hey guys,
I am new to react, so this is my child component that takes state from its parent component. My goal is to re-render component every time the array this.props.saved is changed.
This component renders: <p>You have no saved tasks.</p> when the this.props.saved.length === 0 and it renders <div>{array[0].task}</div> when i enter the first task, but it keeps it at <div>{array[0].task}</div> after that. I do see that the state keeps changing and this.props.saved keeps getting bigger, but my component doesn't change anymore.
Here's your problem:
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
//the first time we get here, it immediately ends the function!
return <div>{array[i].task}</div>
}
}
This loop only ever goes through once (at i=0) and then returns, exiting the runArray function and cancelling the rest of the loop. You probably wanted to return an array of elements, one for each of the tasks. I recommend using Array.map() for this, which takes an array and transforms each element, creating a new array:
runArray = (array) => {
return array.map(arrayElement => <div>arrayElement.task</div>);
}
This should do the trick. Note that React may complain about the fact that your elements lack the key property - see the documentation for more info: https://reactjs.org/docs/lists-and-keys.html
The problem is in your runArray function. Inside your loop, you are returning the first element and that's it. My guess is, you see only the first entry?
When you are trying to render all your tasks, I would suggest to map your tasks, e.g.
runArray = (array) => array.map(entry => <div>{entry.task}</div>)
It is because you write wrong the runArray function. You make a return in the for loop so it breaks after the first iteration. It will not iterate over the full array.
You need to transform your for loop to a map : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
runArray = (array) => {
return array.map(v => <div>{v.task}</div>)
}
Does it fix your issue ?
You have to update state of the component to trigger render function. Your render function is not triggered because you did not update the state when the props changed. There are many ways to update state when props updated. One method may be the following:
componentWillReceiveProps(nextProps){
if (nextProps.saved !== this.props.saved) {
this.setState({ saved: nextProps.saved })
}
}
Also change yoour render function to use state of the component as below:
renderElements = () =>{
if (this.state.savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(this.state.savedTasks)
}
}
Use .map so that it renders your task correctly. You can remove runArray and rely entirely on props so you don't need to pass arguments across functions as it can get messy quickly. Here's a quick running example of how to create a parent component where you can add a task and pass them into a component so that it renders your data when props are changed, therefore making it reactive.
class App extends React.Component {
state = {
taskLabel: "",
tasks: [
{
id: 1,
label: "Do something"
},
{
id: 2,
label: "Learn sometihng"
}
]
};
handleInput = evt => {
this.setState({
[evt.target.name]: evt.target.value
});
};
handleSubmit = evt => {
evt.preventDefault();
this.setState(prevState => ({
taskLabel: "",
tasks: [
...prevState.tasks,
{
id: prevState.tasks.length + 1,
label: this.state.taskLabel
}
]
}));
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
name="taskLabel"
type="text"
placeholder="Task label"
value={this.state.taskLabel}
onChange={this.handleInput}
/>
<button>Create task</button>
</form>
<DisplayCard tasks={this.state.tasks} />
</div>
);
}
}
class DisplayCard extends React.Component {
renderTasks = () => {
if (this.props.tasks.length !== 0) {
return this.props.tasks.map(task => (
<div key={task.id}>{task.label}</div>
));
} else {
return <div>No tasks</div>;
}
};
render() {
return <div>{this.renderTasks()}</div>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

ReactJS Wrong Component gets removed from DOM

I have three files: ShopsContainer.js ShopsComponent.js and ShopsItemComponent.js
ShopsContainer maintains an array of shop items in local state that gets passed down into ShopsComponent as props. ShopsComponent then maps through the items array that is being received as props and renders a ShopsItemComponent for each item in the array.
Within my ShopsContainer file, I have a method that removes a shop item from state using the following code:
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
When this happens, the correct item is removed from the items array in state, however, whatever the last ShopItem is that is in the DOM at the time of the removeShop call will get removed no matter if it is the correct item that should be removed or not. In other words, when removeShop gets called and the items array in state gets updated correctly, the wrong ShopItemComponent gets removed from the DOM.
What I would like to happen (or what I think should happen) is when removeShop gets called, that shop gets removed from the items array in state and ShopsContainer re-renders causing ShopsComponent to re-render with the updated props being received. And lastly ShopsComponent would map through the newly updated items array in props displaying a `ShopItemComponent for the correct items. Perhaps the problem has to do with the props being updated?
My code is as follows:
ShopsContainer.js
class ShopsContainer extends Component {
constructor() {
this.state = {
items: null
}
this.getAll();
this.removeShop = this.removeShop.bind(this);
}
getAll = () => {
// API request that fetches items and updates state
}
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
render() {
return (
<div>
{this.state.items ? <ShopComponent items={this.state.items} removeShop={this.removeShop} /> : <div><h1>Loading...</h1></div>}
</div>
);
}
}
ShopsComponent.js
class ShopsComponent extends Component {
constructor() {
this.handleRemove = this.handleRemove.bind(this);
}
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem, i) => {
return (<ShopItemComponent key={i} item={shopItem} removeShop={this.handleRemove} />);
})}
</React.Fragment>
);
}
}
Your code is working great, but you only has one mistake , your ShopComponent is assign index as a key for each ShopItemComponent and react is tracking those indexes to update the correct component, so you need to set key as a unique value between items, then I realize that shopAccount should be your id for each item.
The solution code is below.
class ShopsComponent extends Component {
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem) => <ShopItemComponent key={shopItem.shopAccount} item={shopItem} removeShop={this.handleRemove} />)}
</React.Fragment>
);
}
}
I hope you can find useful.
Note, when you are using a arrow function into your class, don't bind that method into the constructor, so remove it, because
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
is already binded.

Focus a certain input on a list of dynamically generated inputs

I have a list of dynamically generated inputs.
input --> onClick new Input beneath
[dynamically added]input
input
How can give just this dynamically added input focus?
The input has the textInput ref. This partly works:
componentWillUpdate(){
this.textInput.focus();
}
Yet, just works or the first new Input. Then it seems like the logic breaks.
the inputs are .map() from an array. Is there a way to either say, if the current rendered element has el.isActive to focus it. Or just say focus the input with the index 5?
CODE
Inputsgenerating file/component
import React from 'react';
import ReactDOM from 'react';
import _ from 'lodash'
class SeveralInputs extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ' '
}
this.showIndex = this
.showIndex
.bind(this)
this.map = this
.map
.bind(this)
this.handleChange = this
.handleChange
.bind(this);
}
componentWillUpdate() {
this.textinput && this
.textInput
.focus();
}
render() {
return (
<ul>
{this.map()}
</ul>
)
}
map() {
{
return this
.props
.data
.map((name, index) => <li
onKeyPress={this
.showIndex
.bind(this, index)}
key={index}><input
onChange={this
.handleChange
.bind(this, index)}
task={this.task}
value={name.value}
ref={(input) => {
this.textInput = input;
}}
type="text"/>{name.value}</li>)
}
}
handleChange(index, e) {
let data = this
.props
.data
.splice(index, 1, {
value: e.target.value,
isActive: true
})
this
.props
.refreshState(data);
}
showIndex(index, e) {
if (e.which === 13 || e.keyPress === 13) {
let data = this.props.data[index].isActive = false
data = this
.props
.data
.splice(index + 1, 0, {
value: ' ',
isActive: true
})
this
.props
.refreshState(data);
} else {
return null
}
}
}
export default SeveralInputs
The data that lives in the parent component
const data = [
{
value: 0,
isActive: true
}, {
value: 2,
isActive: false
}
]
The parents state:
this.state = {
error: null,
data
};
The parents render
render() {
return (
<div>
{/* <Input/> */}
{/* <SeveralItems refreshState={this.refreshState} data={this.state.data.value}/> */}
<SeveralInputs refreshState={this.refreshState} data={this.state.data}/> {/* <SeveralInputsNested refreshState={this.refreshState} data={this.state.data}/> {this.items()} */}
</div>
);
}
refreshState(data) {
this.setState({data: this.state.data})
console.log(this.state.data)
}
The first issue I see is that in refreshState you pass some data that you do not handle, try this:
refreshState(newData) {
this.setState({data: newData})
}
And trying to log this.state right after won't work because :
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.

Categories