I have written the start of a todo list in reactjs and was looking to improve the functionality of how the todos are added to the state. Currently I am concat(ting) the value in put to an array which is in the state, then splicing off the selected li element. It seems to be a bit buggy when you add the first todo. Should i be using reacts immutability helpers to acheive this? Seems overkill to add another thing that can be acheived in plain js.
//Input component
const Input = props => {
return (
<div className="form-group">
<input
className="form-control"
value={props.value}
onChange={props.update}
type="text"
/>
<button className="btn btn-default" onClick={props.handleClick}>
Add Todo
</button>
</div>
);
};
//display list of todos
const Displaytodo = (props) => {
const todolist = props.todo;
const listItems = todolist.map((todo, index) =>
<li
className={
props.highlight ? 'list-unstyled todoItem highlight' : 'list-unstyled todoItem '
}
key={index}>
{todo}
<div
onClick={props.removeTodo.bind(this, index)}
className="removeTodo">
<i className="fa fa-trash" />
</div>
<div onClick={props.changeHighlight.bind(this,index)} className="checkTodo">
<i className="fa fa-check-circle" onClick={props.highlight} />
</div>
</li>
);
return <ul className="todos">{listItems}</ul>;
};
//controlled state component
class Layout extends React.Component {
constructor() {
super();
this.state = { text: "Hello", todo: [], highlight: false };
}
update(e) {
this.setState({ text: e.target.value });
}
handleClick() {
const text = this.state.text;
if (text.length > 0) {
this.setState(
{ todo: this.state.todo.concat(text), text: "", highlight: false },
function() {
console.log(this.state.todo);
}
);
} else {
alert("please enter something");
}
}
removeTodo(e) {
this.state.todo.splice(e, 1);
this.setState({ todo: this.state.todo });
}
changeHighlight(index, e) {
const highlight = this.state.highlight;
this.setState(prevState => ({
highlight: !prevState.highlight
}));
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-md-4 col-md-offset-4">
<div className="wrapper">
<h1>Todo List</h1>
<Input
value={this.state.text}
update={this.update.bind(this)}
handleClick={this.handleClick.bind(this)}
/>
<Displaytodo
removeTodo={this.removeTodo.bind(this)}
todo={this.state.todo}
changeHighlight={this.changeHighlight.bind(this)}
highlight={this.state.highlight}
/>
</div>
</div>
</div>
</div>
);
}
}
const app = document.getElementById("app");
ReactDOM.render(<Layout />, app);
https://codepen.io/mhal12/pen/MomWVg
Also when the user clicks the green tick, it will highlight the row by toggling class 'highlight' off and on, but in console it giving an error. which links to
https://facebook.github.io/react/docs/error-decoder.html?invariant=94&args[]=onClick&args[]=boolean
Simply remove the onClick on <i className="fa fa-check-circle" onClick={props.highlight} />.
As for the highlighting on each todo, it's a bit more complex. You have to have an id on each todo, and then pass the id to the changeHighlight function. You have to remove highlight from global state, and assign a highlight boolean on each todo. Then you have to display todos accordingly.
Same stuff for the removeTodo function, you pass in an id to remove it in the parent component.
Here's the full code :
const Input = props => {
return (
<div className="form-group">
<input
className="form-control"
value={props.value}
onChange={props.update}
type="text"
/>
<button className="btn btn-default" onClick={props.handleClick}>
Add Todo
</button>
</div>
);
};
const Displaytodo = (props) => {
const changeHighlight = function(id) {
props.changeHighlight(id);
}
const removeTodo = function(id) {
props.removeTodo(id);
}
const todolist = props.todo;
const listItems = todolist.map((todo, index) =>
<li
className={
todo.highlight ? 'list-unstyled todoItem highlight' : 'list-unstyled todoItem '
}
key={todo.id}>
{todo.text}
<div
onClick={removeTodo.bind(event, todo.id)}
className="removeTodo">
<i className="fa fa-trash" />
</div>
<div onClick={changeHighlight.bind(event, todo.id)} className="checkTodo">
<i className="fa fa-check-circle" />
</div>
</li>
);
return <ul className="todos">{listItems}</ul>;
};
class Layout extends React.Component {
constructor() {
super();
this.state = {text: "Hello", todo: []};
}
update(e) {
this.setState({ text: e.target.value });
}
handleClick() {
const text = this.state.text;
if (text.length > 0) {
this.setState(
{ todo: this.state.todo.concat({
id: this.state.todo.length + 1,
text: this.state.text,
highlight: false
}), text: ""},
function() {
console.log(this.state.todo);
}
);
} else {
alert("Please enter something");
}
}
removeTodo(id) {
let todos = this.state.todo;
for (let i = 0; i < todos.length; i++) {
let todo = todos[i];
if (todo.id == id) {
todos.splice(i, 1);
}
}
this.setState({ todo: todos });
}
changeHighlight(id) {
let todos = this.state.todo;
for (let i = 0; i < todos.length; i++) {
let todo = todos[i];
if (todo.id == id) {
todos[i].highlight = !todos[i].highlight;
}
}
this.setState({
todo : todos
});
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-md-4 col-md-offset-4">
<div className="wrapper">
<h1>Todo List</h1>
<Input
value={this.state.text}
update={this.update.bind(this)}
handleClick={this.handleClick.bind(this)}
/>
<Displaytodo
removeTodo={this.removeTodo.bind(this)}
todo={this.state.todo}
changeHighlight={this.changeHighlight.bind(this)}
/>
</div>
</div>
</div>
</div>
);
}
}
const app = document.getElementById("app");
ReactDOM.render(<Layout />, app);
Related
I'm facing an issue with setState inside my handleChange.
So here in code after clicking addList a div with input and button will appear, then when i type inside input the input state will update and after clicking Add button the div will disappear.
so my problem is next time when clicking addList the value of previous input is still there and the hideAddList handler does not set the input state to empty and also when I type in the new opened div the previous input state will overwrite. even with indexing and spread operator its still same problem.
what should I do?
export class myClass extends Component {
constructor(props) {
super(props);
this.state = {
input: [{ title: '' }]
};
this.addList = this.addList.bind(this);
this.hideAddList = this.hideAddList.bind(this);
this.titleHandleChange = this.titleHandleChange.bind(this);
}
addList() {
var x = document.getElementById("btn-list")
var y = document.getElementById("add-button")
x.style.display = 'none'
y.style.display = ''
}
hideAddList() {
var x = document.getElementById("btn-list")
var y = document.getElementById("add-button")
x.style.display = ''
y.style.display = 'none';
this.setState(prevState => ({ input: [...prevState.input, {title:''}]}))
}
HandleChange(e, index){
const { name, value } = e.target;
const list = [...this.state.input];
list[index][name] = value;
this.setState({ input: list});
}
render() {
return (
<div>
<button id="btn-list" className="btn btn-default btn-list" type="submit" onClick={this.addList}>Add Another List</button>
{this.state.input.map((x, i) => {
return (
<div key={i} id="add-button" className="add-button" style={{ display: "none" }}>
<input id="input" type="text" onChange={(e) => {this.HandleChange(e, i)}} value={x.title} name="title" className="form-control add-input" />
<button className="btn btn-default btn-list-add" onClick={this.hideAddList} type="submit">Add</button>
</div>
)})}
</div>
)
}
}
export default myClass
from the code you provided, you will always get the first "add-button" element each time you call addList, because all of your list items have the same id.
{this.state.input.map((x, i) => {
...
<div key={i} id="add-button" ...> // here! they got the same id!
so what happened here is that you are actually editing the first item all the time. you will need to add different id to your elements. and the AddList should get the last item index.
for example:
AddList()
...
{this.state.input.map((x, i) => {
return (
<div key={i} id={`add-button-${i}`} .... // here! add unique id for each item!
the whole code should be like this:
export default class myClass extends React.Component {
constructor(props) {
super(props);
this.state = {
input: [{title: ''}],
};
this.addList = this.addList.bind(this);
this.hideAddList = this.hideAddList.bind(this);
this.titleHandleChange = this.titleHandleChange.bind(this);
}
addList() {
var lastItemIndex = this.state.input.length - 1; // you need to get the last item's index to make sure that you display the latest item
var x = document.getElementById('btn-list');
var y = document.getElementById(`add-button-${lastItemIndex}`);
x.style.display = 'none';
y.style.display = '';
}
hideAddList(i) {
var x = document.getElementById('btn-list');
var y = document.getElementById(`add-button-${i}`);
x.style.display = '';
y.style.display = 'none';
this.setState(prevState => ({input: [...prevState.input, {title: ''}]}));
}
titleHandleChange(e, index) {
const {name, value} = e.target;
const list = [...this.state.input];
list[index][name] = value;
this.setState({input: list});
}
render() {
return (
<div>
<button
id="btn-list"
className="btn btn-default btn-list"
type="submit"
onClick={this.addList}>
Add Another List
</button>
{this.state.input.map((x, i) => {
return (
<div
key={i}
id={`add-button-${i}`}
className="add-button"
style={{display: 'none'}}>
<input
id="input"
type="text"
onChange={e => {
this.titleHandleChange(e, i);
}}
value={x.title}
name="title"
className="form-control add-input"
/>
<button
className="btn btn-default btn-list-add"
onClick={() => this.hideAddList(i)}
type="submit">
Add
</button>
</div>
);
})}
</div>
);
}
}
for debugging, maybe you can try to log index out in titleHandleChange, so you can find out which item that you're actually editing.
ps, you could write export default class MyClass extends ... so you don't need to write export default MyClass at the end of the file again.
The state has already been changed correctly. You can add a console log to see it:
render() {
console.log(this.state);
return (
...
The reason that it is not showing on your screen is the {display: 'none'} that you set for id 'add-button'. Your var y = document.getElementById("add-button") will just get the first element, but not the whole list of elements.
FYI it is not quite right to use document.getElementById in a react project for your case. It should be done by state. Here is a complete example:
export class MyClass extends Component {
constructor(props) {
super(props);
this.state = {
input: [{title: ''}],
showAnotherListBtn: true,
showList: false,
};
}
onClickAnotherListBtn = () => {
this.setState({
showAnotherListBtn: false,
showList: true,
});
}
addList = () => {
this.setState({
input: [...this.state.input, {title: ''}],
});
}
handleChange(e, index) {
const {name, value} = e.target;
const list = [...this.state.input];
list[index][name] = value;
this.setState({input: list});
}
render() {
console.log(this.state);
return (
<div>
{this.state.showAnotherListBtn ?
<button id="btn-list" className="btn btn-default btn-list" type="submit" onClick={this.onClickAnotherListBtn}>Add Another
List</button> : null}
{this.state.showList && this.state.input.map((x, i) => {
return (
<div key={i} id="add-button" className="add-button">
<input id="input" type="text" onChange={(e) => {
this.handleChange(e, i)
}} value={x.title} name="title" className="form-control add-input"/>
<button className="btn btn-default btn-list-add" onClick={this.addList} type="submit">Add</button>
</div>
)
})}
</div>
)
}
}
export default MyClass
As you can see from the above code, you an just use a boolean state to determine whether you should show the element or not.
Hope this helps ;)
I was writing a component with the code given as follows, which after rendering looks like:
I have used antd components to render the fields. The problem that I am facing is that I am neither able to select from the select box nor write in the input field as shown below. I have a feeling that I am using React's key inappropriately for mocFields in the render method which is obtained from getMOCField.
import React, { Component } from "react";
import { Button, Icon, Select, Form, Input } from "antd";
const FormItem = Form.Item;
const Option = Select.Option;
import { FormComponentProps } from "antd/lib/form/Form";
type state = {
mocFields: JSX.Element[]
};
export class MOC extends Component<FormComponentProps, state> {
constructor(props) {
super(props);
this.state = {
mocFields: []
};
this.addMOCField = this.addMOCField.bind(this);
this.removeMOCField = this.removeMOCField.bind(this);
}
componentDidMount() {}
componentWillReceiveProps(nextProps) {}
removeMOCField(key, event: React.MouseEvent<HTMLElement>) {
event.preventDefault();
const { mocFields } = this.state;
mocFields.splice(key, 1);
this.setState({
mocFields
});
}
getMOCFieldFooter() {
return (
<div className="d-flex justify-content-between small">
<div className="inline-block">
<Button
type="primary"
shape="circle"
icon="plus"
ghost
size="small"
className="d-font mr-1"
onClick={this.addMOCField}
/>
<div
className="text-primary pointer d-font inline-block letter-spacing-1"
onClick={this.addMOCField}
>
Add another
</div>
<div className="d-font inline-block letter-spacing-1">or </div>
<div className="text-primary pointer d-font inline-block letter-spacing-1">
Create a new MOC
</div>
</div>
</div>
);
}
getMOCField(key) {
const { getFieldDecorator } = this.props.form;
return (
<div className="d-flex justify-content-between">
<div className="inline-block">
<FormItem label="Select MOC">
{getFieldDecorator(`selected_moc[${key}]`, {
rules: [
{
required: true,
message: "Please select moc"
}
]
})(
<Select>
<Option value={"A"}>A</Option>
<Option value={"B"}>B</Option>
</Select>
)}
</FormItem>
</div>
<div className="inline-block">
<FormItem label="Recovery (%)">
{getFieldDecorator(`recovery_percentage[${key}]`, {
rules: [
{
required: true,
message: "Please input the recovery percentage"
}
]
})(<Input type="number" step="0.000001" />)}
</FormItem>
</div>
<div className="inline-block pointer">
<span>
<Icon type="close" onClick={this.removeMOCField.bind(this, key)} />
</span>
</div>
</div>
);
}
addMOCField(event: React.MouseEvent<HTMLElement>) {
event.preventDefault();
const { mocFields } = this.state;
const MOCField = this.getMOCField(mocFields.length);
mocFields.push(MOCField);
this.setState({
mocFields
});
}
getAddMOCButton() {
return (
<div className="d-flex w-100 mt-3">
<Button
type="primary"
ghost
className="w-100"
onClick={this.addMOCField}
>
<Icon type="plus-circle" />
Add MOC
</Button>
</div>
);
}
render() {
const { mocFields } = this.state;
const mocButton = this.getAddMOCButton();
const toRender =
mocFields.length > 0 ? (
<div className="w-100 p-2 gray-background br-25">
{mocFields.map((f, index) => (
<div key={index}>{f}</div>
))}
{this.getMOCFieldFooter()}
</div>
) : (
mocButton
);
return toRender;
}
}
What could be the reason for this? What am I doing incorrectly? Currently the above component renders as follows:
If the number of fields in mocFields is zero, then a button to add new fields is rendered.
After the button is pressed, mocField is populated with the select box and input field as shown above. The key of the div is decided during the render method.
It seems that the listeners doesn't work once they are stored in the array. I've tried to inline the call to getMOCField in the render function and it works. Here is what I've changed to get it work:
class MOC extends Component {
// ...
addMOCField(event) {
event.preventDefault();
const { mocFields } = this.state;
// We only keep inside the state an array of number
// each one of them represent a section of fields.
const lastFieldId = mocFields[mocFields.length - 1] || 0;
const nextFieldId = lastFieldId + 1;
this.setState({
mocFields: mocFields.concat(nextFieldId),
});
}
removeMOCField(key, event) {
event.preventDefault();
this.setState(prevState => ({
mocFields: prevState.mocFields.filter(field => field !== key)
}));
}
render() {
const { mocFields } = this.state;
const mocButton = this.getAddMOCButton();
const toRender =
mocFields.length > 0 ? (
<div className="w-100 p-2 gray-background br-25">
{/* {mocFields.map((f, index) => (
<div key={index}>{f}</div>
))} */}
{mocFields.map(fieldIndex => (
<div key={fieldIndex}>{this.getMOCField(fieldIndex)}</div>
))}
{this.getMOCFieldFooter()}
</div>
) : (
mocButton
);
return toRender;
}
}
I am aware of similar threads here, but any of them still can't help me.
I'm trying to pass deleteItem() function from parent component to onClick argument in grandson component.
Please, look at components and tell me what is wrong, what should I change to access this function in grandson component?
Parent - https://codeshare.io/2E39oO
Child - https://codeshare.io/5XnwN8
Grandson - https://codeshare.io/5z9JXE
Here are the two things I spotted
misspelling in deleteHandler (already mentioned)
the button was disabled, so it wouldn't trigger an event
Example I ended up with
class ToDo extends Component {
constructor(props) {
super(props);
this.state = {
list: [
{
title: "Cup cleaning",
todo: "Wash and take away the Kurzhiy's cup from WC"
},
{
title: "Smoking rollton",
todo: "Do some rollton and cigarettes"
},
{
title: "Curious dream",
todo: "Build a time machine"
}
],
title: "",
todo: ""
};
}
createNewToDoItem = () => {
this.setState(({ list, title, todo }) => ({
list: [
...list,
{
title,
todo
}
],
title: "",
todo: ""
}));
};
handleKeyPress = e => {
if (e.target.value !== "") {
if (e.key === "Enter") {
this.createNewToDoItem();
}
}
};
handleTitleInput = e => {
this.setState({
title: e.target.value
});
};
handleTodoInput = e => {
this.setState({
todo: e.target.value
});
};
deleteItem(indexToDelete) {
console.log("HERE");
this.setState(({ list }) => ({
list: list.filter((toDo, index) => index !== indexToDelete)
}));
}
editItem = (i, updTitle, updToDo) => {
let arr = this.state.list;
arr[i].title = updTitle;
arr[i].todo = updToDo;
this.setState({ list: arr });
};
eachToDo = (item, i) => {
return (
<ToDoItem
key={i}
title={item.title}
todo={item.todo}
deleteItem={this.deleteItem.bind(this, i)}
editItem={this.editItem.bind(this, i)}
/>
);
};
render() {
return (
<div className="ToDo">
<h1 className="ToDo-Header" />
<div className="ToDo-Container">
<div className="ToDo-Content">
{this.state.list.map(this.eachToDo)}
</div>
<div>
<input
type="text"
placeholder="Enter new title"
value={this.state.title}
onChange={this.handleTitleInput}
onKeyPress={this.handleKeyPress}
/>
<input
type="text"
placeholder="Enter new todo"
value={this.state.todo}
onChange={this.handleTodoInput}
onKeyPress={this.handleKeyPress}
/>
{/* <AddButton addHandler={this.createNewToDoItem} /> */}
</div>
</div>
</div>
);
}
}
class ToDoItem extends Component {
constructor(props) {
super(props);
this.state = {
editMode: false
};
}
edit = () => {
this.setState({ editMode: true });
};
save = () => {
let updTitle = this.refs.newTitle.value;
let updToDo = this.refs.newToDo.value;
this.props.editItem(updTitle, updToDo);
this.setState({
editMode: false
});
};
renderNormal = () => {
return (
<div className="ToDoItem">
<p className="ToDoItem-Text">{this.props.title}</p>
<p className="ToDoItem-Text">{this.props.todo}</p>
{/* <EditButton editHandler={this.edit} /> */}
<FloatingActionButtons deleteHandler={this.props.deleteItem} />
{/* <button className="ToDoItem-Button" id="editbtn" onClick={this.edit}>✍</button> */}
{/* <button className="ToDoItem-Button" id="delbtn" onClick={this.props.deleteItem}>−</button> */}
</div>
);
};
renderEdit = () => {
return (
<div className="ToDoItem">
<textarea ref="newTitle" defaultValue={this.props.title} />
<textarea ref="newToDo" defaultValue={this.props.todo} />
<button onClick={this.save} className="ToDoItem-Button" id="savebtn">
💾
</button>
</div>
);
};
render() {
if (this.state.editMode) {
return this.renderEdit();
} else {
return this.renderNormal();
}
}
}
const styles = theme => ({
button: {
margin: theme.spacing.unit
}
});
function FloatingActionButtons(props) {
return (
<div>
<Button variant="fab" aria-label="Delete" onClick={props.deleteHandler}>
Delete
</Button>
</div>
);
}
FloatingActionButtons.propTypes = {
classes: PropTypes.object.isRequired
};
I'm a beginner with React creating a chatbot that returns a set of button choices for every action. I'm using Socket.io on the server end and React on the front.
I want every button to have an OnClick function which I'm unable to implement
Here's my data format that server returns
var data = {
buttonShow: true,
buttons: [
{
text: 'I DID NAAAAHT'
},
{
text: 'OH HAI MARK'
}
],
message: {
author: "Mark",
time: '12:00 PM',
message: "Did you hit Lisa?"
}
};
Here is my Chat Window component class
import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router';
import io from "socket.io-client";
class ChatWindow extends React.Component{
constructor(props){
super(props);
this.state = {
email: '',
message: '',
time: '',
buttonShow: false,
buttons: [],
messages: [],
buttonSelection: ''
}
this.socket = io('http://localhost:8080')
this.socket.on('RECEIVE_MESSAGE', function(data){
addMessage(data);
});
const addMessage = data => {
console.log(data);
this.setState({messages: [...this.state.messages, data.message]});
this.setState({buttonShow: data.buttonShow});
this.setState({buttons: data.buttons});
console.log(this.state);
//if(this.state.buttonShow==false){}
};
this.sendButtonSelection = e => {
this.setState({buttonSelection: e.target.value});
console.log(e.target.value);
};
};
render(){
return(
<div>
<div className="panel-body">
{this.state.messages.map(message => {
return(
<ul className="chat">
<li className="">
<div className="chat-body clearfix">
<div className="header">
<strong className="primary-font">{message.author}</strong> <small className="pull-right text-muted">
<span className="glyphicon glyphicon-time"></span>{message.time}</small>
</div>
<p>{message.message}</p>
</div>
</li>
</ul>
)
})}
</div>
<div class="panel-footer">
{<ChatInput myDataProp = {this.state.buttonShow} myButtonProp = {this.state.buttons} onClickProp = {this.sendButtonSelection}/>}
{}
</div>
</div>
);
}
};
class ChatInput extends React.Component {
render(){
if(this.props.myDataProp == false) {
return (
<div className="input-group">
<input id="btn-input" type="text" className="form-control input-sm" placeholder="Type your message here..." />
<span className="input-group-btn">
<button className="btn btn-warning btn-sm" id="btn-chat">
Send</button>
</span>
</div>
)
} else {
return (
<div>
{this.props.myButtonProp.map(function(item, i){
return(
<Button buttonProp = {item.text} buttonOnClick = {this.props.onClickProp}/>
)
})}
</div>
)
}
}
}
class Button extends React.Component {
render(){
return(
<div>
<button type="button" className="btn btn-default btn-round" value={this.props.buttonProp} onClick={this.props.buttonOnClick}>{this.props.buttonProp}</button>
</div>
)
}
}
module.exports = ChatWindow;
However, when this component is rendered, I see this error -
Cannot read property 'props' of undefined
This is killing me. I've tried everything possible.
You could pass thisArg to map Docs.
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
{this.props.myButtonProp.map(function(item, i){
return(
<Button buttonProp = {item.text} buttonOnClick = {this.props.onClickProp}/>
)
}, this)}
Or use arrow function instead.
{this.props.myButtonProp.map((item, i) => (
<Button buttonProp = {item.text} buttonOnClick = {this.props.onClickProp}/>)
)}
If i click on a particular ToDos edit button, its value should be defaulted inside the textarea but everytime the last ToDo is defaulting, can somebody please help, whether using ref is a right choice or something else, then where i m wrong what i'm suppose to do ?
handleEdit() {
e.preventDefault();
.....
}
renderDisplay() {
return(
<div>
{
this.props.listArr.map((list,i) => {
return(
<div key={i} index={i} ref="newText">
<li>{list}
<div>
<button className="btn btn-primary btn-xs glyphicon glyphicon-pencil"
onClick={this.handleEdit.bind(this)}
/>
</div>
<hr/>
</li>
</div>
)})
}
</div>
);
}
renderForm() {
return(
<div>
<textarea className="form-control" defaultValue={this.refs.newText.innerText} rows="1" cols="100" style={{width: 500}}/>
</div>
)
}
render() {
if(this.state.editing) {
return this.renderForm();
}else {
return this.renderDisplay();
}
}
}
First of all you are using an old ref API. You should use this one, where you set the ref to the instance of the class using this with a callback.
<input ref={ref => {this.myInput = ref}} />
And then you can access its value by just referring to this.myInput .
As for your "bug", keep in mind that you are looping over and overriding the ref. so the last ref assignment would be the last item in the array.
this.props.listArr.map((list,i) => {
return(
<div key={i} index={i} ref="newText">
<li>{list}
There will always be 1 newText ref and it will always be the last item in the array.
You should render different ref names according to the item id and then pass the id of the item to the renderForm so it can access the relevant ref.
With that said, i really recommend to extract the todo to a different component as well as the form. I don't see a valid reason to use refs in this case.
Edit
As a follow-up to your comment, here is a small example of how you would use components instead of refs in order to get information from the child like values etc..
class Todo extends React.Component {
onClick = () => {
const { todoId, onClick } = this.props;
onClick(todoId);
}
render() {
const { value, complete } = this.props;
return (
<div
style={{ textDecoration: complete && 'line-through' }}
onClick={this.onClick}
>
{value}
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: [
{ id: '1', value: 'to do this', complete: false },
{ id: '2', value: 'todo that', complete: true },
{ id: '3', value: 'will do it later', complete: false }]
}
}
toggleTodo = (todoId) => {
const { todos } = this.state;
const nextState = todos.map(todo => {
if (todo.id !== todoId) return todo;
return {
...todo,
complete: !todo.complete
}
});
this.setState({ todos: nextState });
}
render() {
const { todos } = this.state;
return (
<div >
{
todos.map((todo) => {
return (
<Todo
complete={todo.complete}
key={todo.id}
todoId={todo.id}
value={todo.value}
onClick={this.toggleTodo}
/>
)
})
}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>